review: change trait requirements for EAV implementors

This commit is contained in:
2025-10-17 15:46:50 +02:00
parent 5b0aa07a07
commit c865c72c01
4 changed files with 134 additions and 104 deletions

View File

@@ -70,7 +70,7 @@ fn main() {
price: 125000, price: 125000,
}; };
// Insert the new laptop into the catalog. Note that at this time the product is in memory. // Insert the new laptop into the catalog. Note that at this time the product is in memory.
catalog.upsert(new_laptop); let _ = catalog.upsert(new_laptop);
// Persist the changes in the catalog to the database. // Persist the changes in the catalog to the database.
catalog.persist().unwrap(); catalog.persist().unwrap();
// Remove the SQLite database file. // Remove the SQLite database file.

View File

@@ -39,14 +39,15 @@ impl Catalog {
/// # Arguments /// # Arguments
/// ///
/// * `object` - The object to insert. /// * `object` - The object to insert.
pub fn upsert(&mut self, object: impl EAV) { pub fn upsert(&mut self, object: impl EAV) -> Result<(), FailedTo> {
let mut entity = object.into(); let mut entity = object.try_into().map_err(|_| FailedTo::ConvertObject)?;
if self.items.contains_key(&entity.id) { if self.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); self.items.insert(entity.id.clone(), entity);
Ok(())
} }
/// Inserts multiple objects that implement the `EAV` trait into the catalog. /// Inserts multiple objects that implement the `EAV` trait into the catalog.
@@ -54,10 +55,11 @@ impl Catalog {
/// # Arguments /// # Arguments
/// ///
/// * `objects` - A vector of objects to insert. /// * `objects` - A vector of objects to insert.
pub fn insert_many(&mut self, objects: Vec<impl EAV>) { pub fn insert_many(&mut self, objects: Vec<impl EAV>) -> Result<(), FailedTo> {
for object in objects { for object in objects {
self.upsert(object); self.upsert(object)?;
} }
Ok(())
} }
/// Retrieves an entity by its ID and converts it into a specified type `T`. /// Retrieves an entity by its ID and converts it into a specified type `T`.
@@ -69,12 +71,14 @@ impl Catalog {
/// # Returns /// # Returns
/// ///
/// An `Option<T>` containing the converted entity if found, otherwise `None`. /// An `Option<T>` containing the converted entity if found, otherwise `None`.
pub fn get<T>(&self, id: &str) -> Option<T> pub fn get<T>(&self, id: &str) -> Result<Option<T>, FailedTo>
where where
T: EAV, T: EAV,
{ {
let entity = self.items.get(id); let entity = self.items.get(id);
entity.map(|e| T::from(e.clone())) entity
.map(|e| T::try_from(e.clone()).map_err(|_| FailedTo::ConvertEntity))
.transpose()
} }
/// Retrieves the first entity that matches a given attribute and value. /// Retrieves the first entity that matches a given attribute and value.
@@ -91,7 +95,7 @@ impl Catalog {
&self, &self,
attribute: &str, attribute: &str,
value: impl Into<Value> + Clone, value: impl Into<Value> + Clone,
) -> Option<T> ) -> Result<Option<T>, FailedTo>
where where
T: EAV, T: EAV,
{ {
@@ -101,8 +105,8 @@ impl Catalog {
.filter(|item| item.class == T::class()) .filter(|item| item.class == T::class())
.filter(|item| item.value_of(attribute) == Some(&value.clone().into())) .filter(|item| item.value_of(attribute) == Some(&value.clone().into()))
.take(1) .take(1)
.map(|item| T::from(item.clone())); .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity));
items.next() items.next().transpose()
} }
/// Returns an iterator over entities of a specific class. /// Returns an iterator over entities of a specific class.
@@ -110,14 +114,14 @@ impl Catalog {
/// # Returns /// # Returns
/// ///
/// An iterator that yields items of type `T`. /// An iterator that yields items of type `T`.
pub fn list_by_class<T>(&self) -> impl Iterator<Item = T> pub fn list_by_class<T>(&self) -> impl Iterator<Item = Result<T, FailedTo>>
where where
T: EAV, T: EAV,
{ {
self.items self.items
.values() .values()
.filter(move |item| item.class == T::class()) .filter(move |item| item.class == T::class())
.map(|item| T::from(item.clone())) .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity))
} }
/// Returns an iterator over entities that match a given attribute and value. /// Returns an iterator over entities that match a given attribute and value.
@@ -134,7 +138,7 @@ impl Catalog {
&self, &self,
attribute: &str, attribute: &str,
value: impl Into<Value> + Clone, value: impl Into<Value> + Clone,
) -> impl Iterator<Item = T> ) -> impl Iterator<Item = Result<T, FailedTo>>
where where
T: EAV, T: EAV,
{ {
@@ -143,7 +147,7 @@ impl Catalog {
.values() .values()
.filter(move |item| item.class == T::class()) .filter(move |item| item.class == T::class())
.filter(move |item| item.value_of(attribute) == Some(&value)) .filter(move |item| item.value_of(attribute) == Some(&value))
.map(|item| T::from(item.clone())) .map(|item| T::try_from(item.clone()).map_err(|_| FailedTo::ConvertEntity))
} }
/// Schedules an entity for deletion. Actual delition will take place when 'persist' is called. /// Schedules an entity for deletion. Actual delition will take place when 'persist' is called.
@@ -321,7 +325,7 @@ mod tests {
in_stock: true, in_stock: true,
}; };
let item_id = item.id.clone(); let item_id = item.id.clone();
catalog.upsert(item); let _ = catalog.upsert(item);
let entity = catalog.items.get(&item_id).unwrap(); let entity = catalog.items.get(&item_id).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);
@@ -342,14 +346,14 @@ mod tests {
in_stock: true, in_stock: true,
}; };
let item_id = item1.id.clone(); let item_id = item1.id.clone();
catalog.upsert(item1); let _ = catalog.upsert(item1);
let item2 = Item { let item2 = Item {
id: "item-123".to_string(), id: "item-123".to_string(),
name: "Second Item".to_string(), name: "Second Item".to_string(),
price: 200, price: 200,
in_stock: false, in_stock: false,
}; };
catalog.upsert(item2); let _ = catalog.upsert(item2);
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.items.len(), 1);
let entity = catalog.items.get(&item_id).unwrap(); let entity = catalog.items.get(&item_id).unwrap();
assert_eq!(entity.value_of("name"), Some(&Value::from("Second Item"))); assert_eq!(entity.value_of("name"), Some(&Value::from("Second Item")));
@@ -376,7 +380,7 @@ mod tests {
in_stock: false, in_stock: false,
}, },
]; ];
catalog.insert_many(items); let _ = catalog.insert_many(items);
assert_eq!(catalog.items.len(), 2); assert_eq!(catalog.items.len(), 2);
let entity1 = catalog.items.get("item-1").unwrap(); let entity1 = catalog.items.get("item-1").unwrap();
assert_eq!(entity1.state, EntityState::New); assert_eq!(entity1.state, EntityState::New);
@@ -397,8 +401,8 @@ mod tests {
price: 100, price: 100,
in_stock: true, in_stock: true,
}; };
catalog.upsert(item.clone()); let _ = catalog.upsert(item.clone());
let retrieved_item: Option<Item> = catalog.get("item-123"); let retrieved_item: Option<Item> = catalog.get::<Item>("item-123").unwrap();
assert_eq!(retrieved_item, Some(item)); assert_eq!(retrieved_item, Some(item));
} }
@@ -412,8 +416,8 @@ mod tests {
price: 100, price: 100,
in_stock: true, in_stock: true,
}; };
catalog.upsert(item.clone()); let _ = catalog.upsert(item.clone());
let retrieved_item: Option<Item> = catalog.get("nonexistent-id"); let retrieved_item: Option<Item> = catalog.get("nonexistent-id").unwrap();
assert!(retrieved_item.is_none()); assert!(retrieved_item.is_none());
} }
@@ -434,10 +438,11 @@ mod tests {
price: 200, price: 200,
in_stock: false, in_stock: false,
}; };
catalog.upsert(item1.clone()); let _ = catalog.upsert(item1.clone());
catalog.upsert(item2.clone()); let _ = catalog.upsert(item2.clone());
let retrieved_item: Option<Item> = let retrieved_item: Option<Item> = catalog
catalog.get_by_class_and_attribute("name", "Unique Item"); .get_by_class_and_attribute("name", "Unique Item")
.unwrap();
assert_eq!(retrieved_item, Some(item2)); assert_eq!(retrieved_item, Some(item2));
} }
@@ -457,17 +462,21 @@ mod tests {
price: 250, price: 250,
in_stock: false, in_stock: false,
}; };
catalog.upsert(item1.clone()); let _ = catalog.upsert(item1.clone());
catalog.upsert(item2.clone()); let _ = catalog.upsert(item2.clone());
// Test with &str for String attribute // Test with &str for String attribute
let retrieved_by_name: Option<Item> = let retrieved_by_name: Option<Item> = catalog
catalog.get_by_class_and_attribute("name", "Item One"); .get_by_class_and_attribute("name", "Item One")
.unwrap();
assert_eq!(retrieved_by_name, Some(item1.clone())); assert_eq!(retrieved_by_name, Some(item1.clone()));
// Test with u64 for price attribute // Test with u64 for price attribute
let retrieved_by_price: Option<Item> = catalog.get_by_class_and_attribute("price", 250u64); let retrieved_by_price: Option<Item> =
catalog.get_by_class_and_attribute("price", 250u64).unwrap();
assert_eq!(retrieved_by_price, Some(item2.clone())); assert_eq!(retrieved_by_price, Some(item2.clone()));
// Test with bool for in_stock attribute // Test with bool for in_stock attribute
let retrieved_by_stock: Option<Item> = catalog.get_by_class_and_attribute("in_stock", true); let retrieved_by_stock: Option<Item> = catalog
.get_by_class_and_attribute("in_stock", true)
.unwrap();
assert_eq!(retrieved_by_stock, Some(item1.clone())); assert_eq!(retrieved_by_stock, Some(item1.clone()));
} }
@@ -481,14 +490,16 @@ mod tests {
price: 100, price: 100,
in_stock: true, in_stock: true,
}; };
catalog.upsert(item.clone()); let _ = catalog.upsert(item.clone());
// Test with a value that doesn't exist // Test with a value that doesn't exist
let retrieved_item: Option<Item> = let retrieved_item: Option<Item> = catalog
catalog.get_by_class_and_attribute("name", "Non-existent Name"); .get_by_class_and_attribute("name", "Non-existent Name")
.unwrap();
assert!(retrieved_item.is_none()); assert!(retrieved_item.is_none());
// Test with an attribute that doesn't exist // Test with an attribute that doesn't exist
let retrieved_item_2: Option<Item> = let retrieved_item_2: Option<Item> = catalog
catalog.get_by_class_and_attribute("non-existent-attribute", "Test Item"); .get_by_class_and_attribute("non-existent-attribute", "Test Item")
.unwrap();
assert!(retrieved_item_2.is_none()); assert!(retrieved_item_2.is_none());
} }
@@ -509,9 +520,12 @@ mod tests {
price: 200, price: 200,
in_stock: false, in_stock: false,
}; };
catalog.upsert(item1.clone()); let _ = catalog.upsert(item1.clone());
catalog.upsert(item2.clone()); let _ = catalog.upsert(item2.clone());
let results: Vec<Item> = catalog.list_by_class::<Item>().collect(); let results: Vec<Item> = catalog
.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(&item1));
assert!(results.contains(&item2)); assert!(results.contains(&item2));
@@ -521,7 +535,10 @@ mod tests {
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.list_by_class::<Item>().collect(); let results: Vec<Item> = catalog
.list_by_class::<Item>()
.map(|item| item.unwrap())
.collect();
assert!(results.is_empty()); assert!(results.is_empty());
} }
@@ -548,11 +565,12 @@ mod tests {
price: 300, price: 300,
in_stock: true, in_stock: true,
}; };
catalog.upsert(item1.clone()); let _ = catalog.upsert(item1.clone());
catalog.upsert(item2.clone()); let _ = catalog.upsert(item2.clone());
catalog.upsert(item3.clone()); let _ = catalog.upsert(item3.clone());
let results: Vec<Item> = catalog let results: Vec<Item> = catalog
.list_by_class_and_attribute("in_stock", true) .list_by_class_and_attribute("in_stock", true)
.map(|item| item.unwrap())
.collect(); .collect();
assert_eq!(results.len(), 2); assert_eq!(results.len(), 2);
assert!(results.contains(&item1)); assert!(results.contains(&item1));
@@ -570,21 +588,24 @@ mod tests {
price: 100, price: 100,
in_stock: true, in_stock: true,
}; };
catalog.upsert(item); let _ = catalog.upsert(item);
// Search for a value that doesn't exist // Search for a value that doesn't exist
let results: Vec<Item> = catalog let results: Vec<Item> = catalog
.list_by_class_and_attribute("in_stock", false) .list_by_class_and_attribute("in_stock", false)
.map(|item| item.unwrap())
.collect(); .collect();
assert!(results.is_empty()); assert!(results.is_empty());
// Search for an attribute that doesn't exist // Search for an attribute that doesn't exist
let results_2: Vec<Item> = catalog let results_2: Vec<Item> = catalog
.list_by_class_and_attribute("non-existent-attribute", true) .list_by_class_and_attribute("non-existent-attribute", true)
.map(|item| item.unwrap())
.collect(); .collect();
assert!(results_2.is_empty()); assert!(results_2.is_empty());
// Search in a completely empty catalog // Search in a completely empty catalog
let empty_catalog = Catalog::new("dummy.db"); let empty_catalog = Catalog::new("dummy.db");
let results_3: Vec<Item> = empty_catalog let results_3: Vec<Item> = empty_catalog
.list_by_class_and_attribute("any_attribute", "any_value") .list_by_class_and_attribute("any_attribute", "any_value")
.map(|item| item.unwrap())
.collect(); .collect();
assert!(results_3.is_empty()); assert!(results_3.is_empty());
} }
@@ -601,7 +622,7 @@ mod tests {
in_stock: true, in_stock: true,
}; };
let item_id = item.id.clone(); let item_id = item.id.clone();
catalog.upsert(item); let _ = catalog.upsert(item);
catalog.delete(&item_id); catalog.delete(&item_id);
let entity = catalog.items.get(&item_id).unwrap(); let entity = catalog.items.get(&item_id).unwrap();
assert_eq!(entity.state, EntityState::ToDelete); assert_eq!(entity.state, EntityState::ToDelete);
@@ -617,7 +638,7 @@ mod tests {
price: 100, price: 100,
in_stock: true, in_stock: true,
}; };
catalog.upsert(item); let _ = catalog.upsert(item);
let original_items = catalog.items.clone(); 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");
@@ -643,13 +664,13 @@ mod tests {
price: 123, price: 123,
in_stock: true, in_stock: true,
}; };
catalog1.upsert(item1.clone()); let _ = catalog1.upsert(item1.clone());
assert!(catalog1.persist().is_ok()); assert!(catalog1.persist().is_ok());
// 2. Create a new catalog and load the item to verify it was persisted // 2. Create a new catalog and load the item to verify it was persisted
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
assert!(catalog2.load_by_id("item-1").is_ok()); assert!(catalog2.load_by_id("item-1").is_ok());
// 3. Get the item and assert it's the same as the one we inserted // 3. Get the item and assert it's the same as the one we inserted
let loaded_item: Option<Item> = catalog2.get("item-1"); let loaded_item: Option<Item> = catalog2.get("item-1").unwrap();
assert_eq!(loaded_item, Some(item1)); assert_eq!(loaded_item, Some(item1));
// Clean up // Clean up
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
@@ -673,7 +694,7 @@ mod tests {
price: 123, price: 123,
in_stock: true, in_stock: true,
}; };
catalog1.upsert(item1.clone()); let _ = catalog1.upsert(item1.clone());
assert!(catalog1.persist().is_ok()); assert!(catalog1.persist().is_ok());
// 2. Mark the item for deletion and persist again. // 2. Mark the item for deletion and persist again.
catalog1.delete(&item1.id); catalog1.delete(&item1.id);
@@ -682,7 +703,7 @@ mod tests {
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
assert!(catalog2.load_by_id(&item1.id).is_ok()); assert!(catalog2.load_by_id(&item1.id).is_ok());
// 4. Assert that the item was not found. // 4. Assert that the item was not found.
let loaded_item: Option<Item> = catalog2.get(&item1.id); let loaded_item: Option<Item> = catalog2.get(&item1.id).unwrap();
assert!(loaded_item.is_none()); assert!(loaded_item.is_none());
// Clean up // Clean up
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
@@ -706,7 +727,7 @@ mod tests {
price: 100, price: 100,
in_stock: true, in_stock: true,
}; };
catalog1.upsert(original_item.clone()); let _ = catalog1.upsert(original_item.clone());
catalog1.persist().unwrap(); catalog1.persist().unwrap();
// 2. Load it into a new catalog to simulate a separate session. // 2. Load it into a new catalog to simulate a separate session.
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
@@ -718,7 +739,7 @@ mod tests {
price: 200, price: 200,
in_stock: false, in_stock: false,
}; };
catalog2.upsert(updated_item.clone()); let _ = catalog2.upsert(updated_item.clone());
assert_eq!( assert_eq!(
catalog2.items.get("item-1").unwrap().state, catalog2.items.get("item-1").unwrap().state,
EntityState::Updated EntityState::Updated
@@ -728,7 +749,7 @@ mod tests {
// 5. Load the data into a third catalog to verify the update was written to the DB. // 5. Load the data into a third catalog to verify the update was written to the DB.
let mut catalog3 = Catalog::new(db_path); let mut catalog3 = Catalog::new(db_path);
catalog3.load_by_id("item-1").unwrap(); catalog3.load_by_id("item-1").unwrap();
let loaded_item: Item = catalog3.get("item-1").unwrap(); let loaded_item: Item = catalog3.get("item-1").unwrap().unwrap();
// 6. Assert that the loaded item has the updated values. // 6. Assert that the loaded item has the updated values.
assert_eq!(loaded_item, updated_item); assert_eq!(loaded_item, updated_item);
assert_ne!(loaded_item, original_item); assert_ne!(loaded_item, original_item);
@@ -766,9 +787,9 @@ mod tests {
price: 30, price: 30,
in_stock: true, in_stock: true,
}; };
catalog_setup.upsert(item_to_update_original.clone()); let _ = catalog_setup.upsert(item_to_update_original.clone());
catalog_setup.upsert(item_to_delete.clone()); let _ = catalog_setup.upsert(item_to_delete.clone());
catalog_setup.upsert(item_to_keep.clone()); let _ = catalog_setup.upsert(item_to_keep.clone());
catalog_setup.persist().unwrap(); catalog_setup.persist().unwrap();
// 2. Manipulation: Load the data and perform mixed operations. // 2. Manipulation: Load the data and perform mixed operations.
let mut catalog_ops = Catalog::new(db_path); let mut catalog_ops = Catalog::new(db_path);
@@ -780,7 +801,7 @@ mod tests {
price: 40, price: 40,
in_stock: false, in_stock: false,
}; };
catalog_ops.upsert(item_to_add.clone()); // State: New let _ = catalog_ops.upsert(item_to_add.clone()); // State: New
// An updated version of an existing item. // An updated version of an existing item.
let item_to_update_new = Item { let item_to_update_new = Item {
id: "update-me".to_string(), id: "update-me".to_string(),
@@ -788,7 +809,7 @@ mod tests {
price: 11, price: 11,
in_stock: false, in_stock: false,
}; };
catalog_ops.upsert(item_to_update_new.clone()); // State: Updated let _ = catalog_ops.upsert(item_to_update_new.clone()); // State: Updated
// An item to be deleted. // An item to be deleted.
catalog_ops.delete("delete-me"); // State: ToDelete catalog_ops.delete("delete-me"); // State: ToDelete
// item_to_keep is left untouched (State: Synced after load) // item_to_keep is left untouched (State: Synced after load)
@@ -800,16 +821,16 @@ mod tests {
// Check total count // Check total count
assert_eq!(catalog_verify.items.len(), 3); assert_eq!(catalog_verify.items.len(), 3);
// Verify added item // Verify added item
let added_item: Item = catalog_verify.get("add-me").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);
// Verify updated item // Verify updated item
let updated_item: Item = catalog_verify.get("update-me").unwrap(); let updated_item: Item = catalog_verify.get("update-me").unwrap().unwrap();
assert_eq!(updated_item, item_to_update_new); assert_eq!(updated_item, item_to_update_new);
// Verify deleted item // Verify deleted item
let deleted_item: Option<Item> = catalog_verify.get("delete-me"); let deleted_item: Option<Item> = catalog_verify.get("delete-me").unwrap();
assert!(deleted_item.is_none()); assert!(deleted_item.is_none());
// Verify untouched item // Verify untouched item
let kept_item: Item = catalog_verify.get("keep-me").unwrap(); let kept_item: Item = catalog_verify.get("keep-me").unwrap().unwrap();
assert_eq!(kept_item, item_to_keep); assert_eq!(kept_item, item_to_keep);
// Clean up // Clean up
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
@@ -828,7 +849,7 @@ mod tests {
price: 10, price: 10,
in_stock: true, in_stock: true,
}; };
catalog.upsert(item); let _ = catalog.upsert(item);
let result = catalog.persist(); let result = catalog.persist();
assert!(result.is_err()); assert!(result.is_err());
assert_eq!(result.unwrap_err(), FailedTo::PersistCatalog); assert_eq!(result.unwrap_err(), FailedTo::PersistCatalog);
@@ -864,9 +885,9 @@ mod tests {
name: "Keep Me".to_string(), name: "Keep Me".to_string(),
..Default::default() ..Default::default()
}; };
catalog.upsert(item_to_update.clone()); let _ = catalog.upsert(item_to_update.clone());
catalog.upsert(item_to_delete.clone()); let _ = catalog.upsert(item_to_delete.clone());
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.items.len(), 3);
@@ -889,14 +910,14 @@ mod tests {
name: "Add Me".to_string(), name: "Add Me".to_string(),
..Default::default() ..Default::default()
}; };
catalog.upsert(item_new.clone()); // State: New let _ = catalog.upsert(item_new.clone()); // State: New
// An updated version of an existing item. // An updated version of an existing item.
let item_updated = Item { let item_updated = Item {
id: "update-me".to_string(), id: "update-me".to_string(),
name: "Updated".to_string(), name: "Updated".to_string(),
..Default::default() ..Default::default()
}; };
catalog.upsert(item_updated.clone()); // State: Updated let _ = catalog.upsert(item_updated.clone()); // State: Updated
// An item to be deleted. // An item to be deleted.
catalog.delete("delete-me"); // State: ToDelete catalog.delete("delete-me"); // State: ToDelete
// 'item_untouched' remains with state `Loaded`. // 'item_untouched' remains with state `Loaded`.
@@ -963,7 +984,7 @@ mod tests {
price: 123, price: 123,
in_stock: true, in_stock: true,
}; };
catalog1.upsert(item_to_persist.clone()); let _ = catalog1.upsert(item_to_persist.clone());
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);
@@ -985,7 +1006,7 @@ mod tests {
assert_eq!(loaded_entity.value_of("in_stock"), Some(&Value::from(true))); assert_eq!(loaded_entity.value_of("in_stock"), Some(&Value::from(true)));
assert_eq!(loaded_entity.state, EntityState::Loaded); // Should be Synced after loading. assert_eq!(loaded_entity.state, EntityState::Loaded); // Should be Synced after loading.
// 6. Also verify by using the public 'get' method. // 6. Also verify by using the public 'get' method.
let retrieved_item: Option<Item> = catalog2.get("item-1"); let retrieved_item: Option<Item> = catalog2.get("item-1").unwrap();
assert_eq!(retrieved_item, Some(item_to_persist)); assert_eq!(retrieved_item, Some(item_to_persist));
// Clean up // Clean up
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
@@ -1009,7 +1030,7 @@ mod tests {
price: 100, price: 100,
in_stock: true, in_stock: true,
}; };
catalog1.upsert(item_in_db.clone()); let _ = catalog1.upsert(item_in_db.clone());
catalog1.persist().unwrap(); catalog1.persist().unwrap();
// 2. Create a new catalog and add a *different* in-memory version of the same item. // 2. Create a new catalog and add a *different* in-memory version of the same item.
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
@@ -1019,7 +1040,7 @@ mod tests {
price: 200, price: 200,
in_stock: false, in_stock: false,
}; };
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.items.get("item-1").unwrap();
assert_eq!(entity_before_load.state, EntityState::New); assert_eq!(entity_before_load.state, EntityState::New);
assert_eq!( assert_eq!(
@@ -1041,7 +1062,7 @@ mod tests {
Some(&Value::from(100u64)) Some(&Value::from(100u64))
); );
// 5. Verify using the public 'get' method. // 5. Verify using the public 'get' method.
let retrieved_item: Item = catalog2.get("item-1").unwrap(); let retrieved_item: Item = catalog2.get("item-1").unwrap().unwrap();
assert_eq!(retrieved_item, item_in_db); assert_eq!(retrieved_item, item_in_db);
// Clean up // Clean up
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
@@ -1108,8 +1129,8 @@ mod tests {
price: 200, price: 200,
in_stock: false, in_stock: false,
}; };
catalog1.upsert(item1.clone()); let _ = catalog1.upsert(item1.clone());
catalog1.upsert(item2.clone()); let _ = catalog1.upsert(item2.clone());
catalog1.persist().unwrap(); catalog1.persist().unwrap();
// 2. Create a new catalog and load the items by class // 2. Create a new catalog and load the items by class
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
@@ -1117,8 +1138,8 @@ mod tests {
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); assert_eq!(catalog2.items.len(), 2);
let loaded_item1: Item = catalog2.get("item-1").unwrap(); let loaded_item1: Item = catalog2.get("item-1").unwrap().unwrap();
let loaded_item2: Item = catalog2.get("item-2").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!(
@@ -1151,7 +1172,7 @@ mod tests {
price: 100, price: 100,
in_stock: true, in_stock: true,
}; };
catalog1.upsert(item_in_db.clone()); let _ = catalog1.upsert(item_in_db.clone());
catalog1.persist().unwrap(); catalog1.persist().unwrap();
// 2. Create a new catalog with a different in-memory version of the same item. // 2. Create a new catalog with a different in-memory version of the same item.
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
@@ -1161,10 +1182,10 @@ mod tests {
price: 200, price: 200,
in_stock: false, in_stock: false,
}; };
catalog2.upsert(item_in_memory); let _ = catalog2.upsert(item_in_memory);
assert_eq!(catalog2.items.len(), 1); assert_eq!(catalog2.items.len(), 1);
assert_eq!( assert_eq!(
catalog2.get::<Item>("item-1").unwrap().name, catalog2.get::<Item>("item-1").unwrap().unwrap().name,
"Memory Version" "Memory Version"
); );
// 3. Load from the database, which should overwrite the in-memory version. // 3. Load from the database, which should overwrite the in-memory version.
@@ -1172,7 +1193,7 @@ mod tests {
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); assert_eq!(catalog2.items.len(), 1);
let loaded_item: Item = catalog2.get("item-1").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.items.get("item-1").unwrap().state,
@@ -1205,13 +1226,13 @@ mod tests {
price: 100, price: 100,
in_stock: true, in_stock: true,
}; };
catalog.upsert(item_in_memory.clone()); let _ = catalog.upsert(item_in_memory.clone());
assert_eq!(catalog.items.len(), 1); assert_eq!(catalog.items.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); assert_eq!(catalog.items.len(), 1);
let retrieved_item: Item = catalog.get("item-1").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
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
@@ -1253,12 +1274,12 @@ mod tests {
price: 999, price: 999,
in_stock: true, in_stock: true,
}; };
catalog1.upsert(item_to_insert.clone()); let _ = catalog1.upsert(item_to_insert.clone());
catalog1.persist().unwrap(); catalog1.persist().unwrap();
// 2. create a new catalog instance -> 'load_by_id' -> 'get' // 2. create a new catalog instance -> 'load_by_id' -> 'get'
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
catalog2.load_by_id("item-1").unwrap(); catalog2.load_by_id("item-1").unwrap();
let loaded_item: Option<Item> = catalog2.get("item-1"); let loaded_item: Option<Item> = catalog2.get("item-1").unwrap();
// 3. verify data integrity // 3. verify data integrity
assert_eq!(loaded_item, Some(item_to_insert)); assert_eq!(loaded_item, Some(item_to_insert));
// Clean up // Clean up
@@ -1291,12 +1312,15 @@ mod tests {
in_stock: false, in_stock: false,
}, },
]; ];
catalog1.insert_many(items_to_insert.clone()); let _ = catalog1.insert_many(items_to_insert.clone());
catalog1.persist().unwrap(); catalog1.persist().unwrap();
// 2. new catalog -> 'load_by_class' -> 'list_by_class' // 2. new catalog -> 'load_by_class' -> 'list_by_class'
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
catalog2.load_by_class::<Item>().unwrap(); catalog2.load_by_class::<Item>().unwrap();
let mut loaded_items: Vec<Item> = catalog2.list_by_class::<Item>().collect(); let mut loaded_items: Vec<Item> = catalog2
.list_by_class::<Item>()
.map(|item| item.unwrap())
.collect();
// Sort by ID to ensure consistent order for comparison // Sort by ID to ensure consistent order for comparison
let mut expected_items = items_to_insert; let mut expected_items = items_to_insert;
loaded_items.sort_by(|a, b| a.id.cmp(&b.id)); loaded_items.sort_by(|a, b| a.id.cmp(&b.id));
@@ -1326,19 +1350,19 @@ mod tests {
price: 123, price: 123,
in_stock: true, in_stock: true,
}; };
catalog1.upsert(item_to_delete.clone()); let _ = catalog1.upsert(item_to_delete.clone());
catalog1.persist().unwrap(); catalog1.persist().unwrap();
// 2. 'load_by_id' to confirm it's there // 2. 'load_by_id' to confirm it's there
let mut catalog2 = Catalog::new(db_path); let mut catalog2 = Catalog::new(db_path);
catalog2.load_by_id("item-to-delete").unwrap(); catalog2.load_by_id("item-to-delete").unwrap();
assert!(catalog2.get::<Item>("item-to-delete").is_some()); assert!(catalog2.get::<Item>("item-to-delete").unwrap().is_some());
// 3. 'delete' -> 'persist' // 3. 'delete' -> 'persist'
catalog2.delete("item-to-delete"); catalog2.delete("item-to-delete");
catalog2.persist().unwrap(); catalog2.persist().unwrap();
// 4. 'load_by_id' should now return nothing // 4. 'load_by_id' should now return nothing
let mut catalog3 = Catalog::new(db_path); let mut catalog3 = Catalog::new(db_path);
catalog3.load_by_id("item-to-delete").unwrap(); catalog3.load_by_id("item-to-delete").unwrap();
let loaded_item: Option<Item> = catalog3.get("item-to-delete"); let loaded_item: Option<Item> = catalog3.get("item-to-delete").unwrap();
assert!(loaded_item.is_none()); assert!(loaded_item.is_none());
// Clean up // Clean up
std::fs::remove_file(path).unwrap(); std::fs::remove_file(path).unwrap();
@@ -1361,11 +1385,12 @@ mod tests {
price: 200, price: 200,
in_stock: false, in_stock: false,
}; };
catalog.upsert(item1.clone()); let _ = catalog.upsert(item1.clone());
catalog.upsert(item2.clone()); let _ = catalog.upsert(item2.clone());
// Retrieve by a unique attribute // Retrieve by a unique attribute
let retrieved_item: Option<Item> = let retrieved_item: Option<Item> = catalog
catalog.get_by_class_and_attribute("name", "Second Item"); .get_by_class_and_attribute("name", "Second Item")
.unwrap();
// Verify the correct item was retrieved // Verify the correct item was retrieved
assert_eq!(retrieved_item, Some(item2)); assert_eq!(retrieved_item, Some(item2));
} }
@@ -1393,10 +1418,11 @@ mod tests {
price: 150, price: 150,
in_stock: true, in_stock: true,
}; };
catalog.insert_many(vec![item1.clone(), item2.clone(), item3.clone()]); let _ = catalog.insert_many(vec![item1.clone(), item2.clone(), item3.clone()]);
// List all items that are in stock // List all items that are in stock
let mut results: Vec<Item> = catalog let mut results: Vec<Item> = catalog
.list_by_class_and_attribute("in_stock", true) .list_by_class_and_attribute("in_stock", true)
.map(|item| item.unwrap())
.collect(); .collect();
// Sort for deterministic comparison // Sort for deterministic comparison
results.sort_by(|a, b| a.id.cmp(&b.id)); results.sort_by(|a, b| a.id.cmp(&b.id));
@@ -1428,7 +1454,7 @@ mod tests {
price: 100, price: 100,
in_stock: true, in_stock: true,
}; };
catalog_setup.upsert(initial_item); let _ = catalog_setup.upsert(initial_item);
catalog_setup.persist().unwrap(); catalog_setup.persist().unwrap();
let db_path_arc = std::sync::Arc::new(String::from(db_path)); let db_path_arc = std::sync::Arc::new(String::from(db_path));
// 2. Thread 1: Loads, updates name, and persists. // 2. Thread 1: Loads, updates name, and persists.
@@ -1436,9 +1462,9 @@ mod tests {
let handle1 = std::thread::spawn(move || { let handle1 = std::thread::spawn(move || {
let mut catalog1 = Catalog::new(&db_path_arc1); let mut catalog1 = Catalog::new(&db_path_arc1);
catalog1.load_by_id("item-1").unwrap(); catalog1.load_by_id("item-1").unwrap();
let mut item = catalog1.get::<Item>("item-1").unwrap(); let mut item = catalog1.get::<Item>("item-1").unwrap().unwrap();
item.name = "Updated by Thread 1".to_string(); item.name = "Updated by Thread 1".to_string();
catalog1.upsert(item); let _ = catalog1.upsert(item);
catalog1.persist().unwrap(); catalog1.persist().unwrap();
}); });
// 3. Thread 2: Loads, updates price, and persists. // 3. Thread 2: Loads, updates price, and persists.
@@ -1446,9 +1472,9 @@ mod tests {
let handle2 = std::thread::spawn(move || { let handle2 = std::thread::spawn(move || {
let mut catalog2 = Catalog::new(&db_path_arc2); let mut catalog2 = Catalog::new(&db_path_arc2);
catalog2.load_by_id("item-1").unwrap(); catalog2.load_by_id("item-1").unwrap();
let mut item = catalog2.get::<Item>("item-1").unwrap(); let mut item = catalog2.get::<Item>("item-1").unwrap().unwrap();
item.price = 200; item.price = 200;
catalog2.upsert(item); let _ = catalog2.upsert(item);
catalog2.persist().unwrap(); catalog2.persist().unwrap();
}); });
handle1.join().unwrap(); handle1.join().unwrap();
@@ -1456,7 +1482,7 @@ mod tests {
// 4. Verification: Load the data and check the final state. // 4. Verification: Load the data and check the final state.
let mut catalog_verify = Catalog::new(db_path); let mut catalog_verify = Catalog::new(db_path);
catalog_verify.load_by_id("item-1").unwrap(); catalog_verify.load_by_id("item-1").unwrap();
let final_item: Item = catalog_verify.get("item-1").unwrap(); let final_item: Item = catalog_verify.get("item-1").unwrap().unwrap();
// The final state depends on which thread persisted last. One update will have been lost. // The final state depends on which thread persisted last. One update will have been lost.
let thread1_won = final_item.name == "Updated by Thread 1" && final_item.price == 100; let thread1_won = final_item.name == "Updated by Thread 1" && final_item.price == 100;
let thread2_won = final_item.name == "Original" && final_item.price == 200; let thread2_won = final_item.name == "Original" && final_item.price == 200;

View File

@@ -3,6 +3,10 @@ use crate::*;
/// Represents the possible failures that can occur in the library. /// Represents the possible failures that can occur in the library.
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Hash)] #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Hash)]
pub enum FailedTo { pub enum FailedTo {
/// Failed to convert from Entity to type.
ConvertEntity,
/// Failed to convert from type to Entity.
ConvertObject,
/// Failed to convert from Value to type. /// Failed to convert from Value to type.
ConvertValue, ConvertValue,
/// Failed to initialize the database. /// Failed to initialize the database.

View File

@@ -5,8 +5,8 @@ use crate::*;
/// This trait provides the necessary conversions to and from the generic `Entity` /// This trait provides the necessary conversions to and from the generic `Entity`
/// representation, and it requires the type to define its own class name. /// representation, and it requires the type to define its own class name.
pub trait T where pub trait T where
Self: From<Entity>, Self: TryFrom<Entity>,
Self: Into<Entity>, Self: TryInto<Entity>,
{ {
/// Returns the class name of the type. /// Returns the class name of the type.
/// ///