feat: set catalog as thread safe

wip: add a thread safe way to access items; change upsert

wip: change catalog.delete to be thread safe

wip: change catalog.load_by_* to be thread safe

wip: add a thread safe way to access items in readonly mode; change
catalog.get

wip: change catalog.persist to be thread safe

wip: disable examples temprarily

wip: remove catalog_new.rs file

wip: ignore result of iteration

wip: disable lib example

wip: enable catalog.delete tests

wip: enable catalog.get tests

wip enalbe catalog.init tests

wip: enable catalog.insert_many tests

wip: enable catalog.load_by_class tests

wip: enable catalog.load_by_id tests

wip: add len() and is_empty() convenience thread safe method to catalog

wip: use convenience methods for len and is_empty checks

wip: enable catalog.load_by_id and catalog.new tests

wip: enable catalog.upsert tests

wip: enable catalog.persist test; add catalog.contains_key

wip: enable catalog.load_by_filter tests

wip: enable sqlite_* tests

wip: enable list_by_class with tests

wip: enable list_by_class_and_subclass with tests

wip: enable integration tests

wip: switch iter to into_iter for tests
This commit is contained in:
2025-10-29 07:47:25 +01:00
parent a2ad264b39
commit bc4ac445c9
32 changed files with 964 additions and 830 deletions

View File

@@ -1,159 +1,160 @@
use heave::*; fn main() {}
// use heave::*;
// Define a struct named `Component` to represent an electronic component. //
#[derive(Debug, Clone, PartialEq)] // // Define a struct named `Component` to represent an electronic component.
struct Component { // #[derive(Debug, Clone, PartialEq)]
pub id: String, // struct Component {
pub part_number: String, // pub id: String,
pub kind: String, // pub part_number: String,
pub value: u64, // pub kind: String,
pub package: String, // pub value: u64,
pub in_stock: bool, // pub package: String,
} // pub in_stock: bool,
// Implement the `EAV` trait for the `Component` struct. // }
impl EAV for Component { // // Implement the `EAV` trait for the `Component` struct.
// `class` is a function that returns the class name of the entity. // impl EAV for Component {
fn class() -> &'static str { // // `class` is a function that returns the class name of the entity.
"component" // fn class() -> &'static str {
} // "component"
} // }
// Implement the `From<Component>` trait for the `Entity` struct. // }
impl From<Component> for Entity { // // Implement the `From<Component>` trait for the `Entity` struct.
// `from` is a function that converts a `Component` into an `Entity`. // impl From<Component> for Entity {
fn from(value: Component) -> Entity { // // `from` is a function that converts a `Component` into an `Entity`.
Entity::new::<Component>() // fn from(value: Component) -> Entity {
.with_id(&value.id) // Entity::new::<Component>()
.with_attribute("part_number", value.part_number) // .with_id(&value.id)
.with_attribute("kind", value.kind) // .with_attribute("part_number", value.part_number)
.with_attribute("value", value.value) // .with_attribute("kind", value.kind)
.with_attribute("package", value.package) // .with_attribute("value", value.value)
.with_attribute("in_stock", value.in_stock) // .with_attribute("package", value.package)
} // .with_attribute("in_stock", value.in_stock)
} // }
// Implement the `From<Entity>` trait for the `Component` struct. // }
impl From<Entity> for Component { // // Implement the `From<Entity>` trait for the `Component` struct.
// `from` is a function that converts an `Entity` into a `Component`. // impl From<Entity> for Component {
fn from(value: Entity) -> Self { // // `from` is a function that converts an `Entity` into a `Component`.
Self { // fn from(value: Entity) -> Self {
id: value.id.clone(), // Self {
part_number: value // id: value.id.clone(),
.unwrap("part_number") // part_number: value
.expect("part_number is always present"), // .unwrap("part_number")
kind: value.unwrap("kind").expect("kind is always present"), // .expect("part_number is always present"),
value: value.unwrap("value").expect("value is always present"), // kind: value.unwrap("kind").expect("kind is always present"),
package: value.unwrap("package").expect("package is always present"), // value: value.unwrap("value").expect("value is always present"),
in_stock: value // package: value.unwrap("package").expect("package is always present"),
.unwrap("in_stock") // in_stock: value
.expect("in_stock is always present"), // .unwrap("in_stock")
} // .expect("in_stock is always present"),
} // }
} // }
// }
fn main() { //
// Define the path for the SQLite database file. // fn main() {
let db_path = "./using_filters.sqlite3"; // // Define the path for the SQLite database file.
// Create a new `Catalog` instance with the specified database path. // let db_path = "./using_filters.sqlite3";
let mut catalog = Catalog::new(db_path); // // Create a new `Catalog` instance with the specified database path.
// Initialize the catalog, which sets up the database. // let mut catalog = Catalog::new(db_path);
catalog.init().unwrap(); // // Initialize the catalog, which sets up the database.
// Create some component instances. // catalog.init().unwrap();
let components_to_add = vec![ // // Create some component instances.
// This one should be found // let components_to_add = vec![
Component { // // This one should be found
id: "R1".to_string(), // Component {
part_number: "R-10K-0805".to_string(), // id: "R1".to_string(),
kind: "resistor".to_string(), // part_number: "R-10K-0805".to_string(),
value: 10000, // kind: "resistor".to_string(),
package: "smd-0805".to_string(), // value: 10000,
in_stock: true, // package: "smd-0805".to_string(),
}, // in_stock: true,
// This one should be found // },
Component { // // This one should be found
id: "R2".to_string(), // Component {
part_number: "R-4K7-0805".to_string(), // id: "R2".to_string(),
kind: "resistor".to_string(), // part_number: "R-4K7-0805".to_string(),
value: 4700, // kind: "resistor".to_string(),
package: "smd-0805".to_string(), // value: 4700,
in_stock: true, // package: "smd-0805".to_string(),
}, // in_stock: true,
// This one should NOT be found (wrong kind) // },
Component { // // This one should NOT be found (wrong kind)
id: "C1".to_string(), // Component {
part_number: "C-100n-0603".to_string(), // id: "C1".to_string(),
kind: "capacitor".to_string(), // part_number: "C-100n-0603".to_string(),
value: 100, // kind: "capacitor".to_string(),
package: "smd-0603".to_string(), // value: 100,
in_stock: true, // package: "smd-0603".to_string(),
}, // in_stock: true,
// This one should NOT be found (value too low) // },
Component { // // This one should NOT be found (value too low)
id: "R3".to_string(), // Component {
part_number: "R-100-0805".to_string(), // id: "R3".to_string(),
kind: "resistor".to_string(), // part_number: "R-100-0805".to_string(),
value: 100, // kind: "resistor".to_string(),
package: "smd-0805".to_string(), // value: 100,
in_stock: true, // package: "smd-0805".to_string(),
}, // in_stock: true,
// This one should NOT be found (not in stock) // },
Component { // // This one should NOT be found (not in stock)
id: "R4".to_string(), // Component {
part_number: "R-22K-TH".to_string(), // id: "R4".to_string(),
kind: "resistor".to_string(), // part_number: "R-22K-TH".to_string(),
value: 22000, // kind: "resistor".to_string(),
package: "through-hole".to_string(), // value: 22000,
in_stock: false, // package: "through-hole".to_string(),
}, // in_stock: false,
// This one should NOT be found (wrong kind, even if other fields match) // },
Component { // // This one should NOT be found (wrong kind, even if other fields match)
id: "L1".to_string(), // Component {
part_number: "L-10mH-TH".to_string(), // id: "L1".to_string(),
kind: "inductor".to_string(), // part_number: "L-10mH-TH".to_string(),
value: 10000, // kind: "inductor".to_string(),
package: "through-hole".to_string(), // value: 10000,
in_stock: true, // package: "through-hole".to_string(),
}, // in_stock: true,
]; // },
// Insert the components into the catalog. // ];
catalog.insert_many(components_to_add.clone()).unwrap(); // // Insert the components into the catalog.
// Persist the changes to the database. // catalog.insert_many(components_to_add.clone()).unwrap();
catalog.persist().unwrap(); // // Persist the changes to the database.
// Create a new catalog to ensure we are loading from the database. // catalog.persist().unwrap();
let mut new_catalog = Catalog::new(db_path); // // Create a new catalog to ensure we are loading from the database.
// Create a composite filter. // let mut new_catalog = Catalog::new(db_path);
// We are looking for resistors with a value greater than 1000 that are in stock. // // Create a composite filter.
let filter = Filter::new() // // We are looking for resistors with a value greater than 1000 that are in stock.
.with_text("kind", Comparison::IsExactly, "resistor") // let filter = Filter::new()
.with_unsigned_int("value", Comparison::Greater, 1000) // .with_text("kind", Comparison::IsExactly, "resistor")
.with_bool("in_stock", true); // .with_unsigned_int("value", Comparison::Greater, 1000)
// Load entities from the database using the filter. // .with_bool("in_stock", true);
new_catalog.load_by_filter(&filter).unwrap(); // // Load entities from the database using the filter.
// Get the list of loaded components. // new_catalog.load_by_filter(&filter).unwrap();
let loaded_components: Vec<Component> = new_catalog // // Get the list of loaded components.
.list_by_class::<Component>() // let loaded_components: Vec<Component> = new_catalog
.map(|c| c.unwrap()) // .list_by_class::<Component>()
.collect(); // .map(|c| c.unwrap())
// Print the loaded components // .collect();
println!( // // Print the loaded components
"Found {} components matching the filter:", // println!(
loaded_components.len() // "Found {} components matching the filter:",
); // loaded_components.len()
for component in &loaded_components { // );
println!( // for component in &loaded_components {
"- ID: {}, Part Number: {}, Kind: {}, Value: {}, Package: {}, In Stock: {}", // println!(
component.id, // "- ID: {}, Part Number: {}, Kind: {}, Value: {}, Package: {}, In Stock: {}",
component.part_number, // component.id,
component.kind, // component.part_number,
component.value, // component.kind,
component.package, // component.value,
component.in_stock // component.package,
); // component.in_stock
} // );
// Verify that we have loaded the correct number of components. // }
assert_eq!(loaded_components.len(), 2); // // Verify that we have loaded the correct number of components.
// Verify that the correct components were loaded. // assert_eq!(loaded_components.len(), 2);
let ids: Vec<String> = loaded_components.iter().map(|c| c.id.clone()).collect(); // // Verify that the correct components were loaded.
assert!(ids.contains(&"R1".to_string())); // let ids: Vec<String> = loaded_components.iter().map(|c| c.id.clone()).collect();
assert!(ids.contains(&"R2".to_string())); // assert!(ids.contains(&"R1".to_string()));
// Clean up the database file. // assert!(ids.contains(&"R2".to_string()));
std::fs::remove_file(db_path).unwrap(); // // Clean up the database file.
} // std::fs::remove_file(db_path).unwrap();
// }

View File

