doc: improve documentation of publicly available functions, structs and enums

This commit is contained in:
2025-10-20 15:21:25 +02:00
parent 1b10d350cc
commit c41e806817
5 changed files with 184 additions and 25 deletions

View File

@@ -11,32 +11,58 @@ pub struct O {
pub(crate) items: HashMap<String, Entity>, pub(crate) items: HashMap<String, Entity>,
} }
impl Catalog { impl Catalog {
/// Creates a new `Catalog` instance. /// Creates a new, empty in-memory `Catalog` for the database at the given path.
///
/// This method does not create the database file or connect to it. It only
/// initializes an empty catalog in memory. The database file will be accessed
/// when `init()`, `persist()`, or `load_*` methods are called.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `path` - The path to the SQLite database file. /// * `path` - The path to the SQLite database file that this catalog will manage.
/// ///
/// # Returns /// # Returns
/// ///
/// A new `Catalog` instance. /// A new `Catalog` instance with an empty in-memory item cache.
pub fn new(path: &str) -> Self { pub fn new(path: &str) -> Self {
Self { Self {
path: String::from(path), path: String::from(path),
..O::default() ..O::default()
} }
} }
/// Initializes the database. /// Initializes the database by creating the file and schema if they don't exist.
///
/// This method interacts with the filesystem to ensure the database file and its
/// underlying tables are ready. It has no effect on the in-memory state of the
/// catalog.
///
/// # Effects
///
/// - **Database State:** Creates the SQLite file and required tables if they are not
/// present. If the file already exists, it does nothing.
/// - **In-Memory State:** This method does not alter the in-memory entity cache.
pub fn init(&self) -> result::Result<(), FailedTo> { pub fn init(&self) -> result::Result<(), FailedTo> {
let path = path::Path::new(&self.path); let path = path::Path::new(&self.path);
sqlite::init::db(path).map_err(|_| FailedTo::InitDatabase)?; sqlite::init::db(path).map_err(|_| FailedTo::InitDatabase)?;
Ok(()) Ok(())
} }
/// Inserts or updates a single object that implements the `EAV` trait into the catalog. /// Inserts or updates an object in the in-memory catalog.
///
/// This is a purely in-memory operation. The change will not be written to the
/// database until `persist()` is called.
///
/// # Effects
///
/// - **In-Memory State:**
/// - If the entity does not exist in the catalog, it is added with the state
/// `EntityState::New`.
/// - If the entity already exists, it is overwritten and its state is set to
/// `EntityState::Updated`.
/// - **Database State:** Unchanged. Call `persist()` to save the changes.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `object` - The object to insert. /// * `object` - The object to insert or update, which must implement the `EAV` trait.
pub fn upsert(&mut self, object: impl EAV) -> Result<(), FailedTo> { pub fn upsert(&mut self, object: impl EAV) -> Result<(), FailedTo> {
let mut entity = object.try_into().map_err(|_| FailedTo::ConvertObject)?; let mut entity = object.try_into().map_err(|_| FailedTo::ConvertObject)?;
if self.items.contains_key(&entity.id) { if self.items.contains_key(&entity.id) {
@@ -47,18 +73,29 @@ impl Catalog {
self.items.insert(entity.id.clone(), entity); self.items.insert(entity.id.clone(), entity);
Ok(()) Ok(())
} }
/// Inserts multiple objects that implement the `EAV` trait into the catalog. /// Inserts or updates multiple objects in the in-memory catalog.
///
/// This method calls `upsert()` for each object in the provided vector. Like
/// `upsert()`, this is a purely in-memory operation.
///
/// # Effects
///
/// - **In-Memory State:** Adds or updates multiple entities in the cache.
/// - **Database State:** Unchanged. Call `persist()` to save the changes.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `objects` - A vector of objects to insert. /// * `objects` - A vector of objects to insert or update.
pub fn insert_many(&mut self, objects: Vec<impl EAV>) -> Result<(), FailedTo> { pub fn insert_many(&mut self, objects: Vec<impl EAV>) -> Result<(), FailedTo> {
for object in objects { for object in objects {
self.upsert(object)?; self.upsert(object)?;
} }
Ok(()) Ok(())
} }
/// Retrieves an entity by its ID and converts it into a specified type `T`. /// Retrieves an entity by its ID from the in-memory catalog.
///
/// This is a purely in-memory operation and does not interact with the database.
/// It will only find entities that have been loaded into or created in the catalog.
/// ///
/// # Arguments /// # Arguments
/// ///
@@ -66,7 +103,8 @@ impl Catalog {
/// ///
/// # Returns /// # Returns
/// ///
/// An `Option<T>` containing the converted entity if found, otherwise `None`. /// An `Option<T>` containing the converted entity if found in the in-memory
/// cache, otherwise `None`.
pub fn get<T>(&self, id: &str) -> Result<Option<T>, FailedTo> pub fn get<T>(&self, id: &str) -> Result<Option<T>, FailedTo>
where where
T: EAV, T: EAV,
@@ -76,7 +114,10 @@ impl Catalog {
.map(|e| T::try_from(e.clone()).map_err(|_| FailedTo::ConvertEntity)) .map(|e| T::try_from(e.clone()).map_err(|_| FailedTo::ConvertEntity))
.transpose() .transpose()
} }
/// Retrieves the first entity that matches a given attribute and value. /// Retrieves the first entity from the in-memory catalog that matches a given
/// class, attribute, and value.
///
/// This is a purely in-memory operation and does not interact with the database.
/// ///
/// # Arguments /// # Arguments
/// ///
@@ -85,7 +126,8 @@ impl Catalog {
/// ///
/// # Returns /// # Returns
/// ///
/// An `Option<T>` containing the converted entity if found, otherwise `None`. /// An `Option<T>` containing the first matching converted entity if found,
/// otherwise `None`.
pub fn get_by_class_and_attribute<T>( pub fn get_by_class_and_attribute<T>(
&self, &self,
attribute: &str, attribute: &str,
@@ -103,11 +145,13 @@ impl Catalog {
.map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity)); .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity));
items.next().transpose() items.next().transpose()
} }
/// Returns an iterator over entities of a specific class. /// Returns an iterator over all entities of a specific class in the in-memory catalog.
///
/// This is a purely in-memory operation and does not interact with the database.
/// ///
/// # Returns /// # Returns
/// ///
/// An iterator that yields items of type `T`. /// An iterator that yields items of type `T` from the in-memory cache.
pub fn list_by_class<T>(&self) -> impl Iterator<Item = Result<T, FailedTo>> pub fn list_by_class<T>(&self) -> impl Iterator<Item = Result<T, FailedTo>>
where where
T: EAV, T: EAV,
@@ -117,7 +161,10 @@ impl Catalog {
.filter(move |item| item.class == T::class()) .filter(move |item| item.class == T::class())
.map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity)) .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity))
} }
/// Returns an iterator over entities that match a given attribute and value. /// Returns an iterator over entities in the in-memory catalog that match a given
/// class, attribute, and value.
///
/// This is a purely in-memory operation and does not interact with the database.
/// ///
/// # Arguments /// # Arguments
/// ///
@@ -126,7 +173,7 @@ impl Catalog {
/// ///
/// # Returns /// # Returns
/// ///
/// An iterator that yields items of type `T`. /// An iterator that yields matching items of type `T` from the in-memory cache.
pub fn list_by_class_and_attribute<T>( pub fn list_by_class_and_attribute<T>(
&self, &self,
attribute: &str, attribute: &str,
@@ -142,21 +189,44 @@ impl Catalog {
.filter(move |item| item.value_of(attribute) == Some(&value)) .filter(move |item| item.value_of(attribute) == Some(&value))
.map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity)) .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity))
} }
/// Schedules an entity for deletion. Actual delition will take place when 'persist' is called. /// Marks an entity for deletion from the in-memory catalog.
///
/// This is a purely in-memory operation. The entity will not be removed from the
/// database until `persist()` is called.
///
/// # Effects
///
/// - **In-Memory State:** If the entity exists, its state is set to
/// `EntityState::ToDelete`. If it was in a `New` state, it will simply be
/// forgotten upon the next `persist()` call without ever touching the database.
/// - **Database State:** Unchanged. Call `persist()` to apply the deletion.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `id` - The ID of the entity to delete. /// * `id` - The ID of the entity to mark for deletion.
pub fn delete(&mut self, id: &str) { pub fn delete(&mut self, id: &str) {
let entity = self.items.get_mut(id); let entity = self.items.get_mut(id);
if let Some(entity) = entity { if let Some(entity) = entity {
entity.state = EntityState::ToDelete; entity.state = EntityState::ToDelete;
} }
} }
/// Persists the current state of the catalog to the database. /// Persists all in-memory changes to the database and updates the in-memory state.
/// ///
/// - new entities will be written onto DB /// This method synchronizes the state of the in-memory catalog with the database
/// - marked for delition entities will be deleted /// by writing all pending changes (new, updated, and deleted entities).
///
/// # Effects
///
/// - **Database State:**
/// - Entities marked as `EntityState::New` are inserted into the database.
/// - Entities marked as `EntityState::Updated` are updated in the database.
/// - Entities marked as `EntityState::ToDelete` are deleted from the database.
///
/// - **In-Memory State:**
/// - After a successful database write, entities marked `ToDelete` are permanently
/// removed from the in-memory catalog.
/// - All other entities that were successfully persisted (new or updated) have
/// their state changed to `EntityState::Loaded`.
pub fn persist(&mut self) -> result::Result<(), FailedTo> { pub fn persist(&mut self) -> result::Result<(), FailedTo> {
let path = path::Path::new(&self.path); let path = path::Path::new(&self.path);
sqlite::persist::catalog(path, self).map_err(|_| FailedTo::PersistCatalog)?; sqlite::persist::catalog(path, self).map_err(|_| FailedTo::PersistCatalog)?;
@@ -176,7 +246,21 @@ impl Catalog {
.collect(); .collect();
Ok(()) Ok(())
} }
/// Loads an entity by its ID from the database into the catalog. /// Loads a single entity by its ID from the database into the in-memory catalog.
///
/// This method fetches the entity from the database and updates the in-memory
/// catalog. If an entity with the same ID already exists in memory, it will be
/// **overwritten** with the version from the database.
///
/// # Effects
///
/// - **In-Memory State:**
/// - If an entity is found in the database, it is inserted or updated in the
/// in-memory cache with the state `EntityState::Loaded`.
/// - Any existing in-memory entity with the same ID, regardless of its state
/// (`New`, `Updated`), will be replaced.
/// - If no entity is found in the database, the in-memory cache is not modified.
/// - **Database State:** Unchanged.
/// ///
/// # Arguments /// # Arguments
/// ///
@@ -189,7 +273,19 @@ impl Catalog {
} }
Ok(()) Ok(())
} }
/// Loads all entities of a specific class from the database into the catalog. /// Loads all entities of a specific class from the database into the in-memory catalog.
///
/// This method fetches all entities matching the given class from the database.
/// If any of the loaded entities have IDs that match entities already in the
/// in-memory catalog, the in-memory versions will be **overwritten**.
///
/// # Effects
///
/// - **In-Memory State:**
/// - All found entities are inserted or updated in the in-memory cache with the
/// state `EntityState::Loaded`.
/// - Any existing in-memory entities with matching IDs are replaced.
/// - **Database State:** Unchanged.
pub fn load_by_class<T>(&mut self) -> Result<(), FailedTo> pub fn load_by_class<T>(&mut self) -> Result<(), FailedTo>
where where
T: EAV, T: EAV,
@@ -202,8 +298,24 @@ impl Catalog {
} }
Ok(()) Ok(())
} }
/// Loads all entities matching the filter values /// Loads entities from the database that match a given `Filter`.
/// Different entity class with clashing attribute names might be loaded ///
/// This method queries the database for all entities that satisfy the conditions
/// specified in the filter. If any of the loaded entities have IDs that match
/// entities already in the in-memory catalog, the in-memory versions will be
/// **overwritten**.
///
/// **Warning:** The filter is applied at the attribute level. If different entity
/// classes share attribute names, this method may load entities of multiple
/// classes.
///
/// # Effects
///
/// - **In-Memory State:**
/// - All found entities are inserted or updated in the in-memory cache with the
/// state `EntityState::Loaded`.
/// - Any existing in-memory entities with matching IDs are replaced.
/// - **Database State:** Unchanged.
pub fn load_by_filter(&mut self, filter: &Filter) -> Result<(), FailedTo> { pub fn load_by_filter(&mut self, filter: &Filter) -> Result<(), FailedTo> {
let path = path::Path::new(&self.path); let path = path::Path::new(&self.path);
let entities = sqlite::load::by_filter(path, filter).map_err(|_| FailedTo::LoadFromDB)?; let entities = sqlite::load::by_filter(path, filter).map_err(|_| FailedTo::LoadFromDB)?;

View File

@@ -1,12 +1,25 @@
/// Defines the comparison operators used in a `Filter` condition.
///
/// These operators are used to compare attribute values in the database when
/// loading entities via `load_by_filter`.
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Hash)] #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Hash)]
pub enum E { pub enum E {
/// Exact equality (`==`). Applicable to all value types.
Equal, Equal,
/// Greater than (`>`). Applicable to numeric types.
Greater, Greater,
/// Greater than or equal to (`>=`). Applicable to numeric types.
GreaterOrEqual, GreaterOrEqual,
/// Lesser than (`<`). Applicable to numeric types.
Lesser, Lesser,
/// Lesser than or equal to (`<=`). Applicable to numeric types.
LesserOrEqual, LesserOrEqual,
/// Case-insensitive exact match. Applicable to text values.
IsExactly, IsExactly,
/// Case-insensitive prefix search (`LIKE 'value%'`). Applicable to text values.
StartsWith, StartsWith,
/// Case-insensitive suffix search (`LIKE '%value'`). Applicable to text values.
EndsWith, EndsWith,
/// Case-insensitive substring search (`LIKE '%value%'`). Applicable to text values.
Contains, Contains,
} }

