feat: add catalog.get_by in order to get items given a predicate

This commit is contained in:
2025-11-11 12:21:18 +01:00
parent 2aa1e7cb46
commit 2ecee20145
10 changed files with 128 additions and 2 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,45 @@
use crate::*;
impl Catalog {
/// Retrieves the first entity from the in-memory catalog that satisfies a given predicate.
///
/// This is a purely in-memory operation and does not interact with the database.
/// It iterates through all entities currently loaded in the catalog, attempts to
/// convert each to the specified type `T`, and then applies the provided
/// `predicate` function. The first entity that successfully converts and
/// satisfies the predicate is returned.
///
/// Entities that fail to convert to type `T` are silently skipped and do not
/// cause an error to be returned by this function.
///
/// # Type Parameters
///
/// * `T` - The target type to convert the entity into, which must implement the `EAV` trait.
/// * `F` - The type of the predicate closure, which takes a reference to `T` and returns a `bool`.
///
/// # Arguments
///
/// * `predicate` - A closure that defines the condition an entity must meet to be returned.
///
/// # Returns
///
/// An `Ok(Some(T))` containing the first converted entity that satisfies the predicate,
/// or `Ok(None)` if no such entity is found or if all matching entities fail conversion.
///
/// # Errors
///
/// Returns `Err(FailedTo)` if an internal error occurs during the `with_items` operation
/// (e.g., mutex poisoning).
pub fn get_by<T, F>(&self, predicate: F) -> Result<Option<T>, FailedTo>
where
T: EAV,
F: Fn(&T) -> bool,
{
self.with_items(|items| {
Ok(items
.values()
.flat_map(|entity| T::try_from(entity.clone()))
.find(|item| predicate(item)))
})
}
}

View File

@@ -2,6 +2,7 @@ pub mod bool_try_from_value;
pub mod catalog_contains_key; pub mod catalog_contains_key;
pub mod catalog_delete; pub mod catalog_delete;
pub mod catalog_get; pub mod catalog_get;
pub mod catalog_get_by;
pub mod catalog_init; pub mod catalog_init;
pub mod catalog_insert_many; pub mod catalog_insert_many;
pub mod catalog_is_empty; pub mod catalog_is_empty;

View File

@@ -29,7 +29,6 @@ impl From<Item> for Entity {
let mut entity = Entity::new::<Item>() let mut entity = Entity::new::<Item>()
.with_id(&value.id) .with_id(&value.id)
.with_ref_date(value.first_seen) .with_ref_date(value.first_seen)
.with_subclass("subitem")
.with_attribute("name", value.name) .with_attribute("name", value.name)
.with_attribute("price", value.price) .with_attribute("price", value.price)
.with_attribute("discount", value.discount) .with_attribute("discount", value.discount)

View File

@@ -0,0 +1,80 @@
#[cfg(test)]
mod tests {
use crate::*;
fn catalog(name: &str) -> Catalog {
let path = format!("{}.db", name);
let fs_path = path::Path::new(&path);
if fs_path.exists() {
std::fs::remove_file(fs_path).unwrap();
}
let catalog = Catalog::new(&path);
catalog.init().unwrap();
catalog
}
#[test]
fn get_by_finds_item() {
let mut catalog = catalog("get_by_finds_item");
let item1 = Item {
id: "1".to_string(),
name: "one".to_string(),
..Default::default()
};
let item2 = Item {
id: "2".to_string(),
name: "two".to_string(),
..Default::default()
};
catalog
.insert_many(vec![item1.clone(), item2.clone()])
.unwrap();
let found = catalog
.get_by(|item: &Item| item.name == "two")
.unwrap()
.unwrap();
assert_eq!(found, item2);
}
#[test]
fn get_by_returns_none_when_no_match() {
let mut catalog = catalog("get_by_returns_none_when_no_match");
let item1 = Item {
id: "1".to_string(),
name: "one".to_string(),
..Default::default()
};
catalog.insert_many(vec![item1.clone()]).unwrap();
let found = catalog.get_by(|item: &Item| item.name == "two").unwrap();
assert!(found.is_none());
}
#[test]
fn get_by_on_empty_catalog_returns_none() {
let catalog = catalog("get_by_on_empty_catalog_returns_none");
let found = catalog.get_by(|item: &Item| item.name == "any").unwrap();
assert!(found.is_none());
}
#[test]
fn get_by_multiple_matches() {
let mut catalog = catalog("get_by_multiple_matches");
let item1 = Item {
id: "1".to_string(),
name: "match".to_string(),
price: 10,
..Default::default()
};
let item2 = Item {
id: "2".to_string(),
name: "match".to_string(),
price: 20,
..Default::default()
};
catalog
.insert_many(vec![item1.clone(), item2.clone()])
.unwrap();
// `get_by` should return the first match it finds. The order is not guaranteed
// by the underlying HashMap, so we just check that it returns one of them.
let found = catalog
.get_by(|item: &Item| item.name == "match")
.unwrap()
.unwrap();
assert!(found == item1 || found == item2);
}
}

View File

@@ -1462,7 +1462,7 @@ mod tests {
}; };
let item4 = Item { let item4 = Item {
id: "item-4".to_string(), id: "item-4".to_string(),
subclass: None, // -> "subitem" subclass: Some("subitem".to_string()),
..Default::default() ..Default::default()
}; };
let another_item = AnotherItem { let another_item = AnotherItem {

View File

@@ -1,5 +1,6 @@
pub mod catalog_delete; pub mod catalog_delete;
pub mod catalog_get; pub mod catalog_get;
pub mod catalog_get_by;
pub mod catalog_init; pub mod catalog_init;
pub mod catalog_insert_many; pub mod catalog_insert_many;
pub mod catalog_integration; pub mod catalog_integration;