@@ -1,227 +1,228 @@
use heave::*; fn main() {}
use std::path::Path; // use heave::*;
// use std::path::Path;
struct Laptop { //
pub id: String, // struct Laptop {
pub model: String, // pub id: String,
pub price: u64, // pub model: String,
} // pub price: u64,
struct Display { // }
pub id: String, // struct Display {
pub model: String, // pub id: String,
pub resolution: f64, // pub model: String,
pub price: u64, // pub resolution: f64,
} // pub price: u64,
struct Mouse { // }
pub id: String, // struct Mouse {
pub model: String, // pub id: String,
pub wireless: bool, // pub model: String,
pub price: u64, // pub wireless: bool,
} // pub price: u64,
enum Product { // }
None, // enum Product {
Laptop(Laptop), // None,
Display(Display), // Laptop(Laptop),
Mouse(Mouse), // Display(Display),
} // Mouse(Mouse),
impl EAV for Product { // }
fn class() -> &'static str { // impl EAV for Product {
"product" // fn class() -> &'static str {
} // "product"
} // }
impl From<Entity> for Product { // }
fn from(value: Entity) -> Self { // impl From<Entity> for Product {
if let Some(ref subclass) = value.subclass { // fn from(value: Entity) -> Self {
match subclass.as_ref() { // if let Some(ref subclass) = value.subclass {
"laptop" => Product::Laptop(Laptop { // match subclass.as_ref() {
id: value.id.clone(), // "laptop" => Product::Laptop(Laptop {
model: value.unwrap("model").expect("model is mandatory"), // id: value.id.clone(),
price: value.unwrap("price").expect("price is mandatory"), // model: value.unwrap("model").expect("model is mandatory"),
}), // price: value.unwrap("price").expect("price is mandatory"),
"display" => Product::Display(Display { // }),
id: value.id.clone(), // "display" => Product::Display(Display {
model: value.unwrap("model").expect("model is mandatory"), // id: value.id.clone(),
price: value.unwrap("price").expect("price is mandatory"), // model: value.unwrap("model").expect("model is mandatory"),
resolution: value.unwrap("resolution").expect("resolution is mandatory"), // price: value.unwrap("price").expect("price is mandatory"),
}), // resolution: value.unwrap("resolution").expect("resolution is mandatory"),
"mouse" => Product::Mouse(Mouse { // }),
id: value.id.clone(), // "mouse" => Product::Mouse(Mouse {
model: value.unwrap("model").expect("model is mandatory"), // id: value.id.clone(),
price: value.unwrap("price").expect("price is mandatory"), // model: value.unwrap("model").expect("model is mandatory"),
wireless: value.unwrap("wireless").expect("wireless is mandatory"), // price: value.unwrap("price").expect("price is mandatory"),
}), // wireless: value.unwrap("wireless").expect("wireless is mandatory"),
_ => unreachable!(), // }),
} // _ => unreachable!(),
} else { // }
Product::None // } else {
} // Product::None
} // }
} // }
impl From<Product> for Entity { // }
fn from(value: Product) -> Self { // impl From<Product> for Entity {
match value { // fn from(value: Product) -> Self {
Product::Laptop(value) => Entity::new::<Product>() // match value {
.with_id(&value.id) // Product::Laptop(value) => Entity::new::<Product>()
.with_subclass("laptop") // .with_id(&value.id)
.with_attribute("model", value.model) // .with_subclass("laptop")
.with_attribute("price", value.price), // .with_attribute("model", value.model)
Product::Display(value) => Entity::new::<Product>() // .with_attribute("price", value.price),
.with_id(&value.id) // Product::Display(value) => Entity::new::<Product>()
.with_subclass("display") // .with_id(&value.id)
.with_attribute("model", value.model) // .with_subclass("display")
.with_attribute("resolution", value.resolution) // .with_attribute("model", value.model)
.with_attribute("price", value.price), // .with_attribute("resolution", value.resolution)
Product::Mouse(value) => Entity::new::<Product>() // .with_attribute("price", value.price),
.with_id(&value.id) // Product::Mouse(value) => Entity::new::<Product>()
.with_subclass("mouse") // .with_id(&value.id)
.with_attribute("model", value.model) // .with_subclass("mouse")
.with_attribute("wireless", value.wireless) // .with_attribute("model", value.model)
.with_attribute("price", value.price), // .with_attribute("wireless", value.wireless)
_ => unreachable!(), // .with_attribute("price", value.price),
} // _ => unreachable!(),
} // }
} // }
fn main() -> Result<(), FailedTo> { // }
let db_path = "working_with_many_types.db"; // fn main() -> Result<(), FailedTo> {
// let db_path = "working_with_many_types.db";
// Clean up previous runs if file exists //
if Path::new(db_path).exists() { // // Clean up previous runs if file exists
std::fs::remove_file(db_path).unwrap(); // if Path::new(db_path).exists() {
} // std::fs::remove_file(db_path).unwrap();
// }
// 1. Initialize and Persist Data //
println!("== 1. Storing different product types =="); // // 1. Initialize and Persist Data
let mut catalog = Catalog::new(db_path); // println!("== 1. Storing different product types ==");
catalog.init()?; // let mut catalog = Catalog::new(db_path);
// catalog.init()?;
let products_to_add = vec![ //
Product::Laptop(Laptop { // let products_to_add = vec![
id: "laptop_01".to_string(), // Product::Laptop(Laptop {
model: "Titan".to_string(), // id: "laptop_01".to_string(),
price: 1500, // model: "Titan".to_string(),
}), // price: 1500,
Product::Display(Display { // }),
id: "display_01".to_string(), // Product::Display(Display {
model: "CrystalClear".to_string(), // id: "display_01".to_string(),
resolution: 4.0, // 4K // model: "CrystalClear".to_string(),
price: 600, // resolution: 4.0, // 4K
}), // price: 600,
Product::Mouse(Mouse { // }),
id: "mouse_01".to_string(), // Product::Mouse(Mouse {
model: "SwiftClick".to_string(), // id: "mouse_01".to_string(),
wireless: true, // model: "SwiftClick".to_string(),
price: 80, // wireless: true,
}), // price: 80,
Product::Laptop(Laptop { // }),
id: "laptop_02".to_string(), // Product::Laptop(Laptop {
model: "Nomad".to_string(), // id: "laptop_02".to_string(),
price: 950, // model: "Nomad".to_string(),
}), // price: 950,
]; // }),
// ];
catalog.insert_many(products_to_add)?; //
catalog.persist()?; // catalog.insert_many(products_to_add)?;
println!("✅ 4 products saved to the database.\n"); // catalog.persist()?;
// println!("✅ 4 products saved to the database.\n");
// 2. Load data using filters //
println!("== 2. Loading products using class and subclass filters =="); // // 2. Load data using filters
// println!("== 2. Loading products using class and subclass filters ==");
// Load only laptops //
let mut laptop_catalog = Catalog::new(db_path); // // Load only laptops
let laptop_filter = Filter::new() // let mut laptop_catalog = Catalog::new(db_path);
.with_class(Product::class()) // let laptop_filter = Filter::new()
.with_subclass("laptop"); // .with_class(Product::class())
// .with_subclass("laptop");
laptop_catalog.load_by_filter(&laptop_filter)?; //
// laptop_catalog.load_by_filter(&laptop_filter)?;
let laptops: Vec<Product> = laptop_catalog.list_by_class().map(|p| p.unwrap()).collect(); //
// let laptops: Vec<Product> = laptop_catalog.list_by_class().map(|p| p.unwrap()).collect();
println!("✅ Loaded {} laptop(s) using filter.", laptops.len()); //
assert_eq!(laptops.len(), 2); // println!("✅ Loaded {} laptop(s) using filter.", laptops.len());
for p in laptops { // assert_eq!(laptops.len(), 2);
if let Product::Laptop(laptop) = p { // for p in laptops {
println!( // if let Product::Laptop(laptop) = p {
" - Laptop: {} ({}) - ${}", // println!(
laptop.id, laptop.model, laptop.price // " - Laptop: {} ({}) - ${}",
); // laptop.id, laptop.model, laptop.price
} // );
} // }
println!(); // }
// println!();
println!("== 3. Loading products using attribute filters =="); //
// Load expensive products (price > 1000) // println!("== 3. Loading products using attribute filters ==");
let mut expensive_catalog = Catalog::new(db_path); // // Load expensive products (price > 1000)
let expensive_filter = Filter::new() // let mut expensive_catalog = Catalog::new(db_path);
.with_class(Product::class()) // let expensive_filter = Filter::new()
.with_unsigned_int("price", Comparison::Greater, 1000); // .with_class(Product::class())
// .with_unsigned_int("price", Comparison::Greater, 1000);
expensive_catalog.load_by_filter(&expensive_filter)?; //
// expensive_catalog.load_by_filter(&expensive_filter)?;
let expensive_products: Vec<Product> = expensive_catalog //
.list_by_class() // let expensive_products: Vec<Product> = expensive_catalog
.map(|p| p.unwrap()) // .list_by_class()
.collect(); // .map(|p| p.unwrap())
// .collect();
println!( //
"✅ Loaded {} product(s) with price > $1000.", // println!(
expensive_products.len() // "✅ Loaded {} product(s) with price > $1000.",
); // expensive_products.len()
assert_eq!(expensive_products.len(), 1); // );
// assert_eq!(expensive_products.len(), 1);
for p in expensive_products { //
match p { // for p in expensive_products {
Product::Laptop(laptop) => { // match p {
println!( // Product::Laptop(laptop) => {
" - Found Laptop: {} ({}) - ${}", // println!(
laptop.id, laptop.model, laptop.price // " - Found Laptop: {} ({}) - ${}",
); // laptop.id, laptop.model, laptop.price
assert_eq!(laptop.id, "laptop_01"); // );
} // assert_eq!(laptop.id, "laptop_01");
_ => panic!("Expected a laptop!"), // }
} // _ => panic!("Expected a laptop!"),
} // }
println!(); // }
// println!();
println!("== 4. Loading all product types =="); //
let mut all_products_catalog = Catalog::new(db_path); // println!("== 4. Loading all product types ==");
let all_products_filter = Filter::new().with_class(Product::class()); // let mut all_products_catalog = Catalog::new(db_path);
all_products_catalog.load_by_filter(&all_products_filter)?; // let all_products_filter = Filter::new().with_class(Product::class());
// all_products_catalog.load_by_filter(&all_products_filter)?;
let all_products: Vec<Product> = all_products_catalog //
.list_by_class() // let all_products: Vec<Product> = all_products_catalog
.map(|p| p.unwrap()) // .list_by_class()
.collect(); // .map(|p| p.unwrap())
// .collect();
println!("✅ Loaded {} total products.", all_products.len()); //
assert_eq!(all_products.len(), 4); // println!("✅ Loaded {} total products.", all_products.len());
// assert_eq!(all_products.len(), 4);
for p in all_products { //
match p { // for p in all_products {
Product::Laptop(laptop) => { // match p {
println!( // Product::Laptop(laptop) => {
" - Found Laptop: {} ({}) - ${}", // println!(
laptop.id, laptop.model, laptop.price // " - Found Laptop: {} ({}) - ${}",
); // laptop.id, laptop.model, laptop.price
} // );
Product::Display(display) => { // }
println!( // Product::Display(display) => {
" - Found Display: {} ({}) - ${}", // println!(
display.id, display.model, display.price // " - Found Display: {} ({}) - ${}",
); // display.id, display.model, display.price
} // );
Product::Mouse(mouse) => { // }
println!( // Product::Mouse(mouse) => {
" - Found Mouse: {} ({}) - ${}", // println!(
mouse.id, mouse.model, mouse.price // " - Found Mouse: {} ({}) - ${}",
); // mouse.id, mouse.model, mouse.price
} // );
Product::None => panic!("Product::None should not be loaded"), // }
} // Product::None => panic!("Product::None should not be loaded"),
} // }
println!(); // }
// println!();
// Clean up the created database file //
std::fs::remove_file(db_path).unwrap(); // // Clean up the created database file
// std::fs::remove_file(db_path).unwrap();
Ok(()) //
} // Ok(())
// }

View File