View File

@@ -1,8 +1,17 @@
/// Represents the value part of a `Filter` condition.
///
/// Each variant holds a specific data type to be used in a comparison when
/// querying the database.
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
pub enum E<'a> { pub enum E<'a> {
/// A boolean value (`true` or `false`).
Bool(bool), Bool(bool),
/// A floating-point number (`f64`).
Real(f64), Real(f64),
/// A signed integer (`i64`).
SignedInt(i64), SignedInt(i64),
/// A text value (`&str`).
Text(&'a str), Text(&'a str),
/// An unsigned integer (`u64`), stored as `i64` for SQLite compatibility.
UnsignedInt(i64), UnsignedInt(i64),
} }

View File

@@ -3,10 +3,19 @@ use crate::*;
/// Represents a generic entity with an ID, class, and a set of attributes. /// Represents a generic entity with an ID, class, and a set of attributes.
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct O { pub struct O {
/// The unique identifier for this entity.
pub id: String, pub id: String,
/// The current state of the entity within the `Catalog`'s in-memory cache.
/// This tracks whether the entity is new, modified, or marked for deletion.
pub state: EntityState, pub state: EntityState,
/// An optional timestamp or version number, typically used for optimistic
/// locking or tracking when the entity was last referenced or modified.
pub ref_date: Option<u64>, pub ref_date: Option<u64>,
/// A string identifying the "type" or "class" of the entity (e.g., "product", "user").
/// This is used to group and query entities of the same kind.
pub class: String, pub class: String,
/// A map of attribute names to `Attribute` values, holding the actual data
/// of the entity.
pub attributes: std::collections::HashMap<String, Attribute>, pub attributes: std::collections::HashMap<String, Attribute>,
} }

