chore: extract catalog function into their own impl file

also get rid of
- get_by_class_and_attribute
- list_by_class_and_attribute
This commit is contained in:
2025-10-22 06:33:30 +02:00
parent e362188438
commit d4ecd1a6cc
14 changed files with 369 additions and 588 deletions

View File

@@ -0,0 +1,28 @@
use crate::*;
impl Catalog {
/// 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
///
/// * `id` - The ID of the entity to mark for deletion.
pub fn delete(&mut self, id: &str) {
let entity = self.items.get_mut(id);
if let Some(entity) = entity {
entity.state = EntityState::ToDelete;
}
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,29 @@
use crate::*;
impl Catalog {
/// 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
///
/// * `id` - The ID of the entity to retrieve.
///
/// # Returns
///
/// 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>
where
T: EAV,
{
let entity = self.items.get(id);
entity
.map(|e| T::try_from(e.clone()).map_err(|_| FailedTo::ConvertEntity))
.transpose()
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,23 @@
use crate::*;
impl Catalog {
/// 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> {
let path = path::Path::new(&self.path);
sqlite::init::db(path).map_err(|_| FailedTo::InitDatabase)?;
Ok(())
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,26 @@
use crate::*;
impl 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
///
/// * `objects` - A vector of objects to insert or update.
pub fn insert_many(&mut self, objects: Vec<impl EAV>) -> Result<(), FailedTo> {
for object in objects {
self.upsert(object)?;
}
Ok(())
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,23 @@
use crate::*;
impl Catalog {
/// 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
///
/// 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>>
where
T: EAV,
{
self.items
.values()
.filter(move |item| item.class == T::class())
.map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity))
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,28 @@
use crate::*;
impl Catalog {
/// Returns an iterator over all entities of a specific class and subclass
/// in the in-memory catalog.
///
/// This is a purely in-memory operation and does not interact with the database.
///
/// # Returns
///
/// An iterator that yields items of type `T` from the in-memory cache.
pub fn list_by_class_and_subclass<T>(
&self,
subclass: &str,
) -> impl Iterator<Item = Result<T, FailedTo>>
where
T: EAV,
{
self.items
.values()
.filter(move |item| item.class == T::class())
.filter(move |item| item.subclass == Some(subclass.to_string()))
.map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity))
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,32 @@
use crate::*;
impl 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>
where
T: EAV,
{
let class = T::class();
let path = path::Path::new(&self.path);
let entities = sqlite::load::by_class(path, class).map_err(|_| FailedTo::LoadFromDB)?;
for entity in entities {
self.items.insert(entity.id.clone(), entity);
}
Ok(())
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,33 @@
use crate::*;
impl Catalog {
/// Loads entities from the database that match a given `Filter`.
///
/// 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> {
let path = path::Path::new(&self.path);
let entities = sqlite::load::by_filter(path, filter).map_err(|_| FailedTo::LoadFromDB)?;
for entity in entities {
self.items.insert(entity.id.clone(), entity);
}
Ok(())
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,34 @@
use crate::*;
impl 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
///
/// * `id` - The ID of the entity to load.
pub fn load_by_id(&mut self, id: &str) -> Result<(), FailedTo> {
let path = path::Path::new(&self.path);
let entity = sqlite::load::by_id(path, id).map_err(|_| FailedTo::LoadFromDB)?;
if let Some(entity) = entity {
self.items.insert(entity.id.clone(), entity);
}
Ok(())
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,23 @@
use crate::*;
impl Catalog {
/// 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
///
/// * `path` - The path to the SQLite database file that this catalog will manage.
///
/// # Returns
///
/// A new `Catalog` instance with an empty in-memory item cache.
pub fn new(path: &str) -> Self {
Self {
path: String::from(path),
..Catalog::default()
}
}
}

View File

@@ -0,0 +1,43 @@
use crate::*;
impl Catalog {
/// Persists all in-memory changes to the database and updates the in-memory state.
///
/// This method synchronizes the state of the in-memory catalog with the database
/// 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> {
let path = path::Path::new(&self.path);
sqlite::persist::catalog(path, self).map_err(|_| FailedTo::PersistCatalog)?;
// cleaning catalog state after db write
self.items = self
.items
.extract_if(|_, item| item.state != EntityState::ToDelete)
.map(|(k, item)| {
(
k,
Entity {
state: EntityState::Loaded,
..item
},
)
})
.collect();
Ok(())
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,34 @@
use crate::*;
impl 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
///
/// * `object` - The object to insert or update, which must implement the `EAV` trait.
pub fn upsert(&mut self, object: impl EAV) -> Result<(), FailedTo> {
let mut entity = object.try_into().map_err(|_| FailedTo::ConvertObject)?;
if self.items.contains_key(&entity.id) {
entity.state = EntityState::Updated;
} else {
entity.state = EntityState::New;
}
self.items.insert(entity.id.clone(), entity);
Ok(())
}
}
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -1,4 +1,16 @@
pub mod bool_try_from_value; pub mod bool_try_from_value;
pub mod catalog_delete;
pub mod catalog_get;
pub mod catalog_init;
pub mod catalog_insert_many;
pub mod catalog_list_by_class;
pub mod catalog_list_by_class_and_subclass;
pub mod catalog_load_by_class;
pub mod catalog_load_by_filter;
pub mod catalog_load_by_id;
pub mod catalog_new;
pub mod catalog_persist;
pub mod catalog_upsert;
pub mod f64_try_from_value; pub mod f64_try_from_value;
pub mod i32_try_from_value; pub mod i32_try_from_value;
pub mod i64_try_from_value; pub mod i64_try_from_value;

View File

@@ -7,345 +7,9 @@ use std::collections::HashMap;
/// them, as well as to persist changes to and load data from a database file. /// them, as well as to persist changes to and load data from a database file.
#[derive(Debug, Default, PartialEq, Clone)] #[derive(Debug, Default, PartialEq, Clone)]
pub struct O { pub struct O {
path: String, pub(crate) path: String,
pub(crate) items: HashMap<String, Entity>, pub(crate) items: HashMap<String, Entity>,
} }
impl Catalog {
/// 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
///
/// * `path` - The path to the SQLite database file that this catalog will manage.
///
/// # Returns
///
/// A new `Catalog` instance with an empty in-memory item cache.
pub fn new(path: &str) -> Self {
Self {
path: String::from(path),
..O::default()
}
}
/// 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> {
let path = path::Path::new(&self.path);
sqlite::init::db(path).map_err(|_| FailedTo::InitDatabase)?;
Ok(())
}
/// 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
///
/// * `object` - The object to insert or update, which must implement the `EAV` trait.
pub fn upsert(&mut self, object: impl EAV) -> Result<(), FailedTo> {
let mut entity = object.try_into().map_err(|_| FailedTo::ConvertObject)?;
if self.items.contains_key(&entity.id) {
entity.state = EntityState::Updated;
} else {
entity.state = EntityState::New;
}
self.items.insert(entity.id.clone(), entity);
Ok(())
}
/// 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
///
/// * `objects` - A vector of objects to insert or update.
pub fn insert_many(&mut self, objects: Vec<impl EAV>) -> Result<(), FailedTo> {
for object in objects {
self.upsert(object)?;
}
Ok(())
}
/// 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
///
/// * `id` - The ID of the entity to retrieve.
///
/// # Returns
///
/// 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>
where
T: EAV,
{
let entity = self.items.get(id);
entity
.map(|e| T::try_from(e.clone()).map_err(|_| FailedTo::ConvertEntity))
.transpose()
}
/// 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
///
/// * `attribute` - The attribute of the entity to match.
/// * `value` - The value of the attribute to match.
///
/// # Returns
///
/// An `Option<T>` containing the first matching converted entity if found,
/// otherwise `None`.
pub fn get_by_class_and_attribute<T>(
&self,
attribute: &str,
value: impl Into<Value> + Clone,
) -> Result<Option<T>, FailedTo>
where
T: EAV,
{
let mut items = self
.items
.values()
.filter(|item| item.class == T::class())
.filter(|item| item.value_of(attribute) == Some(&value.clone().into()))
.take(1)
.map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity));
items.next().transpose()
}
/// 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
///
/// 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>>
where
T: EAV,
{
self.items
.values()
.filter(move |item| item.class == T::class())
.map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity))
}
/// Returns an iterator over all entities of a specific class and subclass
/// in the in-memory catalog.
///
/// This is a purely in-memory operation and does not interact with the database.
///
/// # Returns
///
/// An iterator that yields items of type `T` from the in-memory cache.
pub fn list_by_class_and_subclass<T>(
&self,
subclass: &str,
) -> impl Iterator<Item = Result<T, FailedTo>>
where
T: EAV,
{
self.items
.values()
.filter(move |item| item.class == T::class())
.filter(move |item| item.subclass == Some(subclass.to_string()))
.map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity))
}
/// 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
///
/// * `attribute` - The attribute of the entities to match.
/// * `value` - The value of the attribute to match.
///
/// # Returns
///
/// An iterator that yields matching items of type `T` from the in-memory cache.
pub fn list_by_class_and_attribute<T>(
&self,
attribute: &str,
value: impl Into<Value> + Clone,
) -> impl Iterator<Item = Result<T, FailedTo>>
where
T: EAV,
{
let value: Value = value.into();
self.items
.values()
.filter(move |item| item.class == T::class())
.filter(move |item| item.value_of(attribute) == Some(&value))
.map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity))
}
/// 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
///
/// * `id` - The ID of the entity to mark for deletion.
pub fn delete(&mut self, id: &str) {
let entity = self.items.get_mut(id);
if let Some(entity) = entity {
entity.state = EntityState::ToDelete;
}
}
/// Persists all in-memory changes to the database and updates the in-memory state.
///
/// This method synchronizes the state of the in-memory catalog with the database
/// 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> {
let path = path::Path::new(&self.path);
sqlite::persist::catalog(path, self).map_err(|_| FailedTo::PersistCatalog)?;
// cleaning catalog state after db write
self.items = self
.items
.extract_if(|_, item| item.state != EntityState::ToDelete)
.map(|(k, item)| {
(
k,
Entity {
state: EntityState::Loaded,
..item
},
)
})
.collect();
Ok(())
}
/// 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
///
/// * `id` - The ID of the entity to load.
pub fn load_by_id(&mut self, id: &str) -> Result<(), FailedTo> {
let path = path::Path::new(&self.path);
let entity = sqlite::load::by_id(path, id).map_err(|_| FailedTo::LoadFromDB)?;
if let Some(entity) = entity {
self.items.insert(entity.id.clone(), entity);
}
Ok(())
}
/// 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>
where
T: EAV,
{
let class = T::class();
let path = path::Path::new(&self.path);
let entities = sqlite::load::by_class(path, class).map_err(|_| FailedTo::LoadFromDB)?;
for entity in entities {
self.items.insert(entity.id.clone(), entity);
}
Ok(())
}
/// Loads entities from the database that match a given `Filter`.
///
/// 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> {
let path = path::Path::new(&self.path);
let entities = sqlite::load::by_filter(path, filter).map_err(|_| FailedTo::LoadFromDB)?;
for entity in entities {
self.items.insert(entity.id.clone(), entity);
}
Ok(())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -582,103 +246,6 @@ mod tests {
let retrieved_item: Option<Item> = catalog.get("nonexistent-id").unwrap(); let retrieved_item: Option<Item> = catalog.get("nonexistent-id").unwrap();
assert!(retrieved_item.is_none()); assert!(retrieved_item.is_none());
} }
// ## 'get_by_class_and_attribute()'
#[test]
fn get_by_class_and_attribute_should_retrieve_correct_entity() {
// Should retrieve the first entity matching the class, attribute, and value.
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-1".to_string(),
name: "Test Item".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item2 = Item {
id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Unique Item".to_string(),
price: 200,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog.upsert(item1.clone());
let _ = catalog.upsert(item2.clone());
let retrieved_item: Option<Item> = catalog
.get_by_class_and_attribute("name", "Unique Item")
.unwrap();
assert_eq!(retrieved_item, Some(item2));
}
#[test]
fn get_by_class_and_attribute_should_work_with_different_value_types() {
// Should work with different value types (String, u64, bool).
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(),
price: 100,
sell_trend: 10,
in_stock: true,
..Item::default()
};
let item2 = Item {
id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(),
price: 250,
sell_trend: 20,
in_stock: false,
..Item::default()
};
let _ = catalog.upsert(item1.clone());
let _ = catalog.upsert(item2.clone());
// Test with &str for String attribute
let retrieved_by_name: Option<Item> = catalog
.get_by_class_and_attribute("name", "Item One")
.unwrap();
assert_eq!(retrieved_by_name, Some(item1.clone()));
// Test with u64 for price attribute
let retrieved_by_price: Option<Item> =
catalog.get_by_class_and_attribute("price", 250u64).unwrap();
assert_eq!(retrieved_by_price, Some(item2.clone()));
// Test with bool for in_stock attribute
let retrieved_by_stock: Option<Item> = catalog
.get_by_class_and_attribute("in_stock", true)
.unwrap();
assert_eq!(retrieved_by_stock, Some(item1.clone()));
// Test with i64 for sell_trend attribute
let retrieved_by_sell_trend: Option<Item> = catalog
.get_by_class_and_attribute("sell_trend", 10i64)
.unwrap();
assert_eq!(retrieved_by_sell_trend, Some(item1.clone()));
}
#[test]
fn get_by_class_and_attribute_should_return_none_if_no_match() {
// Should return 'None' if no entity matches the criteria.
let mut catalog = Catalog::new("dummy.db");
let item = Item {
id: "item-1".to_string(),
name: "Test Item".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog.upsert(item.clone());
// Test with a value that doesn't exist
let retrieved_item: Option<Item> = catalog
.get_by_class_and_attribute("name", "Non-existent Name")
.unwrap();
assert!(retrieved_item.is_none());
// Test with an attribute that doesn't exist
let retrieved_item_2: Option<Item> = catalog
.get_by_class_and_attribute("non-existent-attribute", "Test Item")
.unwrap();
assert!(retrieved_item_2.is_none());
}
// ## 'list_by_class()' // ## 'list_by_class()'
#[test] #[test]
fn list_by_class_should_return_all_entities_of_class() { fn list_by_class_should_return_all_entities_of_class() {
@@ -722,83 +289,6 @@ mod tests {
.collect(); .collect();
assert!(results.is_empty()); assert!(results.is_empty());
} }
// ## 'list_by_class_and_attribute()'
#[test]
fn list_by_class_and_attribute_should_return_all_matching_entities() {
// Should return an iterator with all entities matching the class, attribute, and value.
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item2 = Item {
id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(),
price: 200,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let item3 = Item {
id: "item-3".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Three".to_string(),
price: 300,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog.upsert(item1.clone());
let _ = catalog.upsert(item2.clone());
let _ = catalog.upsert(item3.clone());
let results: Vec<Item> = catalog
.list_by_class_and_attribute("in_stock", true)
.map(|item| item.unwrap())
.collect();
assert_eq!(results.len(), 2);
assert!(results.contains(&item1));
assert!(results.contains(&item3));
assert!(!results.contains(&item2));
}
#[test]
fn list_by_class_and_attribute_should_return_empty_iterator_if_no_match() {
// Should return an empty iterator if no entities match.
let mut catalog = Catalog::new("dummy.db");
let item = Item {
id: "item-1".to_string(),
name: "Test Item".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog.upsert(item);
// Search for a value that doesn't exist
let results: Vec<Item> = catalog
.list_by_class_and_attribute("in_stock", false)
.map(|item| item.unwrap())
.collect();
assert!(results.is_empty());
// Search for an attribute that doesn't exist
let results_2: Vec<Item> = catalog
.list_by_class_and_attribute("non-existent-attribute", true)
.map(|item| item.unwrap())
.collect();
assert!(results_2.is_empty());
// Search in a completely empty catalog
let empty_catalog = Catalog::new("dummy.db");
let results_3: Vec<Item> = empty_catalog
.list_by_class_and_attribute("any_attribute", "any_value")
.map(|item| item.unwrap())
.collect();
assert!(results_3.is_empty());
}
// ## 'delete()' // ## 'delete()'
#[test] #[test]
fn delete_should_mark_entity_as_to_delete() { fn delete_should_mark_entity_as_to_delete() {
@@ -2326,83 +1816,6 @@ mod tests {
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
fn integration_test_insert_get_by_class_and_attribute() {
// Scenario: 'insert' -> 'get_by_class_and_attribute' should return the correct item.
// This test focuses on in-memory functionality.
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-1".to_string(),
name: "First Item".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item2 = Item {
id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Second Item".to_string(),
price: 200,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let _ = catalog.upsert(item1.clone());
let _ = catalog.upsert(item2.clone());
// Retrieve by a unique attribute
let retrieved_item: Option<Item> = catalog
.get_by_class_and_attribute("name", "Second Item")
.unwrap();
// Verify the correct item was retrieved
assert_eq!(retrieved_item, Some(item2));
}
#[test]
fn integration_test_insert_many_list_by_class_and_attribute() {
// Scenario: 'insert_many' -> 'list_by_class_and_attribute' should return all matching items.
// This test focuses on in-memory functionality.
let mut catalog = Catalog::new("dummy.db");
let item1 = Item {
id: "item-1".to_string(),
subclass: Some("subitem".to_string()),
name: "Item One".to_string(),
price: 100,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let item2 = Item {
id: "item-2".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Two".to_string(),
price: 200,
sell_trend: 0,
in_stock: false,
..Item::default()
};
let item3 = Item {
id: "item-3".to_string(),
subclass: Some("subitem".to_string()),
name: "Item Three".to_string(),
price: 150,
sell_trend: 0,
in_stock: true,
..Item::default()
};
let _ = catalog.insert_many(vec![item1.clone(), item2.clone(), item3.clone()]);
// List all items that are in stock
let mut results: Vec<Item> = catalog
.list_by_class_and_attribute("in_stock", true)
.map(|item| item.unwrap())
.collect();
// Sort for deterministic comparison
results.sort_by(|a, b| a.id.cmp(&b.id));
let mut expected = vec![item1, item3];
expected.sort_by(|a, b| a.id.cmp(&b.id));
// Verify the correct items were retrieved
assert_eq!(results.len(), 2);
assert_eq!(results, expected);
}
#[test]
#[ignore] // This test can be flaky as it depends on thread scheduling. #[ignore] // This test can be flaky as it depends on thread scheduling.
fn integration_test_concurrency() { fn integration_test_concurrency() {
// Scenario: Concurrency - what happens if two 'Catalog' instances point to the same file? // Scenario: Concurrency - what happens if two 'Catalog' instances point to the same file?