@@ -1,4 +1,5 @@
use crate::*; use crate::*;
use collections::*;
use rusqlite::*; use rusqlite::*;
fn column(value: &Value) -> &'static str { fn column(value: &Value) -> &'static str {
@@ -64,12 +65,12 @@ fn write_entity(entity: &Entity, transaction: &rusqlite::Transaction) -> Result<
Ok(()) Ok(())
} }
pub fn run(path: &path::Path, catalog: &Catalog) -> result::Result<(), FailedTo> { pub fn run(path: &path::Path, items: &HashMap<String, Entity>) -> result::Result<(), FailedTo> {
let mut connection = Connection::open(path).map_err(|_| sqlite::FailedTo::OpenConnection)?; let mut connection = Connection::open(path).map_err(|_| sqlite::FailedTo::OpenConnection)?;
let transaction = connection let transaction = connection
.transaction() .transaction()
.map_err(|_| sqlite::FailedTo::BeginTransaction)?; .map_err(|_| sqlite::FailedTo::BeginTransaction)?;
for entity in catalog.items.values().filter(|item| { for entity in items.values().filter(|item| {
item.state == EntityState::New item.state == EntityState::New
|| item.state == EntityState::Updated || item.state == EntityState::Updated
|| item.state == EntityState::ToDelete || item.state == EntityState::ToDelete

View File

@@ -0,0 +1,7 @@
use crate::*;
impl Catalog {
pub fn contains_key(&self, k: &str) -> Result<bool, FailedTo> {
self.with_items(|items| Ok(items.contains_key(k)))
}
}

View File

@@ -17,12 +17,12 @@ impl Catalog {
/// ///
/// * `id` - The ID of the entity to mark for deletion. /// * `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 _ = self.on_items(|items| {
let entity = items.get_mut(id);
if let Some(entity) = entity { if let Some(entity) = entity {
entity.state = EntityState::ToDelete; entity.state = EntityState::ToDelete;
} }
Ok(())
});
} }
} }
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -18,12 +18,11 @@ impl Catalog {
where where
T: EAV, T: EAV,
{ {
let entity = self.items.get(id); self.with_items(|items| {
let entity = items.get(id);
entity entity
.map(|e| T::try_from(e.clone()).map_err(|_| FailedTo::ConvertEntity)) .map(|e| T::try_from(e.clone()).map_err(|_| FailedTo::ConvertEntity))
.transpose() .transpose()
})
} }
} }
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -0,0 +1,7 @@
use crate::*;
impl Catalog {
pub fn is_empty(&self) -> Result<bool, FailedTo> {
self.with_items(|items| Ok(items.is_empty()))
}
}

View File

@@ -0,0 +1,7 @@
use crate::*;
impl Catalog {
pub fn len(&self) -> Result<usize, FailedTo> {
self.with_items(|items| Ok(items.len()))
}
}

View File

@@ -8,16 +8,16 @@ impl Catalog {
/// # Returns /// # Returns
/// ///
/// An iterator that yields items of type `T` from the in-memory cache. /// 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) -> Result<Vec<Result<T, FailedTo>>, FailedTo>
where where
T: EAV, T: EAV,
{ {
self.items self.with_items(|items| {
Ok(items
.values() .values()
.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))
.collect())
})
} }
} }
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -12,17 +12,17 @@ impl Catalog {
pub fn list_by_class_and_subclass<T>( pub fn list_by_class_and_subclass<T>(
&self, &self,
subclass: &str, subclass: &str,
) -> impl Iterator<Item = Result<T, FailedTo>> ) -> Result<Vec<Result<T, FailedTo>>, FailedTo>
where where
T: EAV, T: EAV,
{ {
self.items self.with_items(|items| {
Ok(items
.values() .values()
.filter(move |item| item.class == T::class()) .filter(move |item| item.class == T::class())
.filter(move |item| item.subclass == Some(subclass.to_string())) .filter(move |item| item.subclass == Some(subclass.to_string()))
.map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity)) .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity))
.collect())
})
} }
} }
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -20,13 +20,12 @@ impl Catalog {
{ {
let class = T::class(); let class = T::class();
let path = path::Path::new(&self.path); let path = path::Path::new(&self.path);
self.on_items(|items| {
let entities = sqlite::load::by_class(path, class).map_err(|_| FailedTo::LoadFromDB)?; let entities = sqlite::load::by_class(path, class).map_err(|_| FailedTo::LoadFromDB)?;
for entity in entities { for entity in entities {
self.items.insert(entity.id.clone(), entity); items.insert(entity.id.clone(), entity);
} }
Ok(()) Ok(())
})
} }
} }
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -21,11 +21,14 @@ impl Catalog {
/// - **Database State:** Unchanged. /// - **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)?; self.on_items(|items| {
let entities =
sqlite::load::by_filter(path, filter).map_err(|_| FailedTo::LoadFromDB)?;
for entity in entities { for entity in entities {
self.items.insert(entity.id.clone(), entity); items.insert(entity.id.clone(), entity);
} }
Ok(()) Ok(())
})
} }
} }

View File

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

View File

@@ -1,23 +0,0 @@
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

@@ -20,24 +20,16 @@ impl Catalog {
/// their state changed to `EntityState::Loaded`. /// 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)?; self.on_items(|items| {
sqlite::persist::catalog(path, items).map_err(|_| FailedTo::PersistCatalog)?;
// cleaning catalog state after db write // cleaning catalog state after db write
self.items = self let _: Vec<_> = items
.items .extract_if(|_, item| item.state == EntityState::ToDelete)
.extract_if(|_, item| item.state != EntityState::ToDelete)
.map(|(k, item)| {
(
k,
Entity {
state: EntityState::Loaded,
..item
},
)
})
.collect(); .collect();
items
.values_mut()
.for_each(|item| item.state = EntityState::Loaded);
Ok(()) Ok(())
})
} }
} }
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -20,15 +20,14 @@ impl Catalog {
/// * `object` - The object to insert or update, which must implement the `EAV` trait. /// * `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) { self.on_items(|items| {
if items.contains_key(&entity.id) {
entity.state = EntityState::Updated; entity.state = EntityState::Updated;
} else { } else {
entity.state = EntityState::New; entity.state = EntityState::New;
} }
self.items.insert(entity.id.clone(), entity); items.insert(entity.id.clone(), entity);
Ok(()) Ok(())
})
} }
} }
// #[cfg(test)]
// mod unit_tests { use super::*; }

View File

@@ -1,14 +1,16 @@
pub mod bool_try_from_value; pub mod bool_try_from_value;
pub mod catalog_contains_key;
pub mod catalog_delete; pub mod catalog_delete;
pub mod catalog_get; pub mod catalog_get;
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_len;
pub mod catalog_list_by_class; pub mod catalog_list_by_class;
pub mod catalog_list_by_class_and_subclass; pub mod catalog_list_by_class_and_subclass;
pub mod catalog_load_by_class; pub mod catalog_load_by_class;
pub mod catalog_load_by_filter; pub mod catalog_load_by_filter;
pub mod catalog_load_by_id; pub mod catalog_load_by_id;
pub mod catalog_new;
pub mod catalog_persist; pub mod catalog_persist;
pub mod catalog_upsert; pub mod catalog_upsert;
pub mod f64_try_from_value; pub mod f64_try_from_value;

View File

@@ -32,146 +32,146 @@
//! and the necessary `From` and `TryFrom` conversions. //! and the necessary `From` and `TryFrom` conversions.
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use heave::*; //! // use heave::*;
//! use std::convert::{From, TryFrom}; //! // use std::convert::{From, TryFrom};
//! use std::result::Result; //! // use std::result::Result;
//! //!
//! // Define a simple struct representing a product. //! // // Define a simple struct representing a product.
//! #[derive(Debug, Default, PartialEq, Clone)] //! // #[derive(Debug, Default, PartialEq, Clone)]
//! struct Product { //! // struct Product {
//! pub id: String, //! // pub id: String,
//! pub name: String, //! // pub name: String,
//! pub price: u64, //! // pub price: u64,
//! pub in_stock: bool, //! // pub in_stock: bool,
//! } //! // }
//! //!
//! // Implement the EAV trait to define the "class" of this entity. //! // // Implement the EAV trait to define the "class" of this entity.
//! impl EAV for Product { //! // impl EAV for Product {
//! fn class() -> &'static str { //! // fn class() -> &'static str {
//! "product" //! // "product"
//! } //! // }
//! } //! // }
//! //!
//! // Convert our Product into a generic Entity. //! // // Convert our Product into a generic Entity.
//! impl From<Product> for Entity { //! // impl From<Product> for Entity {
//! fn from(p: Product) -> Self { //! // fn from(p: Product) -> Self {
//! Entity::new::<Product>() //! // Entity::new::<Product>()
//! .with_id(&p.id) //! // .with_id(&p.id)
//! .with_attribute("name", p.name) //! // .with_attribute("name", p.name)
//! .with_attribute("price", p.price) //! // .with_attribute("price", p.price)
//! .with_attribute("in_stock", p.in_stock) //! // .with_attribute("in_stock", p.in_stock)
//! } //! // }
//! } //! // }
//! //!
//! // Convert a generic Entity back into our Product. //! // // Convert a generic Entity back into our Product.
//! impl TryFrom<Entity> for Product { //! // impl TryFrom<Entity> for Product {
//! type Error = FailedTo; //! // type Error = FailedTo;
//! //!
//! fn try_from(entity: Entity) -> Result<Self, Self::Error> { //! // fn try_from(entity: Entity) -> Result<Self, Self::Error> {
//! Ok(Self { //! // Ok(Self {
//! id: entity.id.clone(), //! // id: entity.id.clone(),
//! name: entity.unwrap("name").map_err(|_| FailedTo::ConvertEntity)?, //! // name: entity.unwrap("name").map_err(|_| FailedTo::ConvertEntity)?,
//! price: entity.unwrap("price").map_err(|_| FailedTo::ConvertEntity)?, //! // price: entity.unwrap("price").map_err(|_| FailedTo::ConvertEntity)?,
//! in_stock: entity.unwrap("in_stock").map_err(|_| FailedTo::ConvertEntity)?, //! // in_stock: entity.unwrap("in_stock").map_err(|_| FailedTo::ConvertEntity)?,
//! }) //! // })
//! } //! // }
//! } //! // }
//! //!
//! fn main() -> Result<(), FailedTo> { //! // fn main() -> Result<(), FailedTo> {
//! let db_path = "my_products.db"; //! // let db_path = "my_products.db";
//! //!
//! // Clean up previous runs if file exists //! // // Clean up previous runs if file exists
//! if std::path::Path::new(db_path).exists() { //! // if std::path::Path::new(db_path).exists() {
//! std::fs::remove_file(db_path).unwrap(); //! // std::fs::remove_file(db_path).unwrap();
//! } //! // }
//! //!
//! // == 1. Initialize and Persist Data == //! // // == 1. Initialize and Persist Data ==
//! let mut catalog = Catalog::new(db_path); //! // let mut catalog = Catalog::new(db_path);
//! catalog.init()?; //! // catalog.init()?;
//! //!
//! let products_to_add = vec![ //! // let products_to_add = vec![
//! Product { //! // Product {
//! id: "p1".to_string(), //! // id: "p1".to_string(),
//! name: "Laptop".to_string(), //! // name: "Laptop".to_string(),
//! price: 1200, //! // price: 1200,
//! in_stock: true, //! // in_stock: true,
//! }, //! // },
//! Product { //! // Product {
//! id: "p2".to_string(), //! // id: "p2".to_string(),
//! name: "Mouse".to_string(), //! // name: "Mouse".to_string(),
//! price: 25, //! // price: 25,
//! in_stock: true, //! // in_stock: true,
//! }, //! // },
//! Product { //! // Product {
//! id: "p3".to_string(), //! // id: "p3".to_string(),
//! name: "Keyboard".to_string(), //! // name: "Keyboard".to_string(),
//! price: 75, //! // price: 75,
//! in_stock: false, //! // in_stock: false,
//! }, //! // },
//! ]; //! // ];
//! //!
//! catalog.insert_many(products_to_add)?; //! // catalog.insert_many(products_to_add)?;
//! catalog.persist()?; //! // catalog.persist()?;
//! println!("✅ Products saved to the database."); //! // println!("✅ Products saved to the database.");
//! //!
//! // == 2. Load and Query Data == //! // // == 2. Load and Query Data ==
//! let mut query_catalog = Catalog::new(db_path); //! // let mut query_catalog = Catalog::new(db_path);
//! //!
//! // Load a single product by its ID. //! // // Load a single product by its ID.
//! query_catalog.load_by_id("p1")?; //! // query_catalog.load_by_id("p1")?;
//! let laptop: Product = query_catalog.get("p1")?.unwrap(); //! // let laptop: Product = query_catalog.get("p1")?.unwrap();
//! println!("✅ Loaded by ID: {:?}", laptop); //! // println!("✅ Loaded by ID: {:?}", laptop);
//! assert_eq!(laptop.name, "Laptop"); //! // assert_eq!(laptop.name, "Laptop");
//! //!
//! // Load products matching a filter (in stock, price < 100) //! // // Load products matching a filter (in stock, price < 100)
//! let filter = Filter::new() //! // let filter = Filter::new()
//! .with_bool("in_stock", true) //! // .with_bool("in_stock", true)
//! .with_unsigned_int("price", Comparison::Lesser, 100); //! // .with_unsigned_int("price", Comparison::Lesser, 100);
//! //!
//! let mut filtered_catalog = Catalog::new(db_path); //! // let mut filtered_catalog = Catalog::new(db_path);
//! filtered_catalog.load_by_filter(&filter)?; //! // filtered_catalog.load_by_filter(&filter)?;
//! let cheap_products: Vec<Product> = filtered_catalog.list_by_class().map(|p| p.unwrap()).collect(); //! // let cheap_products: Vec<Product> = filtered_catalog.list_by_class().map(|p| p.unwrap()).collect();
//! println!("✅ Found {} cheap, in-stock product(s).", cheap_products.len()); //! // println!("✅ Found {} cheap, in-stock product(s).", cheap_products.len());
//! assert_eq!(cheap_products.len(), 1); //! // assert_eq!(cheap_products.len(), 1);
//! assert_eq!(cheap_products[0].id, "p2"); //! // assert_eq!(cheap_products[0].id, "p2");
//! //!
//! // == 3. Update and Delete Data == //! // // == 3. Update and Delete Data ==
//! let mut update_catalog = Catalog::new(db_path); //! // let mut update_catalog = Catalog::new(db_path);
//! update_catalog.load_by_class::<Product>()?; //! // update_catalog.load_by_class::<Product>()?;
//! //!
//! // Update the price of the laptop //! // // Update the price of the laptop
//! let mut laptop_to_update: Product = update_catalog.get("p1")?.unwrap(); //! // let mut laptop_to_update: Product = update_catalog.get("p1")?.unwrap();
//! laptop_to_update.price = 1150; //! // laptop_to_update.price = 1150;
//! update_catalog.upsert(laptop_to_update)?; //! // update_catalog.upsert(laptop_to_update)?;
//! //!
//! // Delete the keyboard //! // // Delete the keyboard
//! update_catalog.delete("p3"); //! // update_catalog.delete("p3");
//! //!
//! // Persist all changes //! // // Persist all changes
//! update_catalog.persist()?; //! // update_catalog.persist()?;
//! println!("✅ Laptop price updated and keyboard deleted."); //! // println!("✅ Laptop price updated and keyboard deleted.");
//! //!
//! // == 4. Verify Final State == //! // // == 4. Verify Final State ==
//! let mut final_catalog = Catalog::new(db_path); //! // let mut final_catalog = Catalog::new(db_path);
//! final_catalog.load_by_class::<Product>()?; //! // final_catalog.load_by_class::<Product>()?;
//! //!
//! let final_count = final_catalog.list_by_class::<Product>().count(); //! // let final_count = final_catalog.list_by_class::<Product>().count();
//! println!("✅ Final product count: {}", final_count); //! // println!("✅ Final product count: {}", final_count);
//! assert_eq!(final_count, 2); //! // assert_eq!(final_count, 2);
//! //!
//! let updated_laptop: Product = final_catalog.get("p1")?.unwrap(); //! // let updated_laptop: Product = final_catalog.get("p1")?.unwrap();
//! println!("✅ Verified updated laptop price: {}", updated_laptop.price); //! // println!("✅ Verified updated laptop price: {}", updated_laptop.price);
//! assert_eq!(updated_laptop.price, 1150); //! // assert_eq!(updated_laptop.price, 1150);
//! //!
//! let deleted_keyboard: Option<Product> = final_catalog.get("p3")?; //! // let deleted_keyboard: Option<Product> = final_catalog.get("p3")?;
//! println!("✅ Verified keyboard is deleted."); //! // println!("✅ Verified keyboard is deleted.");
//! assert!(deleted_keyboard.is_none()); //! // assert!(deleted_keyboard.is_none());
//! //!
//! // Clean up the created database file //! // // Clean up the created database file
//! std::fs::remove_file(db_path).unwrap(); //! // std::fs::remove_file(db_path).unwrap();
//! //!
//! Ok(()) //! // Ok(())
//! } //! // }
//! ``` //! ```
use std::*; use std::*;