View File

@@ -1,16 +1,25 @@
use crate::*; use crate::*;
/// A builder for creating complex queries to load entities from the database.
///
/// A `Filter` consists of one or more conditions that are combined with a logical AND.
/// It is used with `Catalog::load_by_filter` to retrieve entities that match
/// all specified criteria.
#[derive(Debug, Default, PartialEq, Clone)] #[derive(Debug, Default, PartialEq, Clone)]
pub struct O<'a> { pub struct O<'a> {
conditions: Vec<(String, Comparison, Condition<'a>)>, conditions: Vec<(String, Comparison, Condition<'a>)>,
} }
impl<'a> Filter<'a> { impl<'a> Filter<'a> {
/// Creates a new, empty `Filter`.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
conditions: Vec::new(), conditions: Vec::new(),
} }
} }
/// Adds a boolean condition to the filter.
///
/// This is a shorthand for `with_bool(name, Comparison::Equal, value)`.
pub fn with_bool(mut self, attribute_name: &str, value: bool) -> Self { pub fn with_bool(mut self, attribute_name: &str, value: bool) -> Self {
self.conditions.push(( self.conditions.push((
attribute_name.to_string(), attribute_name.to_string(),
@@ -19,6 +28,7 @@ impl<'a> Filter<'a> {
)); ));
self self
} }
/// Adds a signed integer (`i64`) condition to the filter.
pub fn with_signed_int( pub fn with_signed_int(
mut self, mut self,
attribute_name: &str, attribute_name: &str,
@@ -32,6 +42,7 @@ impl<'a> Filter<'a> {
)); ));
self self
} }
/// Adds an unsigned integer (`u64`) condition to the filter.
pub fn with_unsigned_int( pub fn with_unsigned_int(
mut self, mut self,
attribute_name: &str, attribute_name: &str,
@@ -45,6 +56,7 @@ impl<'a> Filter<'a> {
)); ));
self self
} }
/// Adds a text (`&str`) condition to the filter.
pub fn with_text( pub fn with_text(
mut self, mut self,
attribute_name: &str, attribute_name: &str,
@@ -58,6 +70,7 @@ impl<'a> Filter<'a> {
)); ));
self self
} }
/// Adds a real number (`f64`) condition to the filter.
pub fn with_real(mut self, attribute_name: &str, comparison: Comparison, value: f64) -> Self { pub fn with_real(mut self, attribute_name: &str, comparison: Comparison, value: f64) -> Self {
self.conditions.push(( self.conditions.push((
attribute_name.to_string(), attribute_name.to_string(),
@@ -66,6 +79,9 @@ impl<'a> Filter<'a> {
)); ));
self self
} }
/// Returns an iterator over the conditions in the filter.
///
/// This is used internally by the persistence engine.
pub(crate) fn conditions(&self) -> impl Iterator<Item = &(String, Comparison, Condition<'a>)> { pub(crate) fn conditions(&self) -> impl Iterator<Item = &(String, Comparison, Condition<'a>)> {
self.conditions.iter() self.conditions.iter()
} }