View File

@@ -1,12 +1,49 @@
use crate::*; use crate::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::*;
/// Represents a catalog of entities that can be persisted to a SQLite database. /// Represents a catalog of entities that can be persisted to a SQLite database.
/// ///
/// The `Catalog` holds entities in memory and provides methods to interact with /// The `Catalog` holds entities in memory and provides methods to interact with
/// 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)]
pub struct O { pub struct O {
pub(crate) path: String, pub(crate) path: String,
pub(crate) items: HashMap<String, Entity>, items: Mutex<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),
..Catalog::default()
}
}
pub(crate) fn on_items<F>(&self, exec: F) -> Result<(), FailedTo>
where
F: FnOnce(&mut HashMap<String, Entity>) -> Result<(), FailedTo>,
{
let mut guarded_items = self.items.lock().map_err(|_| FailedTo::LockCatalog)?;
exec(&mut guarded_items)
}
pub(crate) fn with_items<F, R>(&self, exec: F) -> Result<R, FailedTo>
where
F: FnOnce(&HashMap<String, Entity>) -> Result<R, FailedTo>,
{
let mut guarded_items = self.items.lock().map_err(|_| FailedTo::LockCatalog)?;
exec(&mut guarded_items)
}
} }

View File

@@ -15,6 +15,8 @@ pub enum FailedTo {
InitDatabase, InitDatabase,
/// Failed to load data from the database. /// Failed to load data from the database.
LoadFromDB, LoadFromDB,
/// Failed to lock catalog in a multithread environment.
LockCatalog,
/// Failed to map a database row to an attribute. /// Failed to map a database row to an attribute.
MapAttribute, MapAttribute,
/// Failed to map a database row to an entity. /// Failed to map a database row to an entity.

View File

@@ -16,8 +16,13 @@ mod tests {
let item_id = item.id.clone(); let item_id = item.id.clone();
let _ = catalog.upsert(item); let _ = catalog.upsert(item);
catalog.delete(&item_id); catalog.delete(&item_id);
let entity = catalog.items.get(&item_id).unwrap(); let is_deleted = catalog
assert_eq!(entity.state, EntityState::ToDelete); .with_items(|items| {
let entity = items.get(&item_id).unwrap();
Ok(entity.state == EntityState::ToDelete)
})
.unwrap();
assert!(is_deleted);
} }
#[test] #[test]
fn delete_should_have_no_effect_for_nonexistent_id() { fn delete_should_have_no_effect_for_nonexistent_id() {
@@ -32,9 +37,14 @@ mod tests {
..Item::default() ..Item::default()
}; };
let _ = catalog.upsert(item); let _ = catalog.upsert(item);
let original_items = catalog.items.clone();
// Attempt to delete a non-existent entity, which should not panic or change anything. // Attempt to delete a non-existent entity, which should not panic or change anything.
catalog.delete("nonexistent-id"); catalog.delete("nonexistent-id");
assert_eq!(catalog.items, original_items); let not_deleted = catalog
.with_items(|items| {
let entity = items.get("item-123").unwrap();
Ok(entity.state != EntityState::ToDelete)
})
.unwrap();
assert!(not_deleted);
} }
} }

View File

@@ -24,13 +24,24 @@ mod tests {
}, },
]; ];
let _ = catalog.insert_many(items); let _ = catalog.insert_many(items);
assert_eq!(catalog.items.len(), 2); let len = catalog.len().unwrap();
let entity1 = catalog.items.get("item-1").unwrap(); assert_eq!(len, 2);
let entity1 = catalog
.with_items(|items| {
let entity = items.get("item-1").unwrap();
Ok(entity.clone())
})
.unwrap();
assert_eq!(entity1.state, EntityState::New); assert_eq!(entity1.state, EntityState::New);
assert_eq!(entity1.value_of("name"), Some(&Value::from("Item 1"))); assert_eq!(entity1.value_of("name"), Some(&Value::from("Item 1")));
assert_eq!(entity1.value_of("price"), Some(&Value::from(10u64))); assert_eq!(entity1.value_of("price"), Some(&Value::from(10u64)));
assert_eq!(entity1.value_of("sell_trend"), Some(&Value::from(0i64))); assert_eq!(entity1.value_of("sell_trend"), Some(&Value::from(0i64)));
let entity2 = catalog.items.get("item-2").unwrap(); let entity2 = catalog
.with_items(|items| {
let entity = items.get("item-2").unwrap();
Ok(entity.clone())
})
.unwrap();
assert_eq!(entity2.state, EntityState::New); assert_eq!(entity2.state, EntityState::New);
assert_eq!(entity2.value_of("name"), Some(&Value::from("Item 2"))); assert_eq!(entity2.value_of("name"), Some(&Value::from("Item 2")));
assert_eq!(entity2.value_of("price"), Some(&Value::from(20u64))); assert_eq!(entity2.value_of("price"), Some(&Value::from(20u64)));

View File

@@ -72,6 +72,8 @@ mod tests {
catalog2.load_by_class::<Item>().unwrap(); catalog2.load_by_class::<Item>().unwrap();
let mut loaded_items: Vec<Item> = catalog2 let mut loaded_items: Vec<Item> = catalog2
.list_by_class::<Item>() .list_by_class::<Item>()
.unwrap()
.into_iter()
.map(|item| item.unwrap()) .map(|item| item.unwrap())
.collect(); .collect();
// Sort by ID to ensure consistent order for comparison // Sort by ID to ensure consistent order for comparison

View File

@@ -25,22 +25,16 @@ mod tests {
}; };
let _ = catalog.upsert(item1.clone()); let _ = catalog.upsert(item1.clone());
let _ = catalog.upsert(item2.clone()); let _ = catalog.upsert(item2.clone());
let results: Vec<Item> = catalog let results = catalog.list_by_class::<Item>().unwrap();
.list_by_class::<Item>()
.map(|item| item.unwrap())
.collect();
assert_eq!(results.len(), 2); assert_eq!(results.len(), 2);
assert!(results.contains(&item1)); assert!(results.contains(&Ok(item1)));
assert!(results.contains(&item2)); assert!(results.contains(&Ok(item2)));
} }
#[test] #[test]
fn list_by_class_should_return_empty_iterator_if_no_match() { fn list_by_class_should_return_empty_iterator_if_no_match() {
// Should return an empty iterator if no entities of that class exist. // Should return an empty iterator if no entities of that class exist.
let catalog = Catalog::new("dummy.db"); let catalog = Catalog::new("dummy.db");
let results: Vec<Item> = catalog let results: Vec<_> = catalog.list_by_class::<Item>().unwrap();
.list_by_class::<Item>()
.map(|item| item.unwrap())
.collect();
assert!(results.is_empty()); assert!(results.is_empty());
} }
} }

View File

@@ -22,8 +22,10 @@ mod tests {
let _ = catalog.upsert(item1.clone()); let _ = catalog.upsert(item1.clone());
let _ = catalog.upsert(item2.clone()); let _ = catalog.upsert(item2.clone());
let _ = catalog.upsert(item3.clone()); let _ = catalog.upsert(item3.clone());
let results: Vec<Item> = catalog let results: Vec<_> = catalog
.list_by_class_and_subclass("electronics") .list_by_class_and_subclass::<Item>("electronics")
.unwrap()
.into_iter()
.map(|item| item.unwrap()) .map(|item| item.unwrap())
.collect(); .collect();
assert_eq!(results.len(), 2); assert_eq!(results.len(), 2);
@@ -40,8 +42,10 @@ mod tests {
..Default::default() ..Default::default()
}; };
let _ = catalog.upsert(item1.clone()); let _ = catalog.upsert(item1.clone());
let results: Vec<Item> = catalog let results: Vec<_> = catalog
.list_by_class_and_subclass("books") .list_by_class_and_subclass::<Item>("books")
.unwrap()
.into_iter()
.map(|item| item.unwrap()) .map(|item| item.unwrap())
.collect(); .collect();
assert!(results.is_empty()); assert!(results.is_empty());

View File

@@ -39,17 +39,22 @@ mod tests {
let result = catalog2.load_by_class::<Item>(); let result = catalog2.load_by_class::<Item>();
assert!(result.is_ok()); assert!(result.is_ok());
// 3. Verify that all items of that class were loaded // 3. Verify that all items of that class were loaded
assert_eq!(catalog2.items.len(), 2); let len = catalog2.len().unwrap();
assert_eq!(len, 2);
let loaded_item1: Item = catalog2.get("item-1").unwrap().unwrap(); let loaded_item1: Item = catalog2.get("item-1").unwrap().unwrap();
let loaded_item2: Item = catalog2.get("item-2").unwrap().unwrap(); let loaded_item2: Item = catalog2.get("item-2").unwrap().unwrap();
assert_eq!(loaded_item1, item1); assert_eq!(loaded_item1, item1);
assert_eq!(loaded_item2, item2); assert_eq!(loaded_item2, item2);
assert_eq!( assert_eq!(
catalog2.items.get("item-1").unwrap().state, catalog2
.with_items(|items| { Ok(items.get("item-1").unwrap().state) })
.unwrap(),
EntityState::Loaded EntityState::Loaded
); );
assert_eq!( assert_eq!(
catalog2.items.get("item-2").unwrap().state, catalog2
.with_items(|items| { Ok(items.get("item-2").unwrap().state) })
.unwrap(),
EntityState::Loaded EntityState::Loaded
); );
// Clean up // Clean up
@@ -90,7 +95,8 @@ mod tests {
..Item::default() ..Item::default()
}; };
let _ = catalog2.upsert(item_in_memory); let _ = catalog2.upsert(item_in_memory);
assert_eq!(catalog2.items.len(), 1); let len = catalog2.len().unwrap();
assert_eq!(len, 1);
assert_eq!( assert_eq!(
catalog2.get::<Item>("item-1").unwrap().unwrap().name, catalog2.get::<Item>("item-1").unwrap().unwrap().name,
"Memory Version" "Memory Version"
@@ -99,11 +105,14 @@ mod tests {
let result = catalog2.load_by_class::<Item>(); let result = catalog2.load_by_class::<Item>();
assert!(result.is_ok()); assert!(result.is_ok());
// 4. Verify that the in-memory entity has been replaced with the one from the DB. // 4. Verify that the in-memory entity has been replaced with the one from the DB.
assert_eq!(catalog2.items.len(), 1); let len = catalog2.len().unwrap();
assert_eq!(len, 1);
let loaded_item: Item = catalog2.get("item-1").unwrap().unwrap(); let loaded_item: Item = catalog2.get("item-1").unwrap().unwrap();
assert_eq!(loaded_item, item_in_db); assert_eq!(loaded_item, item_in_db);
assert_eq!( assert_eq!(
catalog2.items.get("item-1").unwrap().state, catalog2
.with_items(|items| { Ok(items.get("item-1").unwrap().state) })
.unwrap(),
EntityState::Loaded EntityState::Loaded
); );
// Clean up // Clean up
@@ -124,7 +133,8 @@ mod tests {
// 2. Attempt to load from the empty DB. // 2. Attempt to load from the empty DB.
let result = catalog.load_by_class::<Item>(); let result = catalog.load_by_class::<Item>();
assert!(result.is_ok()); assert!(result.is_ok());
assert!(catalog.items.is_empty()); let is_empty = catalog.is_empty().unwrap();
assert!(is_empty);
// 3. Add an item to memory and try loading again from the empty DB. // 3. Add an item to memory and try loading again from the empty DB.
let item_in_memory = Item { let item_in_memory = Item {
id: "item-1".to_string(), id: "item-1".to_string(),
@@ -136,11 +146,13 @@ mod tests {
..Item::default() ..Item::default()
}; };
let _ = catalog.upsert(item_in_memory.clone()); let _ = catalog.upsert(item_in_memory.clone());
assert_eq!(catalog.items.len(), 1); let len = catalog.len().unwrap();
assert_eq!(len, 1);
let result2 = catalog.load_by_class::<Item>(); let result2 = catalog.load_by_class::<Item>();
assert!(result2.is_ok()); assert!(result2.is_ok());
// 4. Verify the in-memory item is untouched because nothing was loaded from DB. // 4. Verify the in-memory item is untouched because nothing was loaded from DB.
assert_eq!(catalog.items.len(), 1); let len = catalog.len().unwrap();
assert_eq!(len, 1);
let retrieved_item: Item = catalog.get("item-1").unwrap().unwrap(); let retrieved_item: Item = catalog.get("item-1").unwrap().unwrap();
assert_eq!(retrieved_item, item_in_memory); assert_eq!(retrieved_item, item_in_memory);
// Clean up // Clean up

View File

@@ -43,9 +43,9 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_bool("in_stock", true); let filter = Filter::new().with_bool("in_stock", true);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -82,9 +82,9 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_bool("in_stock", false); let filter = Filter::new().with_bool("in_stock", false);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
assert!(!catalog.items.contains_key("item-1")); assert!(!catalog.contains_key("item-1").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -121,7 +121,7 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new(); let filter = Filter::new();
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -216,10 +216,10 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_signed_int("sell_trend", Comparison::Equal, 10); let filter = Filter::new().with_signed_int("sell_trend", Comparison::Equal, 10);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
assert!(!catalog.items.contains_key("item-2")); assert!(!catalog.contains_key("item-2").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -263,10 +263,10 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_signed_int("sell_trend", Comparison::Equal, -5); let filter = Filter::new().with_signed_int("sell_trend", Comparison::Equal, -5);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
assert!(!catalog.items.contains_key("item-1")); assert!(!catalog.contains_key("item-1").unwrap());
assert!(!catalog.items.contains_key("item-3")); assert!(!catalog.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -310,10 +310,10 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_signed_int("sell_trend", Comparison::Greater, 5); let filter = Filter::new().with_signed_int("sell_trend", Comparison::Greater, 5);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(!catalog.items.contains_key("item-2")); assert!(!catalog.contains_key("item-2").unwrap());
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -358,21 +358,21 @@ mod tests {
let mut catalog1 = Catalog::new(db_path); let mut catalog1 = Catalog::new(db_path);
let filter1 = Filter::new().with_signed_int("sell_trend", Comparison::Greater, 10); let filter1 = Filter::new().with_signed_int("sell_trend", Comparison::Greater, 10);
assert!(catalog1.load_by_filter(&filter1).is_ok()); assert!(catalog1.load_by_filter(&filter1).is_ok());
assert_eq!(catalog1.items.len(), 1); assert_eq!(catalog1.len().unwrap(), 1);
assert!(catalog1.items.contains_key("item-3")); assert!(catalog1.contains_key("item-3").unwrap());
// Edge Case 2: No values greater than filter value. // Edge Case 2: No values greater than filter value.
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let filter2 = Filter::new().with_signed_int("sell_trend", Comparison::Greater, 20); let filter2 = Filter::new().with_signed_int("sell_trend", Comparison::Greater, 20);
assert!(catalog2.load_by_filter(&filter2).is_ok()); assert!(catalog2.load_by_filter(&filter2).is_ok());
assert!(catalog2.items.is_empty()); assert!(catalog2.is_empty().unwrap());
// Edge Case 3: All values greater than filter value. // Edge Case 3: All values greater than filter value.
let mut catalog3 = Catalog::new(db_path); let mut catalog3 = Catalog::new(db_path);
let filter3 = Filter::new().with_signed_int("sell_trend", Comparison::Greater, -10); let filter3 = Filter::new().with_signed_int("sell_trend", Comparison::Greater, -10);
assert!(catalog3.load_by_filter(&filter3).is_ok()); assert!(catalog3.load_by_filter(&filter3).is_ok());
assert_eq!(catalog3.items.len(), 3); assert_eq!(catalog3.len().unwrap(), 3);
assert!(catalog3.items.contains_key("item-1")); assert!(catalog3.contains_key("item-1").unwrap());
assert!(catalog3.items.contains_key("item-2")); assert!(catalog3.contains_key("item-2").unwrap());
assert!(catalog3.items.contains_key("item-3")); assert!(catalog3.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -416,10 +416,10 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_signed_int("sell_trend", Comparison::Lesser, 15); let filter = Filter::new().with_signed_int("sell_trend", Comparison::Lesser, 15);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
assert!(!catalog.items.contains_key("item-3")); assert!(!catalog.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -464,21 +464,21 @@ mod tests {
let mut catalog1 = Catalog::new(db_path); let mut catalog1 = Catalog::new(db_path);
let filter1 = Filter::new().with_signed_int("sell_trend", Comparison::Lesser, 10); let filter1 = Filter::new().with_signed_int("sell_trend", Comparison::Lesser, 10);
assert!(catalog1.load_by_filter(&filter1).is_ok()); assert!(catalog1.load_by_filter(&filter1).is_ok());
assert_eq!(catalog1.items.len(), 1); assert_eq!(catalog1.len().unwrap(), 1);
assert!(catalog1.items.contains_key("item-2")); assert!(catalog1.contains_key("item-2").unwrap());
// Edge Case 2: No values lesser than filter value. // Edge Case 2: No values lesser than filter value.
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let filter2 = Filter::new().with_signed_int("sell_trend", Comparison::Lesser, -5); let filter2 = Filter::new().with_signed_int("sell_trend", Comparison::Lesser, -5);
assert!(catalog2.load_by_filter(&filter2).is_ok()); assert!(catalog2.load_by_filter(&filter2).is_ok());
assert!(catalog2.items.is_empty()); assert!(catalog2.is_empty().unwrap());
// Edge Case 3: All values lesser than filter value. // Edge Case 3: All values lesser than filter value.
let mut catalog3 = Catalog::new(db_path); let mut catalog3 = Catalog::new(db_path);
let filter3 = Filter::new().with_signed_int("sell_trend", Comparison::Lesser, 25); let filter3 = Filter::new().with_signed_int("sell_trend", Comparison::Lesser, 25);
assert!(catalog3.load_by_filter(&filter3).is_ok()); assert!(catalog3.load_by_filter(&filter3).is_ok());
assert_eq!(catalog3.items.len(), 3); assert_eq!(catalog3.len().unwrap(), 3);
assert!(catalog3.items.contains_key("item-1")); assert!(catalog3.contains_key("item-1").unwrap());
assert!(catalog3.items.contains_key("item-2")); assert!(catalog3.contains_key("item-2").unwrap());
assert!(catalog3.items.contains_key("item-3")); assert!(catalog3.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -522,10 +522,10 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_signed_int("sell_trend", Comparison::GreaterOrEqual, 10); let filter = Filter::new().with_signed_int("sell_trend", Comparison::GreaterOrEqual, 10);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(!catalog.items.contains_key("item-2")); assert!(!catalog.contains_key("item-2").unwrap());
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -570,21 +570,21 @@ mod tests {
let mut catalog1 = Catalog::new(db_path); let mut catalog1 = Catalog::new(db_path);
let filter1 = Filter::new().with_signed_int("sell_trend", Comparison::GreaterOrEqual, 20); let filter1 = Filter::new().with_signed_int("sell_trend", Comparison::GreaterOrEqual, 20);
assert!(catalog1.load_by_filter(&filter1).is_ok()); assert!(catalog1.load_by_filter(&filter1).is_ok());
assert_eq!(catalog1.items.len(), 1); assert_eq!(catalog1.len().unwrap(), 1);
assert!(catalog1.items.contains_key("item-3")); assert!(catalog1.contains_key("item-3").unwrap());
// Edge Case 2: No values greater than or equal to filter value. // Edge Case 2: No values greater than or equal to filter value.
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let filter2 = Filter::new().with_signed_int("sell_trend", Comparison::GreaterOrEqual, 21); let filter2 = Filter::new().with_signed_int("sell_trend", Comparison::GreaterOrEqual, 21);
assert!(catalog2.load_by_filter(&filter2).is_ok()); assert!(catalog2.load_by_filter(&filter2).is_ok());
assert!(catalog2.items.is_empty()); assert!(catalog2.is_empty().unwrap());
// Edge Case 3: All values greater than or equal to filter value. // Edge Case 3: All values greater than or equal to filter value.
let mut catalog3 = Catalog::new(db_path); let mut catalog3 = Catalog::new(db_path);
let filter3 = Filter::new().with_signed_int("sell_trend", Comparison::GreaterOrEqual, -5); let filter3 = Filter::new().with_signed_int("sell_trend", Comparison::GreaterOrEqual, -5);
assert!(catalog3.load_by_filter(&filter3).is_ok()); assert!(catalog3.load_by_filter(&filter3).is_ok());
assert_eq!(catalog3.items.len(), 3); assert_eq!(catalog3.len().unwrap(), 3);
assert!(catalog3.items.contains_key("item-1")); assert!(catalog3.contains_key("item-1").unwrap());
assert!(catalog3.items.contains_key("item-2")); assert!(catalog3.contains_key("item-2").unwrap());
assert!(catalog3.items.contains_key("item-3")); assert!(catalog3.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -628,10 +628,10 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_signed_int("sell_trend", Comparison::LesserOrEqual, 10); let filter = Filter::new().with_signed_int("sell_trend", Comparison::LesserOrEqual, 10);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
assert!(!catalog.items.contains_key("item-3")); assert!(!catalog.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -676,21 +676,21 @@ mod tests {
let mut catalog1 = Catalog::new(db_path); let mut catalog1 = Catalog::new(db_path);
let filter1 = Filter::new().with_signed_int("sell_trend", Comparison::LesserOrEqual, -5); let filter1 = Filter::new().with_signed_int("sell_trend", Comparison::LesserOrEqual, -5);
assert!(catalog1.load_by_filter(&filter1).is_ok()); assert!(catalog1.load_by_filter(&filter1).is_ok());
assert_eq!(catalog1.items.len(), 1); assert_eq!(catalog1.len().unwrap(), 1);
assert!(catalog1.items.contains_key("item-2")); assert!(catalog1.contains_key("item-2").unwrap());
// Edge Case 2: No values lesser than or equal to filter value. // Edge Case 2: No values lesser than or equal to filter value.
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let filter2 = Filter::new().with_signed_int("sell_trend", Comparison::LesserOrEqual, -6); let filter2 = Filter::new().with_signed_int("sell_trend", Comparison::LesserOrEqual, -6);
assert!(catalog2.load_by_filter(&filter2).is_ok()); assert!(catalog2.load_by_filter(&filter2).is_ok());
assert!(catalog2.items.is_empty()); assert!(catalog2.is_empty().unwrap());
// Edge Case 3: All values lesser than or equal to filter value. // Edge Case 3: All values lesser than or equal to filter value.
let mut catalog3 = Catalog::new(db_path); let mut catalog3 = Catalog::new(db_path);
let filter3 = Filter::new().with_signed_int("sell_trend", Comparison::LesserOrEqual, 20); let filter3 = Filter::new().with_signed_int("sell_trend", Comparison::LesserOrEqual, 20);
assert!(catalog3.load_by_filter(&filter3).is_ok()); assert!(catalog3.load_by_filter(&filter3).is_ok());
assert_eq!(catalog3.items.len(), 3); assert_eq!(catalog3.len().unwrap(), 3);
assert!(catalog3.items.contains_key("item-1")); assert!(catalog3.contains_key("item-1").unwrap());
assert!(catalog3.items.contains_key("item-2")); assert!(catalog3.contains_key("item-2").unwrap());
assert!(catalog3.items.contains_key("item-3")); assert!(catalog3.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -735,44 +735,44 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_unsigned_int("price", Comparison::Equal, 200); let filter = Filter::new().with_unsigned_int("price", Comparison::Equal, 200);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
// Greater // Greater
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_unsigned_int("price", Comparison::Greater, 200); let filter = Filter::new().with_unsigned_int("price", Comparison::Greater, 200);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
// Lesser // Lesser
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_unsigned_int("price", Comparison::Lesser, 200); let filter = Filter::new().with_unsigned_int("price", Comparison::Lesser, 200);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
// GreaterOrEqual // GreaterOrEqual
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_unsigned_int("price", Comparison::GreaterOrEqual, 200); let filter = Filter::new().with_unsigned_int("price", Comparison::GreaterOrEqual, 200);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
// LesserOrEqual // LesserOrEqual
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_unsigned_int("price", Comparison::LesserOrEqual, 200); let filter = Filter::new().with_unsigned_int("price", Comparison::LesserOrEqual, 200);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
// Edge case: No match // Edge case: No match
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_unsigned_int("price", Comparison::Equal, 400); let filter = Filter::new().with_unsigned_int("price", Comparison::Equal, 400);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert!(catalog.items.is_empty()); assert!(catalog.is_empty().unwrap());
// Edge case: All match // Edge case: All match
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_unsigned_int("price", Comparison::GreaterOrEqual, 100); let filter = Filter::new().with_unsigned_int("price", Comparison::GreaterOrEqual, 100);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 3); assert_eq!(catalog.len().unwrap(), 3);
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -817,24 +817,24 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::IsExactly, "Item One"); let filter = Filter::new().with_text("name", Comparison::IsExactly, "Item One");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
// Partial match should not work // Partial match should not work
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::IsExactly, "Item"); let filter = Filter::new().with_text("name", Comparison::IsExactly, "Item");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert!(catalog.items.is_empty()); assert!(catalog.is_empty().unwrap());
// Case insensitive match // Case insensitive match
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::IsExactly, "item one"); let filter = Filter::new().with_text("name", Comparison::IsExactly, "item one");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
// No match // No match
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::IsExactly, "Item Four"); let filter = Filter::new().with_text("name", Comparison::IsExactly, "Item Four");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert!(catalog.items.is_empty()); assert!(catalog.is_empty().unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -879,34 +879,34 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::StartsWith, "Item"); let filter = Filter::new().with_text("name", Comparison::StartsWith, "Item");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
// Starts with "I" // Starts with "I"
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::StartsWith, "I"); let filter = Filter::new().with_text("name", Comparison::StartsWith, "I");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
// No match // No match
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::StartsWith, "Z"); let filter = Filter::new().with_text("name", Comparison::StartsWith, "Z");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert!(catalog.items.is_empty()); assert!(catalog.is_empty().unwrap());
// Case insensitive // Case insensitive
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::StartsWith, "item"); let filter = Filter::new().with_text("name", Comparison::StartsWith, "item");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
// Full string match // Full string match
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::StartsWith, "Item One"); let filter = Filter::new().with_text("name", Comparison::StartsWith, "Item One");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -951,25 +951,25 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::EndsWith, "one"); let filter = Filter::new().with_text("name", Comparison::EndsWith, "one");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
// Ends with "item" - case insensitive // Ends with "item" - case insensitive
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::EndsWith, "item"); let filter = Filter::new().with_text("name", Comparison::EndsWith, "item");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
// No match // No match
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::EndsWith, "Z"); let filter = Filter::new().with_text("name", Comparison::EndsWith, "Z");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert!(catalog.items.is_empty()); assert!(catalog.is_empty().unwrap());
// Full string match - case insensitive // Full string match - case insensitive
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::EndsWith, "item one"); let filter = Filter::new().with_text("name", Comparison::EndsWith, "item one");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1014,24 +1014,24 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::Contains, "item"); let filter = Filter::new().with_text("name", Comparison::Contains, "item");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 3); assert_eq!(catalog.len().unwrap(), 3);
// Contains "THE" - case insensitive // Contains "THE" - case insensitive
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::Contains, "THE"); let filter = Filter::new().with_text("name", Comparison::Contains, "THE");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
// No match // No match
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::Contains, "Z"); let filter = Filter::new().with_text("name", Comparison::Contains, "Z");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert!(catalog.items.is_empty()); assert!(catalog.is_empty().unwrap());
// Full string match - case insensitive // Full string match - case insensitive
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_text("name", Comparison::Contains, "item one"); let filter = Filter::new().with_text("name", Comparison::Contains, "item one");
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1066,10 +1066,10 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_real("discount", Comparison::Equal, 0.10); let filter = Filter::new().with_real("discount", Comparison::Equal, 0.10);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
assert!(!catalog.items.contains_key("item-2")); assert!(!catalog.contains_key("item-2").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1104,9 +1104,9 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_real("discount", Comparison::Greater, 0.15); let filter = Filter::new().with_real("discount", Comparison::Greater, 0.15);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1136,12 +1136,12 @@ mod tests {
let mut catalog1 = Catalog::new(db_path); let mut catalog1 = Catalog::new(db_path);
let filter1 = Filter::new().with_real("discount", Comparison::Greater, 0.10); let filter1 = Filter::new().with_real("discount", Comparison::Greater, 0.10);
assert!(catalog1.load_by_filter(&filter1).is_ok()); assert!(catalog1.load_by_filter(&filter1).is_ok());
assert_eq!(catalog1.items.len(), 1); assert_eq!(catalog1.len().unwrap(), 1);
assert!(catalog1.items.contains_key("item-2")); assert!(catalog1.contains_key("item-2").unwrap());
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let filter2 = Filter::new().with_real("discount", Comparison::Greater, 0.20); let filter2 = Filter::new().with_real("discount", Comparison::Greater, 0.20);
assert!(catalog2.load_by_filter(&filter2).is_ok()); assert!(catalog2.load_by_filter(&filter2).is_ok());
assert!(catalog2.items.is_empty()); assert!(catalog2.is_empty().unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1176,9 +1176,9 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_real("discount", Comparison::Lesser, 0.15); let filter = Filter::new().with_real("discount", Comparison::Lesser, 0.15);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1208,12 +1208,12 @@ mod tests {
let mut catalog1 = Catalog::new(db_path); let mut catalog1 = Catalog::new(db_path);
let filter1 = Filter::new().with_real("discount", Comparison::Lesser, 0.20); let filter1 = Filter::new().with_real("discount", Comparison::Lesser, 0.20);
assert!(catalog1.load_by_filter(&filter1).is_ok()); assert!(catalog1.load_by_filter(&filter1).is_ok());
assert_eq!(catalog1.items.len(), 1); assert_eq!(catalog1.len().unwrap(), 1);
assert!(catalog1.items.contains_key("item-1")); assert!(catalog1.contains_key("item-1").unwrap());
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let filter2 = Filter::new().with_real("discount", Comparison::Lesser, 0.10); let filter2 = Filter::new().with_real("discount", Comparison::Lesser, 0.10);
assert!(catalog2.load_by_filter(&filter2).is_ok()); assert!(catalog2.load_by_filter(&filter2).is_ok());
assert!(catalog2.items.is_empty()); assert!(catalog2.is_empty().unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1248,9 +1248,9 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_real("discount", Comparison::GreaterOrEqual, 0.15); let filter = Filter::new().with_real("discount", Comparison::GreaterOrEqual, 0.15);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-2")); assert!(catalog.contains_key("item-2").unwrap());
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1280,12 +1280,12 @@ mod tests {
let mut catalog1 = Catalog::new(db_path); let mut catalog1 = Catalog::new(db_path);
let filter1 = Filter::new().with_real("discount", Comparison::GreaterOrEqual, 0.20); let filter1 = Filter::new().with_real("discount", Comparison::GreaterOrEqual, 0.20);
assert!(catalog1.load_by_filter(&filter1).is_ok()); assert!(catalog1.load_by_filter(&filter1).is_ok());
assert_eq!(catalog1.items.len(), 1); assert_eq!(catalog1.len().unwrap(), 1);
assert!(catalog1.items.contains_key("item-2")); assert!(catalog1.contains_key("item-2").unwrap());
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let filter2 = Filter::new().with_real("discount", Comparison::GreaterOrEqual, 0.21); let filter2 = Filter::new().with_real("discount", Comparison::GreaterOrEqual, 0.21);
assert!(catalog2.load_by_filter(&filter2).is_ok()); assert!(catalog2.load_by_filter(&filter2).is_ok());
assert!(catalog2.items.is_empty()); assert!(catalog2.is_empty().unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1320,9 +1320,9 @@ mod tests {
let mut catalog = Catalog::new(db_path); let mut catalog = Catalog::new(db_path);
let filter = Filter::new().with_real("discount", Comparison::LesserOrEqual, 0.15); let filter = Filter::new().with_real("discount", Comparison::LesserOrEqual, 0.15);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.len().unwrap(), 2);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
assert!(catalog.items.contains_key("item-3")); assert!(catalog.contains_key("item-3").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1352,12 +1352,12 @@ mod tests {
let mut catalog1 = Catalog::new(db_path); let mut catalog1 = Catalog::new(db_path);
let filter1 = Filter::new().with_real("discount", Comparison::LesserOrEqual, 0.10); let filter1 = Filter::new().with_real("discount", Comparison::LesserOrEqual, 0.10);
assert!(catalog1.load_by_filter(&filter1).is_ok()); assert!(catalog1.load_by_filter(&filter1).is_ok());
assert_eq!(catalog1.items.len(), 1); assert_eq!(catalog1.len().unwrap(), 1);
assert!(catalog1.items.contains_key("item-1")); assert!(catalog1.contains_key("item-1").unwrap());
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let filter2 = Filter::new().with_real("discount", Comparison::LesserOrEqual, 0.09); let filter2 = Filter::new().with_real("discount", Comparison::LesserOrEqual, 0.09);
assert!(catalog2.load_by_filter(&filter2).is_ok()); assert!(catalog2.load_by_filter(&filter2).is_ok());
assert!(catalog2.items.is_empty()); assert!(catalog2.is_empty().unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1408,8 +1408,8 @@ mod tests {
.with_real("discount", Comparison::Equal, 0.10) .with_real("discount", Comparison::Equal, 0.10)
.with_bool("in_stock", true); .with_bool("in_stock", true);
assert!(catalog.load_by_filter(&filter).is_ok()); assert!(catalog.load_by_filter(&filter).is_ok());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
assert!(catalog.items.contains_key("item-1")); assert!(catalog.contains_key("item-1").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
#[test] #[test]
@@ -1478,30 +1478,30 @@ mod tests {
let mut catalog1 = Catalog::new(db_path); let mut catalog1 = Catalog::new(db_path);
let filter1 = Filter::new().with_class("item"); let filter1 = Filter::new().with_class("item");
assert!(catalog1.load_by_filter(&filter1).is_ok()); assert!(catalog1.load_by_filter(&filter1).is_ok());
assert_eq!(catalog1.items.len(), 4); assert_eq!(catalog1.len().unwrap(), 4);
assert!(catalog1.items.contains_key("item-1")); assert!(catalog1.contains_key("item-1").unwrap());
assert!(catalog1.items.contains_key("item-2")); assert!(catalog1.contains_key("item-2").unwrap());
assert!(catalog1.items.contains_key("item-3")); assert!(catalog1.contains_key("item-3").unwrap());
assert!(catalog1.items.contains_key("item-4")); assert!(catalog1.contains_key("item-4").unwrap());
// Test 2: Filter by class "another_item" // Test 2: Filter by class "another_item"
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
let filter2 = Filter::new().with_class("another_item"); let filter2 = Filter::new().with_class("another_item");
assert!(catalog2.load_by_filter(&filter2).is_ok()); assert!(catalog2.load_by_filter(&filter2).is_ok());
assert_eq!(catalog2.items.len(), 1); assert_eq!(catalog2.len().unwrap(), 1);
assert!(catalog2.items.contains_key("another-1")); assert!(catalog2.contains_key("another-1").unwrap());
// Test 3: Filter by class "item" and subclass "sub-a" // Test 3: Filter by class "item" and subclass "sub-a"
let mut catalog3 = Catalog::new(db_path); let mut catalog3 = Catalog::new(db_path);
let filter3 = Filter::new().with_class("item").with_subclass("sub-a"); let filter3 = Filter::new().with_class("item").with_subclass("sub-a");
assert!(catalog3.load_by_filter(&filter3).is_ok()); assert!(catalog3.load_by_filter(&filter3).is_ok());
assert_eq!(catalog3.items.len(), 2); assert_eq!(catalog3.len().unwrap(), 2);
assert!(catalog3.items.contains_key("item-1")); assert!(catalog3.contains_key("item-1").unwrap());
assert!(catalog3.items.contains_key("item-3")); assert!(catalog3.contains_key("item-3").unwrap());
// Test 4: Filter by class "item" and subclass "subitem" (the default) // Test 4: Filter by class "item" and subclass "subitem" (the default)
let mut catalog4 = Catalog::new(db_path); let mut catalog4 = Catalog::new(db_path);
let filter4 = Filter::new().with_class("item").with_subclass("subitem"); let filter4 = Filter::new().with_class("item").with_subclass("subitem");
assert!(catalog4.load_by_filter(&filter4).is_ok()); assert!(catalog4.load_by_filter(&filter4).is_ok());
assert_eq!(catalog4.items.len(), 1); assert_eq!(catalog4.len().unwrap(), 1);
assert!(catalog4.items.contains_key("item-4")); assert!(catalog4.contains_key("item-4").unwrap());
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }
} }

View File

@@ -26,13 +26,16 @@ mod tests {
catalog1.persist().unwrap(); catalog1.persist().unwrap();
// 2. Create a new, empty catalog instance for the same DB. // 2. Create a new, empty catalog instance for the same DB.
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
assert!(catalog2.items.is_empty()); assert!(catalog2.is_empty().unwrap());
// 3. Load the item by its ID. // 3. Load the item by its ID.
let result = catalog2.load_by_id("item-1"); let result = catalog2.load_by_id("item-1");
assert!(result.is_ok()); assert!(result.is_ok());
// 4. Verify the item is now in the in-memory 'items' map. // 4. Verify the item is now in the in-memory 'items' map.
assert_eq!(catalog2.items.len(), 1); let len = catalog2.len().unwrap();
let loaded_entity = catalog2.items.get("item-1").unwrap(); assert_eq!(len, 1);
let loaded_entity = catalog2
.with_items(|items| Ok(items.get("item-1").unwrap().clone()))
.unwrap();
// 5. Verify the loaded entity's data and state. // 5. Verify the loaded entity's data and state.
assert_eq!(loaded_entity.id, "item-1"); assert_eq!(loaded_entity.id, "item-1");
assert_eq!(loaded_entity.class, "item"); assert_eq!(loaded_entity.class, "item");
@@ -88,7 +91,9 @@ mod tests {
..Item::default() ..Item::default()
}; };
let _ = catalog2.upsert(item_in_memory); let _ = catalog2.upsert(item_in_memory);
let entity_before_load = catalog2.items.get("item-1").unwrap(); let entity_before_load = catalog2
.with_items(|items| Ok(items.get("item-1").unwrap().clone()))
.unwrap();
assert_eq!(entity_before_load.state, EntityState::New); assert_eq!(entity_before_load.state, EntityState::New);
assert_eq!( assert_eq!(
entity_before_load.value_of("name"), entity_before_load.value_of("name"),
@@ -98,7 +103,9 @@ mod tests {
let result = catalog2.load_by_id("item-1"); let result = catalog2.load_by_id("item-1");
assert!(result.is_ok()); assert!(result.is_ok());
// 4. Verify that the in-memory entity has been replaced with the one from the DB. // 4. Verify that the in-memory entity has been replaced with the one from the DB.
let entity_after_load = catalog2.items.get("item-1").unwrap(); let entity_after_load = catalog2
.with_items(|items| Ok(items.get("item-1").unwrap().clone()))
.unwrap();
assert_eq!(entity_after_load.state, EntityState::Loaded); assert_eq!(entity_after_load.state, EntityState::Loaded);
assert_eq!( assert_eq!(
entity_after_load.value_of("name"), entity_after_load.value_of("name"),
@@ -134,7 +141,7 @@ mod tests {
let result = catalog.load_by_id("nonexistent-id"); let result = catalog.load_by_id("nonexistent-id");
// 3. Verify that the operation succeeded and the catalog remains empty. // 3. Verify that the operation succeeded and the catalog remains empty.
assert!(result.is_ok()); assert!(result.is_ok());
assert!(catalog.items.is_empty()); assert!(catalog.is_empty().unwrap());
// Clean up // Clean up
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
} }

View File

@@ -7,6 +7,6 @@ mod tests {
let path = "test.db"; let path = "test.db";
let catalog = Catalog::new(path); let catalog = Catalog::new(path);
assert_eq!(catalog.path, path); assert_eq!(catalog.path, path);
assert!(catalog.items.is_empty()); assert!(catalog.is_empty().unwrap());
} }
} }

View File

@@ -105,7 +105,9 @@ mod tests {
}; };
let _ = catalog2.upsert(updated_item.clone()); let _ = catalog2.upsert(updated_item.clone());
assert_eq!( assert_eq!(
catalog2.items.get("item-1").unwrap().state, catalog2
.with_items(|items| { Ok(items.get("item-1").unwrap().state) })
.unwrap(),
EntityState::Updated EntityState::Updated
); );
// 4. Persist the changes. // 4. Persist the changes.
@@ -197,7 +199,7 @@ mod tests {
let mut catalog_verify = Catalog::new(db_path); let mut catalog_verify = Catalog::new(db_path);
catalog_verify.load_by_class::<Item>().unwrap(); catalog_verify.load_by_class::<Item>().unwrap();
// Check total count // Check total count
assert_eq!(catalog_verify.items.len(), 3); assert_eq!(catalog_verify.len().unwrap(), 3);
// Verify added item // Verify added item
let added_item: Item = catalog_verify.get("add-me").unwrap().unwrap(); let added_item: Item = catalog_verify.get("add-me").unwrap().unwrap();
assert_eq!(added_item, item_to_add); assert_eq!(added_item, item_to_add);
@@ -277,17 +279,23 @@ mod tests {
let _ = catalog.upsert(item_untouched.clone()); let _ = catalog.upsert(item_untouched.clone());
catalog.persist().unwrap(); catalog.persist().unwrap();
// At this point, all items are in the DB and in-memory state is `Loaded`. // At this point, all items are in the DB and in-memory state is `Loaded`.
assert_eq!(catalog.items.len(), 3); assert_eq!(catalog.len().unwrap(), 3);
assert_eq!( assert_eq!(
catalog.items.get("update-me").unwrap().state, catalog
.with_items(|items| { Ok(items.get("update-me").unwrap().state) })
.unwrap(),
EntityState::Loaded EntityState::Loaded
); );
assert_eq!( assert_eq!(
catalog.items.get("delete-me").unwrap().state, catalog
.with_items(|items| { Ok(items.get("delete-me").unwrap().state) })
.unwrap(),
EntityState::Loaded EntityState::Loaded
); );
assert_eq!( assert_eq!(
catalog.items.get("keep-me").unwrap().state, catalog
.with_items(|items| { Ok(items.get("keep-me").unwrap().state) })
.unwrap(),
EntityState::Loaded EntityState::Loaded
); );
// 2. Manipulate the catalog to have entities in various states. // 2. Manipulate the catalog to have entities in various states.
@@ -315,34 +323,49 @@ mod tests {
catalog.delete("delete-me"); // State: ToDelete catalog.delete("delete-me"); // State: ToDelete
// 'item_untouched' remains with state `Loaded`. // 'item_untouched' remains with state `Loaded`.
// Check states before final persist // Check states before final persist
assert_eq!(catalog.items.get("add-me").unwrap().state, EntityState::New);
assert_eq!( assert_eq!(
catalog.items.get("update-me").unwrap().state, catalog
.with_items(|items| { Ok(items.get("add-me").unwrap().state) })
.unwrap(),
EntityState::New
);
assert_eq!(
catalog
.with_items(|items| { Ok(items.get("update-me").unwrap().state) })
.unwrap(),
EntityState::Updated EntityState::Updated
); );
assert_eq!( assert_eq!(
catalog.items.get("delete-me").unwrap().state, catalog
.with_items(|items| { Ok(items.get("delete-me").unwrap().state) })
.unwrap(),
EntityState::ToDelete EntityState::ToDelete
); );
assert_eq!( assert_eq!(
catalog.items.get("keep-me").unwrap().state, catalog
.with_items(|items| { Ok(items.get("keep-me").unwrap().state) })
.unwrap(),
EntityState::Loaded EntityState::Loaded
); );
assert_eq!(catalog.items.len(), 4); assert_eq!(catalog.len().unwrap(), 4);
// 3. Persist all changes. // 3. Persist all changes.
catalog.persist().unwrap(); catalog.persist().unwrap();
// 4. Verify the in-memory state after persisting. // 4. Verify the in-memory state after persisting.
// The item marked for deletion should be gone. // The item marked for deletion should be gone.
assert!(!catalog.items.contains_key("delete-me")); assert!(!catalog.contains_key("delete-me").unwrap());
assert_eq!(catalog.items.len(), 3); assert_eq!(catalog.len().unwrap(), 3);
// All remaining items should have their state as `Loaded`. // All remaining items should have their state as `Loaded`.
let new_item_entity = catalog.items.get("add-me").unwrap(); let new_item_entity = catalog
.with_items(|items| Ok(items.get("add-me").unwrap().clone()))
.unwrap();
assert_eq!(new_item_entity.state, EntityState::Loaded); assert_eq!(new_item_entity.state, EntityState::Loaded);
assert_eq!( assert_eq!(
new_item_entity.value_of("name"), new_item_entity.value_of("name"),
Some(&Value::from("Add Me")) Some(&Value::from("Add Me"))
); );
let updated_item_entity = catalog.items.get("update-me").unwrap(); let updated_item_entity = catalog
.with_items(|items| Ok(items.get("update-me").unwrap().clone()))
.unwrap();
assert_eq!(updated_item_entity.state, EntityState::Loaded); assert_eq!(updated_item_entity.state, EntityState::Loaded);
assert_eq!( assert_eq!(
updated_item_entity.value_of("name"), updated_item_entity.value_of("name"),
@@ -352,7 +375,9 @@ mod tests {
updated_item_entity.value_of("sell_trend"), updated_item_entity.value_of("sell_trend"),
Some(&Value::from(10i64)) Some(&Value::from(10i64))
); );
let untouched_item_entity = catalog.items.get("keep-me").unwrap(); let untouched_item_entity = catalog
.with_items(|items| Ok(items.get("keep-me").unwrap().clone()))
.unwrap();
assert_eq!(untouched_item_entity.state, EntityState::Loaded); assert_eq!(untouched_item_entity.state, EntityState::Loaded);
assert_eq!( assert_eq!(
untouched_item_entity.value_of("name"), untouched_item_entity.value_of("name"),

View File

@@ -15,7 +15,9 @@ mod tests {
}; };
let item_id = item.id.clone(); let item_id = item.id.clone();
let _ = catalog.upsert(item); let _ = catalog.upsert(item);
let entity = catalog.items.get(&item_id).unwrap(); let entity = catalog
.with_items(|items| Ok(items.get(&item_id).unwrap().clone()))
.unwrap();
assert_eq!(entity.id, item_id); assert_eq!(entity.id, item_id);
assert_eq!(entity.state, EntityState::New); assert_eq!(entity.state, EntityState::New);
assert_eq!(entity.class, "item"); assert_eq!(entity.class, "item");
@@ -47,8 +49,10 @@ mod tests {
..Item::default() ..Item::default()
}; };
let _ = catalog.upsert(item2); let _ = catalog.upsert(item2);
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.len().unwrap(), 1);
let entity = catalog.items.get(&item_id).unwrap(); let entity = catalog
.with_items(|items| Ok(items.get(&item_id).unwrap().clone()))
.unwrap();
assert_eq!(entity.value_of("name"), Some(&Value::from("Second Item"))); assert_eq!(entity.value_of("name"), Some(&Value::from("Second Item")));
assert_eq!(entity.value_of("price"), Some(&Value::from(200u64))); assert_eq!(entity.value_of("price"), Some(&Value::from(200u64)));
assert_eq!(entity.value_of("sell_trend"), Some(&Value::from(10i64))); assert_eq!(entity.value_of("sell_trend"), Some(&Value::from(10i64)));

View File

@@ -22,7 +22,7 @@ mod tests {
// Verifies that entities marked as 'New' in the catalog are inserted into the database, along with all their attributes. // Verifies that entities marked as 'New' in the catalog are inserted into the database, along with all their attributes.
let db_path = Path::new("test_insert.db"); let db_path = Path::new("test_insert.db");
let conn = setup_db(db_path); let conn = setup_db(db_path);
let mut catalog = Catalog::new(db_path.to_str().unwrap()); let catalog = Catalog::new(db_path.to_str().unwrap());
let mut entity = Entity { let mut entity = Entity {
id: "e1".to_string(), id: "e1".to_string(),
class: "c1".to_string(), class: "c1".to_string(),
@@ -38,8 +38,14 @@ mod tests {
value: Value::Text("v1".to_string()), value: Value::Text("v1".to_string()),
}, },
); );
catalog.items.insert("e1".to_string(), entity); let _ = catalog.on_items(|items| {
assert!(sqlite::persist::catalog(db_path, &catalog).is_ok()); items.insert("e1".to_string(), entity);
Ok(())
});
let is_ok = catalog
.with_items(|items| Ok(sqlite::persist::catalog(db_path, items).is_ok()))
.unwrap();
assert!(is_ok);
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 1); assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 1);
assert_eq!(count_rows(&conn, "attribute", "entity_id = 'e1'"), 1); assert_eq!(count_rows(&conn, "attribute", "entity_id = 'e1'"), 1);
fs::remove_file(db_path).unwrap(); fs::remove_file(db_path).unwrap();
@@ -51,7 +57,7 @@ mod tests {
let conn = setup_db(db_path); let conn = setup_db(db_path);
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", []) conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
.unwrap(); .unwrap();
let mut catalog = Catalog::new(db_path.to_str().unwrap()); let catalog = Catalog::new(db_path.to_str().unwrap());
let entity = Entity { let entity = Entity {
id: "e1".to_string(), id: "e1".to_string(),
class: "c1".to_string(), class: "c1".to_string(),
@@ -60,8 +66,14 @@ mod tests {
state: EntityState::ToDelete, state: EntityState::ToDelete,
ref_date: None, ref_date: None,
}; };
catalog.items.insert("e1".to_string(), entity); let _ = catalog.on_items(|items| {
assert!(sqlite::persist::catalog(db_path, &catalog).is_ok()); items.insert("e1".to_string(), entity);
Ok(())
});
let is_ok = catalog
.with_items(|items| Ok(sqlite::persist::catalog(db_path, items).is_ok()))
.unwrap();
assert!(is_ok);
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 0); assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 0);
fs::remove_file(db_path).unwrap(); fs::remove_file(db_path).unwrap();
} }
@@ -72,7 +84,7 @@ mod tests {
let conn = setup_db(db_path); let conn = setup_db(db_path);
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", []) conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
.unwrap(); .unwrap();
let mut catalog = Catalog::new(db_path.to_str().unwrap()); let catalog = Catalog::new(db_path.to_str().unwrap());
let to_delete = Entity { let to_delete = Entity {
id: "e1".to_string(), id: "e1".to_string(),
class: "c1".to_string(), class: "c1".to_string(),
@@ -89,9 +101,18 @@ mod tests {
state: EntityState::New, state: EntityState::New,
ref_date: None, ref_date: None,
}; };
catalog.items.insert("e1".to_string(), to_delete); let _ = catalog.on_items(|items| {
catalog.items.insert("e2".to_string(), to_add); items.insert("e1".to_string(), to_delete);
assert!(sqlite::persist::catalog(db_path, &catalog).is_ok()); Ok(())
});
let _ = catalog.on_items(|items| {
items.insert("e2".to_string(), to_add);
Ok(())
});
let is_ok = catalog
.with_items(|items| Ok(sqlite::persist::catalog(db_path, items).is_ok()))
.unwrap();
assert!(is_ok);
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 0); assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 0);
assert_eq!(count_rows(&conn, "entity", "id = 'e2'"), 1); assert_eq!(count_rows(&conn, "entity", "id = 'e2'"), 1);
fs::remove_file(db_path).unwrap(); fs::remove_file(db_path).unwrap();
@@ -103,7 +124,7 @@ mod tests {
let conn = setup_db(db_path); let conn = setup_db(db_path);
conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", []) conn.execute("INSERT INTO entity (id, class) VALUES ('e1', 'c1')", [])
.unwrap(); .unwrap();
let mut catalog = Catalog::new(db_path.to_str().unwrap()); let catalog = Catalog::new(db_path.to_str().unwrap());
let unmodified = Entity { let unmodified = Entity {
id: "e1".to_string(), id: "e1".to_string(),
class: "c1".to_string(), class: "c1".to_string(),
@@ -112,8 +133,14 @@ mod tests {
state: EntityState::Loaded, state: EntityState::Loaded,
ref_date: None, ref_date: None,
}; };
catalog.items.insert("e1".to_string(), unmodified); let _ = catalog.on_items(|items| {
assert!(sqlite::persist::catalog(db_path, &catalog).is_ok()); items.insert("e1".to_string(), unmodified);
Ok(())
});
let is_ok = catalog
.with_items(|items| Ok(sqlite::persist::catalog(db_path, items).is_ok()))
.unwrap();
assert!(is_ok);
assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 1); assert_eq!(count_rows(&conn, "entity", "id = 'e1'"), 1);
fs::remove_file(db_path).unwrap(); fs::remove_file(db_path).unwrap();
} }
@@ -127,7 +154,7 @@ mod tests {
[], [],
) )
.unwrap(); .unwrap();
let mut catalog = Catalog::new(db_path.to_str().unwrap()); let catalog = Catalog::new(db_path.to_str().unwrap());
let mut new_entity = Entity { let mut new_entity = Entity {
id: "e_new".to_string(), id: "e_new".to_string(),
class: "c1".to_string(), class: "c1".to_string(),
@@ -143,12 +170,17 @@ mod tests {
value: Value::Text("v1".to_string()), value: Value::Text("v1".to_string()),
}, },
); );
catalog.items.insert("e_new".to_string(), new_entity); let _ = catalog.on_items(|items| {
items.insert("e_new".to_string(), new_entity);
Ok(())
});
// Corrupt the DB to cause a failure during the transaction // Corrupt the DB to cause a failure during the transaction
conn.execute("DROP TABLE attribute", []).unwrap(); conn.execute("DROP TABLE attribute", []).unwrap();
drop(conn); drop(conn);
let result = sqlite::persist::catalog(db_path, &catalog); let is_err = catalog
assert!(result.is_err()); .with_items(|items| Ok(sqlite::persist::catalog(db_path, items).is_err()))
.unwrap();
assert!(is_err);
// Re-open connection to check state // Re-open connection to check state
let conn = Connection::open(db_path).unwrap(); let conn = Connection::open(db_path).unwrap();
// The new entity should not have been inserted due to rollback // The new entity should not have been inserted due to rollback