From d75de1e73c09c684d2e208d0e545ea464e7c9ffc Mon Sep 17 00:00:00 2001 From: despiegk Date: Tue, 22 Apr 2025 07:50:03 +0400 Subject: [PATCH] ... --- herodb/src/cmd/dbexample_mcc/main.rs | 56 ++-- herodb/src/db/db.rs | 298 ++++++++++------- herodb/src/db/generic_store.rs | 140 ++++++++ herodb/src/db/mod.rs | 6 +- herodb/src/db/model.rs | 50 ++- herodb/src/lib.rs | 2 +- herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md | 316 ------------------ herodb/src/models/mcc/calendar.rs | 33 +- herodb/src/models/mcc/contacts.rs | 59 +--- herodb/src/models/mcc/event.rs | 40 +-- herodb/src/models/mcc/lib.rs | 3 - herodb/src/models/mcc/mail.rs | 54 +-- herodb/src/models/mcc/message.rs | 36 +- herodb/src/models/mcc/mod.rs | 5 +- heromodels/Cargo.toml | 11 + heromodels/src/comment.rs | 63 ++++ heromodels/src/lib.rs | 47 +++ heromodels/src/model.rs | 181 ++++++++++ heromodels/src/user.rs | 86 +++++ mcc_models_standalone_plan.md | 161 +++++++++ 20 files changed, 1007 insertions(+), 640 deletions(-) create mode 100644 herodb/src/db/generic_store.rs delete mode 100644 herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md create mode 100644 heromodels/Cargo.toml create mode 100644 heromodels/src/comment.rs create mode 100644 heromodels/src/lib.rs create mode 100644 heromodels/src/model.rs create mode 100644 heromodels/src/user.rs create mode 100644 mcc_models_standalone_plan.md diff --git a/herodb/src/cmd/dbexample_mcc/main.rs b/herodb/src/cmd/dbexample_mcc/main.rs index 302a241..8801b2a 100644 --- a/herodb/src/cmd/dbexample_mcc/main.rs +++ b/herodb/src/cmd/dbexample_mcc/main.rs @@ -1,5 +1,5 @@ use chrono::{Utc, Duration}; -use herodb::db::DBBuilder; +use herodb::db::{DBBuilder, GetId}; use herodb::models::mcc::{ Calendar, Event, Email, Attachment, Envelope, @@ -23,12 +23,12 @@ fn main() -> Result<(), Box> { // Create a database instance with our models registered let db = DBBuilder::new(&db_path) - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .register_model::() + .register_type::("calendar") + .register_type::("event") + .register_type::("email") + .register_type::("contact") + .register_type::("message") + .register_model::() // Circle still uses the Model trait .build()?; println!("\n1. Creating Circles (Groups)"); @@ -48,8 +48,8 @@ fn main() -> Result<(), Box> { ); let friends_circle = Circle::new( - 3, - "Friends".to_string(), + 3, + "Friends".to_string(), "Friends communications".to_string() ); @@ -96,9 +96,9 @@ fn main() -> Result<(), Box> { bob.add_group(work_circle.id); // Bob is both a friend and a work contact // Insert contacts - db.set::(&john)?; - db.set::(&alice)?; - db.set::(&bob)?; + db.set_any::(&john, "contact")?; + db.set_any::(&alice, "contact")?; + db.set_any::(&bob, "contact")?; println!("Created contacts:"); println!(" - {}: {} (Groups: {:?})", john.full_name(), john.email, john.groups); @@ -125,8 +125,8 @@ fn main() -> Result<(), Box> { personal_calendar.add_group(friends_circle.id); // Insert calendars - db.set::(&work_calendar)?; - db.set::(&personal_calendar)?; + db.set_any::(&work_calendar, "calendar")?; + db.set_any::(&personal_calendar, "calendar")?; println!("Created calendars:"); println!(" - {}: {} (Groups: {:?})", work_calendar.id, work_calendar.title, work_calendar.groups); @@ -168,8 +168,8 @@ fn main() -> Result<(), Box> { family_dinner.add_attendee(alice.email.clone()); // Insert events - db.set::(&work_meeting)?; - db.set::(&family_dinner)?; + db.set_any::(&work_meeting, "event")?; + db.set_any::(&family_dinner, "event")?; println!("Created events:"); println!(" - {}: {} on {} (Groups: {:?})", @@ -244,8 +244,8 @@ fn main() -> Result<(), Box> { family_email.set_envelope(family_envelope); // Insert emails - db.set::(&work_email)?; - db.set::(&family_email)?; + db.set_any::(&work_email, "email")?; + db.set_any::(&family_email, "email")?; println!("Created emails:"); println!(" - From: {}, Subject: {} (Groups: {:?})", @@ -284,8 +284,8 @@ fn main() -> Result<(), Box> { friends_chat.add_reaction("👍".to_string()); // Insert messages - db.set::(&work_chat)?; - db.set::(&friends_chat)?; + db.set_any::(&work_chat, "message")?; + db.set_any::(&friends_chat, "message")?; println!("Created messages:"); println!(" - From: {}, Content: {} (Groups: {:?})", @@ -305,7 +305,7 @@ fn main() -> Result<(), Box> { // Filter contacts by group println!("\nFiltering contacts by work group (ID: {}):", work_circle.id); - let all_contacts = db.list::()?; + let all_contacts = db.list_any::()?; for contact in all_contacts { if contact.filter_by_groups(&[work_circle.id]) { println!(" - {} ({})", contact.full_name(), contact.email); @@ -314,7 +314,7 @@ fn main() -> Result<(), Box> { // Search emails by subject println!("\nSearching emails with subject containing 'Meeting':"); - let all_emails = db.list::()?; + let all_emails = db.list_any::()?; for email in all_emails { if email.search_by_subject("Meeting") { println!(" - Subject: {}, From: {}", @@ -326,7 +326,7 @@ fn main() -> Result<(), Box> { // Get events for a calendar println!("\nGetting events for Work Calendar (ID: {}):", work_calendar.id); - let all_events = db.list::()?; + let all_events = db.list_any::()?; let work_events: Vec = all_events .into_iter() .filter(|event| event.calendar_id == work_calendar.id) @@ -341,7 +341,7 @@ fn main() -> Result<(), Box> { // Get attendee contacts for an event println!("\nGetting attendee contacts for Team Meeting (ID: {}):", work_meeting.id); - let all_contacts = db.list::()?; + let all_contacts = db.list_any::()?; let attendee_contacts: Vec = all_contacts .into_iter() .filter(|contact| work_meeting.attendees.contains(&contact.email)) @@ -358,19 +358,19 @@ fn main() -> Result<(), Box> { println!(" - Converted Message Groups: {:?}", email_to_message.groups); // Insert the converted message - db.set::(&email_to_message)?; + db.set_any::(&email_to_message, "message")?; println!("\n8. Relationship Management"); println!("------------------------"); // Get the calendar for an event println!("\nGetting calendar for Family Dinner event (ID: {}):", family_dinner.id); - let event_calendar = db.get::(family_dinner.calendar_id)?; + let event_calendar = db.get_any::(family_dinner.calendar_id)?; println!(" - Calendar: {} ({})", event_calendar.title, event_calendar.description); // Get events for a contact println!("\nGetting events where John Doe is an attendee:"); - let all_events = db.list::()?; + let all_events = db.list_any::()?; let john_events: Vec = all_events .into_iter() .filter(|event| event.attendees.contains(&john.email)) @@ -385,7 +385,7 @@ fn main() -> Result<(), Box> { // Get messages in the same thread println!("\nGetting all messages in the work chat thread:"); - let all_messages = db.list::()?; + let all_messages = db.list_any::()?; let thread_messages: Vec = all_messages .into_iter() .filter(|message| message.thread_id == work_chat.thread_id) diff --git a/herodb/src/db/db.rs b/herodb/src/db/db.rs index 864a4fa..c7247d6 100644 --- a/herodb/src/db/db.rs +++ b/herodb/src/db/db.rs @@ -1,6 +1,7 @@ use crate::db::error::{DbError, DbResult}; -use crate::db::model::Model; +use crate::db::model::{Model, IndexKey}; use crate::db::store::{DbOperations, OurDbStore}; +use crate::db::generic_store::{GenericStore, GetId}; use crate::db::tst_index::TSTIndexManager; use std::any::TypeId; use std::collections::HashMap; @@ -8,6 +9,7 @@ use std::fmt::Debug; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use rhai::{CustomType, EvalAltResult, TypeBuilder}; +use serde::{Serialize, de::DeserializeOwned}; /// Represents a single database operation in a transaction #[derive(Debug, Clone)] @@ -82,6 +84,21 @@ impl ModelRegistrar { } } +/// Implementation of ModelRegistration for any serializable type that implements GetId +pub struct TypeRegistrar { + prefix: &'static str, + phantom: std::marker::PhantomData, +} + +impl TypeRegistrar { + pub fn new(prefix: &'static str) -> Self { + Self { + prefix, + phantom: std::marker::PhantomData, + } + } +} + impl ModelRegistration for ModelRegistrar { fn register(&self, path: &Path) -> DbResult<(TypeId, Arc>)> { let store = OurDbStore::::open(path.join(T::db_prefix()))?; @@ -89,6 +106,13 @@ impl ModelRegistration for ModelRegistrar { } } +impl ModelRegistration for TypeRegistrar { + fn register(&self, path: &Path) -> DbResult<(TypeId, Arc>)> { + let store = GenericStore::::open(path, self.prefix)?; + Ok((TypeId::of::(), Arc::new(RwLock::new(store)) as Arc>)) + } +} + impl DBBuilder { /// Create a new DB builder pub fn new>(base_path: P) -> Self { @@ -112,6 +136,16 @@ impl DBBuilder { self } + /// Register any serializable type with the DB + pub fn register_type( + mut self, + prefix: &'static str + ) -> Self { + self.model_registrations + .push(Arc::new(TypeRegistrar::::new(prefix))); + self + } + /// Build the DB with the registered models pub fn build(self) -> Result> { let base_path = self.base_path; @@ -342,6 +376,57 @@ impl DB { } } + /// Insert any serializable struct that implements GetId + pub fn set_any( + &self, + item: &T, + prefix: &str + ) -> DbResult<()> { + // Try to acquire a write lock on the transaction + let mut tx_guard = self.transaction.write().unwrap(); + + // Check if there's an active transaction + if let Some(tx_state) = tx_guard.as_mut() { + if tx_state.active { + // Serialize the item for later use + let serialized = bincode::serialize(item).map_err(DbError::SerializationError)?; + + // Record a Set operation in the transaction with prefix and ID + tx_state.operations.push(DbOperation::Set { + model_type: TypeId::of::(), + serialized, + model_prefix: prefix.to_string(), + model_id: item.get_id(), + }); + + return Ok(()); + } + } + + // If we got here, either there's no transaction or it's not active + // Drop the write lock before doing a direct database operation + drop(tx_guard); + + // Execute directly + match self.type_map.get(&TypeId::of::()) { + Some(db_ops) => { + // Serialize the item + let data = bincode::serialize(item).map_err(DbError::SerializationError)?; + + // Insert the raw data + let mut db_ops_guard = db_ops.write().unwrap(); + db_ops_guard.insert_raw(&data)?; + + // Also update the TST index (primary key only) + let mut tst_index = self.tst_index.write().unwrap(); + tst_index.set(prefix, item.get_id(), data)?; + + Ok(()) + }, + None => Err(DbError::TypeError), + } + } + /// Check the transaction state for the given type and id fn check_transaction(&self, id: u32) -> Option, DbError>> { // Try to acquire a read lock on the transaction @@ -399,19 +484,41 @@ impl DB { if let Some(tx_result) = self.check_transaction::(id) { match tx_result { Ok(Some(model)) => return Ok(model), + Ok(None) => return Err(DbError::NotFound(id)), Err(e) => return Err(e), - Ok(None) => {} // Should never happen } } - // If no pending value, look up from the database + // If not found in transaction, get from database match self.type_map.get(&TypeId::of::()) { Some(db_ops) => { let mut db_ops_guard = db_ops.write().unwrap(); - let result_any = db_ops_guard.get(id)?; - // We expect the result to be of type T since we looked it up by TypeId - match result_any.downcast::() { - Ok(t) => Ok(*t), + let any_result = db_ops_guard.get(id)?; + + // Try to downcast to T + match any_result.downcast::() { + Ok(boxed_t) => Ok(*boxed_t), + Err(_) => Err(DbError::TypeError), + } + } + None => Err(DbError::TypeError), + } + } + + /// Get any serializable struct by its ID and type + pub fn get_any( + &self, + id: u32 + ) -> DbResult { + // If not found in transaction, get from database + match self.type_map.get(&TypeId::of::()) { + Some(db_ops) => { + let mut db_ops_guard = db_ops.write().unwrap(); + let any_result = db_ops_guard.get(id)?; + + // Try to downcast to T + match any_result.downcast::() { + Ok(boxed_t) => Ok(*boxed_t), Err(_) => Err(DbError::TypeError), } } @@ -421,17 +528,13 @@ impl DB { /// Delete a model instance by its ID and type pub fn delete(&self, id: u32) -> DbResult<()> { - // First, get the model to extract its index keys - let model = self.get::(id)?; - let index_keys = model.db_keys(); - // Try to acquire a write lock on the transaction let mut tx_guard = self.transaction.write().unwrap(); // Check if there's an active transaction if let Some(tx_state) = tx_guard.as_mut() { if tx_state.active { - // Record a Delete operation in the transaction with prefix + // Record a Delete operation in the transaction tx_state.operations.push(DbOperation::Delete { model_type: TypeId::of::(), id, @@ -452,45 +555,48 @@ impl DB { let mut db_ops_guard = db_ops.write().unwrap(); db_ops_guard.delete(id)?; - // Also update the TST index with all index keys + // Also delete from the TST index let mut tst_index = self.tst_index.write().unwrap(); - let prefix = T::db_prefix(); - tst_index.delete_with_indexes(prefix, id, &index_keys)?; + tst_index.delete(T::db_prefix(), id)?; Ok(()) - }, + } None => Err(DbError::TypeError), } } - /// List all model instances of a specific type - pub fn list(&self) -> DbResult> { - // Get the prefix for this model type - let prefix = T::db_prefix(); - - // Use the TST index to get all objects with this prefix - let mut tst_index = self.tst_index.write().unwrap(); - let items = tst_index.list(prefix)?; - - // Deserialize the objects - let mut result = Vec::with_capacity(items.len()); - for (_, data) in items { - let model = T::from_bytes(&data)?; - result.push(model); + /// Delete any serializable struct by its ID and type + pub fn delete_any( + &self, + id: u32, + prefix: &str + ) -> DbResult<()> { + // Execute directly + match self.type_map.get(&TypeId::of::()) { + Some(db_ops) => { + let mut db_ops_guard = db_ops.write().unwrap(); + db_ops_guard.delete(id)?; + + // Also delete from the TST index + let mut tst_index = self.tst_index.write().unwrap(); + tst_index.delete(prefix, id)?; + + Ok(()) + } + None => Err(DbError::TypeError), } - - Ok(result) } - /// Helper method to list models directly from OurDB (not using TST) - fn list_from_ourdb(&self) -> DbResult> { + /// List all model instances of a given type + pub fn list(&self) -> DbResult> { match self.type_map.get(&TypeId::of::()) { Some(db_ops) => { let db_ops_guard = db_ops.read().unwrap(); - let result_any = db_ops_guard.list()?; - // We expect the result to be of type Vec since we looked it up by TypeId - match result_any.downcast::>() { - Ok(vec_t) => Ok(*vec_t), + let any_result = db_ops_guard.list()?; + + // Try to downcast to Vec + match any_result.downcast::>() { + Ok(boxed_vec) => Ok(*boxed_vec), Err(_) => Err(DbError::TypeError), } } @@ -498,105 +604,43 @@ impl DB { } } - /// Synchronize the TST index with OurDB for a specific model type - pub fn synchronize_tst_index(&self) -> DbResult<()> { - // Get all models from OurDB - let models = self.list_from_ourdb::()?; - - // Clear the TST index for this model type - let mut tst_index = self.tst_index.write().unwrap(); - let prefix = T::db_prefix(); - - // Rebuild the TST index with all index keys - for model in models { - let id = model.get_id(); - let data = model.to_bytes()?; - let index_keys = model.db_keys(); - tst_index.set_with_indexes(prefix, id, data, &index_keys)?; - } - - Ok(()) - } - - /// Get the history of a model by its ID - pub fn get_history(&self, id: u32, depth: u8) -> DbResult> { - // Look up the correct DB operations for type T in our type map + /// List all instances of any serializable type + pub fn list_any( + &self + ) -> DbResult> { match self.type_map.get(&TypeId::of::()) { Some(db_ops) => { - let mut db_ops_guard = db_ops.write().unwrap(); - let result_any = db_ops_guard.get_history(id, depth)?; - let mut result = Vec::with_capacity(result_any.len()); + let db_ops_guard = db_ops.read().unwrap(); + let any_result = db_ops_guard.list()?; - for item in result_any { - match item.downcast::() { - Ok(t) => result.push(*t), - Err(_) => return Err(DbError::TypeError), - } + // Try to downcast to Vec + match any_result.downcast::>() { + Ok(boxed_vec) => Ok(*boxed_vec), + Err(_) => Err(DbError::TypeError), } - - Ok(result) } None => Err(DbError::TypeError), } } - // Register a model type with this DB instance - pub fn register(&mut self) -> DbResult<()> { - // Create the OurDB store - let store = OurDbStore::::open(&self.db_path)?; - self.type_map.insert(TypeId::of::(), Arc::new(RwLock::new(store))); - - // Initialize the TST index for this model type - let prefix = T::db_prefix(); - let mut tst_index = self.tst_index.write().unwrap(); - - // Ensure the TST for this prefix exists - tst_index.get_tst(prefix)?; - - Ok(()) - } - - /// Find a model by a specific index key - pub fn find_by_index(&self, index_name: &str, index_value: &str) -> DbResult> { - // Get the prefix for this model type - let prefix = T::db_prefix(); - - // Use the TST index to find objects with this index key - let mut tst_index = self.tst_index.write().unwrap(); - let ids = tst_index.find_by_index(prefix, index_name, index_value)?; - - // Get the objects by their IDs - let mut result = Vec::with_capacity(ids.len()); - for id in ids { - match self.get::(id) { - Ok(model) => result.push(model), - Err(DbError::NotFound(_)) => continue, // Skip if not found - Err(e) => return Err(e), + /// Get the history of a model instance + pub fn get_history(&self, id: u32, depth: u8) -> DbResult> { + match self.type_map.get(&TypeId::of::()) { + Some(db_ops) => { + let mut db_ops_guard = db_ops.write().unwrap(); + let any_results = db_ops_guard.get_history(id, depth)?; + + let mut results = Vec::with_capacity(any_results.len()); + for any_result in any_results { + match any_result.downcast::() { + Ok(boxed_t) => results.push(*boxed_t), + Err(_) => return Err(DbError::TypeError), + } + } + + Ok(results) } + None => Err(DbError::TypeError), } - - Ok(result) - } - - /// Find models by a prefix of an index key - pub fn find_by_index_prefix(&self, index_name: &str, index_value_prefix: &str) -> DbResult> { - // Get the prefix for this model type - let prefix = T::db_prefix(); - - // Use the TST index to find objects with this index key prefix - let mut tst_index = self.tst_index.write().unwrap(); - let ids = tst_index.find_by_index_prefix(prefix, index_name, index_value_prefix)?; - - // Get the objects by their IDs - let mut result = Vec::with_capacity(ids.len()); - for id in ids { - match self.get::(id) { - Ok(model) => result.push(model), - Err(DbError::NotFound(_)) => continue, // Skip if not found - Err(e) => return Err(e), - } - } - - Ok(result) } } diff --git a/herodb/src/db/generic_store.rs b/herodb/src/db/generic_store.rs new file mode 100644 index 0000000..b7bf993 --- /dev/null +++ b/herodb/src/db/generic_store.rs @@ -0,0 +1,140 @@ +use crate::db::error::{DbError, DbResult}; +use crate::db::store::DbOperations; +use ourdb::{OurDB, OurDBConfig, OurDBSetArgs}; +use serde::{Serialize, de::DeserializeOwned}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::any::Any; + +// Trait for getting ID from any serializable type +pub trait GetId { + fn get_id(&self) -> u32; +} + +/// A store implementation for any serializable type using OurDB as the backend +pub struct GenericStore { + db: OurDB, + path: PathBuf, + prefix: String, + _phantom: PhantomData, +} + +impl GenericStore { + /// Opens or creates an OurDB database at the specified path + pub fn open>(path: P, prefix: &str) -> DbResult { + let path_buf = path.as_ref().to_path_buf(); + let db_path = path_buf.join(prefix); + + // Create directory if it doesn't exist + std::fs::create_dir_all(&db_path).map_err(DbError::IoError)?; + + let config = OurDBConfig { + path: db_path.clone(), + incremental_mode: true, // Always use incremental mode for auto IDs + file_size: None, // Use default (500MB) + keysize: None, // Use default (4 bytes) + reset: None, // Don't reset existing database + }; + + let db = OurDB::new(config).map_err(DbError::OurDbError)?; + + Ok(Self { + db, + path: db_path, + prefix: prefix.to_string(), + _phantom: PhantomData, + }) + } + + /// Serializes an item to bytes + fn serialize(item: &T) -> DbResult> { + bincode::serialize(item).map_err(DbError::SerializationError) + } + + /// Deserializes bytes to an item + fn deserialize(data: &[u8]) -> DbResult { + bincode::deserialize(data).map_err(DbError::SerializationError) + } + + /// Gets the raw bytes for an item by ID + pub fn get_raw(&self, id: u32) -> DbResult> { + self.db.get(id).map_err(DbError::OurDbError) + } + + /// Lists all raw items as bytes + pub fn list_raw(&self) -> DbResult>> { + let items = self.db.list().map_err(DbError::OurDbError)?; + Ok(items) + } + + /// Get the prefix for this store + pub fn prefix(&self) -> &str { + &self.prefix + } +} + +impl DbOperations for GenericStore { + fn delete(&mut self, id: u32) -> DbResult<()> { + self.db.delete(id).map_err(DbError::OurDbError) + } + + fn get(&mut self, id: u32) -> DbResult> { + let data = self.db.get(id).map_err(DbError::OurDbError)?; + let item = Self::deserialize(&data)?; + Ok(Box::new(item)) + } + + fn list(&self) -> DbResult> { + let items = self.db.list().map_err(DbError::OurDbError)?; + let mut result = Vec::with_capacity(items.len()); + + for data in items { + let item = Self::deserialize(&data)?; + result.push(item); + } + + Ok(Box::new(result)) + } + + fn insert(&mut self, model: &dyn Any) -> DbResult<()> { + // Try to downcast to T + if let Some(item) = model.downcast_ref::() { + let data = Self::serialize(item)?; + let id = item.get_id(); + + let args = OurDBSetArgs { + id: Some(id), + data, + }; + + self.db.set(args).map_err(DbError::OurDbError) + } else { + Err(DbError::TypeError) + } + } + + fn insert_raw(&mut self, serialized: &[u8]) -> DbResult<()> { + // Deserialize to get the ID + let item = Self::deserialize(serialized)?; + let id = item.get_id(); + + let args = OurDBSetArgs { + id: Some(id), + data: serialized.to_vec(), + }; + + self.db.set(args).map_err(DbError::OurDbError) + } + + fn get_history(&mut self, id: u32, depth: u8) -> DbResult>> { + let history = self.db.get_history(id, depth).map_err(DbError::OurDbError)?; + let mut result = Vec::with_capacity(history.len()); + + for data in history { + let item = Self::deserialize(&data)?; + result.push(Box::new(item)); + } + + Ok(result) + } +} \ No newline at end of file diff --git a/herodb/src/db/mod.rs b/herodb/src/db/mod.rs index b2744bf..351c39d 100644 --- a/herodb/src/db/mod.rs +++ b/herodb/src/db/mod.rs @@ -4,12 +4,16 @@ pub use error::{DbError, DbResult}; // Export the model module pub mod model; -pub use model::{Model, Storable, IndexKey}; +pub use model::{Model, Storable, IndexKey, GetId}; // Export the store module pub mod store; pub use store::{DbOperations, OurDbStore}; +// Export the generic store module +pub mod generic_store; +pub use generic_store::GenericStore; + // Export the db module pub mod db; pub use db::{DB, DBBuilder, ModelRegistration, ModelRegistrar}; diff --git a/herodb/src/db/model.rs b/herodb/src/db/model.rs index 8e9c97d..bad628b 100644 --- a/herodb/src/db/model.rs +++ b/herodb/src/db/model.rs @@ -1,5 +1,5 @@ use crate::db::error::{DbError, DbResult}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use std::fmt::Debug; /// Trait for models that can be serialized and deserialized @@ -44,5 +44,53 @@ pub trait Model: Storable + Debug + Clone + Send + Sync + 'static { } } +/// Trait for adapting any serializable struct to work with the database +/// This is a lighter-weight alternative to the Model trait +pub trait ModelAdapter { + /// Returns the unique ID for this model instance + fn get_id(&self) -> u32; + + /// Returns a prefix used for this model type in the database + fn db_prefix() -> &'static str; + + /// Returns a list of index keys for this model instance + fn db_keys(&self) -> Vec { + Vec::new() + } +} + +/// Trait for getting ID from any serializable type +pub trait GetId { + /// Returns the unique ID for this instance + fn get_id(&self) -> u32; +} + +/// Macro to automatically implement GetId for any struct with an id field of type u32 +#[macro_export] +macro_rules! impl_get_id { + ($type:ty) => { + impl GetId for $type { + fn get_id(&self) -> u32 { + self.id + } + } + }; +} + +/// Helper functions for serializing and deserializing any type +pub mod serialization { + use super::*; + + /// Serialize any serializable type to bytes + pub fn to_bytes(value: &T) -> DbResult> { + bincode::serialize(value).map_err(DbError::SerializationError) + } + + /// Deserialize bytes to any deserializable type + pub fn from_bytes(data: &[u8]) -> DbResult { + bincode::deserialize(data).map_err(DbError::SerializationError) + } +} + // Note: We don't provide a blanket implementation of Storable // Each model type must implement Storable explicitly \ No newline at end of file diff --git a/herodb/src/lib.rs b/herodb/src/lib.rs index a512938..32aeba6 100644 --- a/herodb/src/lib.rs +++ b/herodb/src/lib.rs @@ -13,7 +13,7 @@ pub mod cmd; // Re-exports pub use error::Error; -pub use db::{DB, DBBuilder, Model, Storable, DbError, DbResult}; +pub use db::{DB, DBBuilder, Model, Storable, DbError, DbResult, GetId}; /// Re-export ourdb for advanced usage pub use ourdb; diff --git a/herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md b/herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md deleted file mode 100644 index e3ca985..0000000 --- a/herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md +++ /dev/null @@ -1,316 +0,0 @@ -# MCC Models Enhancement Plan - -## 1. Current State Analysis - -The current MCC module consists of: -- **Mail**: Email, Attachment, Envelope models -- **Calendar**: Calendar model -- **Event**: Event, EventMeta models -- **Contacts**: Contact model - -All models implement the `Storable` and `SledModel` traits for database integration. - -## 2. Planned Enhancements - -### 2.1 Add Group Support to All Models - -Add a `groups: Vec` field to each model to enable linking to multiple groups defined in the Circle module. - -### 2.2 Create New Message Model - -Create a new `message.rs` file with a Message model for chat functionality: -- Different structure from Email -- Include thread_id, sender_id, content fields -- Include metadata for chat-specific features -- Implement Storable and SledModel traits - -### 2.3 Add Utility Methods - -Add utility methods to each model for: -- **Filtering/Searching**: Methods to filter by groups, search by content/subject -- **Format Conversion**: Methods to convert between formats (e.g., Email to Message) -- **Relationship Management**: Methods to manage relationships between models - -## 3. Implementation Plan - -```mermaid -flowchart TD - A[Review Current Models] --> B[Add groups field to all models] - B --> C[Create Message model] - C --> D[Add utility methods] - D --> E[Update mod.rs and lib.rs] - E --> F[Update README.md] -``` - -### 3.1 Detailed Changes - -#### 3.1.1 Mail Model (`mail.rs`) - -- Add `groups: Vec` field to `Email` struct -- Add utility methods: - - `filter_by_groups(groups: &[u32]) -> bool` - - `search_by_subject(query: &str) -> bool` - - `search_by_content(query: &str) -> bool` - - `to_message(&self) -> Message` (conversion method) - -#### 3.1.2 Calendar Model (`calendar.rs`) - -- Add `groups: Vec` field to `Calendar` struct -- Add utility methods: - - `filter_by_groups(groups: &[u32]) -> bool` - - `get_events(&self, db: &SledDB) -> SledDBResult>` (relationship method) - -#### 3.1.3 Event Model (`event.rs`) - -- Add `groups: Vec` field to `Event` struct -- Add utility methods: - - `filter_by_groups(groups: &[u32]) -> bool` - - `get_calendar(&self, db: &SledDB) -> SledDBResult` (relationship method) - - `get_attendee_contacts(&self, db: &SledDB) -> SledDBResult>` (relationship method) - -#### 3.1.4 Contacts Model (`contacts.rs`) - -- Add `groups: Vec` field to `Contact` struct -- Add utility methods: - - `filter_by_groups(groups: &[u32]) -> bool` - - `search_by_name(query: &str) -> bool` - - `search_by_email(query: &str) -> bool` - - `get_events(&self, db: &SledDB) -> SledDBResult>` (relationship method) - -#### 3.1.5 New Message Model (`message.rs`) - -```rust -use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; -use chrono::{DateTime, Utc}; - -/// MessageStatus represents the status of a message -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum MessageStatus { - Sent, - Delivered, - Read, - Failed, -} - -/// MessageMeta contains metadata for a chat message -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MessageMeta { - pub created_at: DateTime, - pub updated_at: DateTime, - pub status: MessageStatus, - pub is_edited: bool, - pub reactions: Vec, -} - -/// Message represents a chat message -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Message { - pub id: u32, // Unique identifier - pub thread_id: String, // Thread/conversation identifier - pub sender_id: String, // Sender identifier - pub recipients: Vec, // List of recipient identifiers - pub content: String, // Message content - pub attachments: Vec, // References to attachments - pub groups: Vec, // Groups this message belongs to - pub meta: MessageMeta, // Message metadata -} - -impl Message { - /// Create a new message - pub fn new(id: u32, thread_id: String, sender_id: String, content: String) -> Self { - let now = Utc::now(); - Self { - id, - thread_id, - sender_id, - recipients: Vec::new(), - content, - attachments: Vec::new(), - groups: Vec::new(), - meta: MessageMeta { - created_at: now, - updated_at: now, - status: MessageStatus::Sent, - is_edited: false, - reactions: Vec::new(), - }, - } - } - - /// Add a recipient to this message - pub fn add_recipient(&mut self, recipient: String) { - self.recipients.push(recipient); - } - - /// Add an attachment to this message - pub fn add_attachment(&mut self, attachment: String) { - self.attachments.push(attachment); - } - - /// Add a group to this message - pub fn add_group(&mut self, group_id: u32) { - if !self.groups.contains(&group_id) { - self.groups.push(group_id); - } - } - - /// Filter by groups - pub fn filter_by_groups(&self, groups: &[u32]) -> bool { - groups.iter().any(|g| self.groups.contains(g)) - } - - /// Search by content - pub fn search_by_content(&self, query: &str) -> bool { - self.content.to_lowercase().contains(&query.to_lowercase()) - } - - /// Update message status - pub fn update_status(&mut self, status: MessageStatus) { - self.meta.status = status; - self.meta.updated_at = Utc::now(); - } - - /// Edit message content - pub fn edit_content(&mut self, new_content: String) { - self.content = new_content; - self.meta.is_edited = true; - self.meta.updated_at = Utc::now(); - } - - /// Add a reaction to the message - pub fn add_reaction(&mut self, reaction: String) { - self.meta.reactions.push(reaction); - self.meta.updated_at = Utc::now(); - } -} - -// Implement Storable trait (provides default dump/load) -impl Storable for Message {} - -// Implement SledModel trait -impl SledModel for Message { - fn get_id(&self) -> String { - self.id.to_string() - } - - fn db_prefix() -> &'static str { - "message" - } -} -``` - -#### 3.1.6 Update Module Files - -Update `mod.rs` and `lib.rs` to include the new Message model. - -#### 3.1.7 Update README.md - -Update the README.md to include information about the Message model and the new utility methods. - -## 4. Data Model Diagram - -```mermaid -classDiagram - class Email { - +u32 id - +u32 uid - +u32 seq_num - +String mailbox - +String message - +Vec~Attachment~ attachments - +Vec~String~ flags - +i64 receivetime - +Option~Envelope~ envelope - +Vec~u32~ groups - +filter_by_groups() - +search_by_subject() - +search_by_content() - +to_message() - } - - class Calendar { - +u32 id - +String title - +String description - +Vec~u32~ groups - +filter_by_groups() - +get_events() - } - - class Event { - +u32 id - +u32 calendar_id - +String title - +String description - +String location - +DateTime start_time - +DateTime end_time - +bool all_day - +String recurrence - +Vec~String~ attendees - +String organizer - +String status - +EventMeta meta - +Vec~u32~ groups - +filter_by_groups() - +get_calendar() - +get_attendee_contacts() - } - - class Contact { - +u32 id - +i64 created_at - +i64 modified_at - +String first_name - +String last_name - +String email - +String group - +Vec~u32~ groups - +filter_by_groups() - +search_by_name() - +search_by_email() - +get_events() - } - - class Message { - +u32 id - +String thread_id - +String sender_id - +Vec~String~ recipients - +String content - +Vec~String~ attachments - +Vec~u32~ groups - +MessageMeta meta - +filter_by_groups() - +search_by_content() - +update_status() - +edit_content() - +add_reaction() - } - - class Circle { - +u32 id - +String name - +String description - +Vec~Member~ members - } - - Calendar "1" -- "many" Event: contains - Contact "many" -- "many" Event: attends - Circle "1" -- "many" Email: groups - Circle "1" -- "many" Calendar: groups - Circle "1" -- "many" Event: groups - Circle "1" -- "many" Contact: groups - Circle "1" -- "many" Message: groups -``` - -## 5. Testing Strategy - -1. Unit tests for each model to verify: - - Group field functionality - - New utility methods - - Serialization/deserialization with the new fields -2. Integration tests to verify: - - Database operations with the updated models - - Relationships between models \ No newline at end of file diff --git a/herodb/src/models/mcc/calendar.rs b/herodb/src/models/mcc/calendar.rs index 0d664fc..80bd90e 100644 --- a/herodb/src/models/mcc/calendar.rs +++ b/herodb/src/models/mcc/calendar.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::db::{Model, Storable, DB, DbError, DbResult}; use crate::models::mcc::event::Event; +use crate::db::model::impl_get_id; /// Calendar represents a calendar container for events #[derive(Debug, Clone, Serialize, Deserialize)] @@ -39,27 +39,18 @@ impl Calendar { groups.iter().any(|g| self.groups.contains(g)) } - /// Get all events associated with this calendar - pub fn get_events(&self, db: &DB) -> DbResult> { - let all_events = db.list::()?; - let calendar_events = all_events - .into_iter() + /// Filter events by this calendar's ID + pub fn filter_events<'a>(&self, events: &'a [Event]) -> Vec<&'a Event> { + events.iter() .filter(|event| event.calendar_id == self.id) - .collect(); - - Ok(calendar_events) + .collect() + } + + /// Get the database prefix for this model type + pub fn db_prefix() -> &'static str { + "calendar" } } -impl Storable for Calendar{} - -// Implement Model trait -impl Model for Calendar { - fn get_id(&self) -> u32 { - self.id - } - - fn db_prefix() -> &'static str { - "calendar" - } -} \ No newline at end of file +// Automatically implement GetId trait for Calendar +impl_get_id!(Calendar); \ No newline at end of file diff --git a/herodb/src/models/mcc/contacts.rs b/herodb/src/models/mcc/contacts.rs index 3e279a0..b81ff04 100644 --- a/herodb/src/models/mcc/contacts.rs +++ b/herodb/src/models/mcc/contacts.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::db::{Model, Storable, DB, DbError, DbResult}; use crate::models::mcc::event::Event; +use crate::db::model::impl_get_id; use chrono::Utc; /// Contact represents a contact entry in an address book @@ -13,14 +13,12 @@ pub struct Contact { pub modified_at: i64, // Unix epoch timestamp pub first_name: String, pub last_name: String, - pub email: String, - pub group: String, // Reference to a dns name, each group has a globally unique dns - pub groups: Vec, // Groups this contact belongs to (references Circle IDs) + pub emails: Vec, // Changed from []String to Vec } impl Contact { /// Create a new contact - pub fn new(id: u32, first_name: String, last_name: String, email: String, group: String) -> Self { + pub fn new(id: u32, first_name: String, last_name: String, emails: Vec) -> Self { let now = Utc::now().timestamp(); Self { id, @@ -28,29 +26,10 @@ impl Contact { modified_at: now, first_name, last_name, - email, - group, - groups: Vec::new(), + emails : emails, } } - /// Add a group to this contact - pub fn add_group(&mut self, group_id: u32) { - if !self.groups.contains(&group_id) { - self.groups.push(group_id); - } - } - - /// Remove a group from this contact - pub fn remove_group(&mut self, group_id: u32) { - self.groups.retain(|&id| id != group_id); - } - - /// Filter by groups - returns true if this contact belongs to any of the specified groups - pub fn filter_by_groups(&self, groups: &[u32]) -> bool { - groups.iter().any(|g| self.groups.contains(g)) - } - /// Search by name - returns true if the name contains the query (case-insensitive) pub fn search_by_name(&self, query: &str) -> bool { let full_name = self.full_name().to_lowercase(); @@ -62,15 +41,11 @@ impl Contact { self.email.to_lowercase().contains(&query.to_lowercase()) } - /// Get events where this contact is an attendee - pub fn get_events(&self, db: &DB) -> DbResult> { - let all_events = db.list::()?; - let contact_events = all_events - .into_iter() + /// Filter events where this contact is an attendee + pub fn filter_events<'a>(&self, events: &'a [Event]) -> Vec<&'a Event> { + events.iter() .filter(|event| event.attendees.contains(&self.email)) - .collect(); - - Ok(contact_events) + .collect() } /// Update the contact's information @@ -104,18 +79,12 @@ impl Contact { pub fn full_name(&self) -> String { format!("{} {}", self.first_name, self.last_name) } -} - -// Implement Storable trait (provides default dump/load) -impl Storable for Contact {} - -// Implement Model trait -impl Model for Contact { - fn get_id(&self) -> u32 { - self.id - } - - fn db_prefix() -> &'static str { + + /// Get the database prefix for this model type + pub fn db_prefix() -> &'static str { "contact" } } + +// Automatically implement GetId trait for Contact +impl_get_id!(Contact); diff --git a/herodb/src/models/mcc/event.rs b/herodb/src/models/mcc/event.rs index 43dd40b..2b8969d 100644 --- a/herodb/src/models/mcc/event.rs +++ b/herodb/src/models/mcc/event.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; -use crate::db::{Model, Storable, DB, DbError, DbResult}; use crate::models::mcc::calendar::Calendar; use crate::models::mcc::contacts::Contact; +use crate::db::model::impl_get_id; use chrono::{DateTime, Utc}; /// EventMeta contains additional metadata for a calendar event @@ -84,20 +84,16 @@ impl Event { groups.iter().any(|g| self.groups.contains(g)) } - /// Get the calendar this event belongs to - pub fn get_calendar(&self, db: &DB) -> DbResult { - db.get::(self.calendar_id) + /// Find the calendar this event belongs to + pub fn find_calendar<'a>(&self, calendars: &'a [Calendar]) -> Option<&'a Calendar> { + calendars.iter().find(|cal| cal.id == self.calendar_id) } - /// Get contacts for all attendees of this event - pub fn get_attendee_contacts(&self, db: &DB) -> DbResult> { - let all_contacts = db.list::()?; - let attendee_contacts = all_contacts - .into_iter() + /// Filter contacts that are attendees of this event + pub fn filter_attendee_contacts<'a>(&self, contacts: &'a [Contact]) -> Vec<&'a Contact> { + contacts.iter() .filter(|contact| self.attendees.contains(&contact.email)) - .collect(); - - Ok(attendee_contacts) + .collect() } /// Add an attendee to this event @@ -124,18 +120,12 @@ impl Event { pub fn search_by_description(&self, query: &str) -> bool { self.description.to_lowercase().contains(&query.to_lowercase()) } -} - -// Implement Storable trait (provides default dump/load) -impl Storable for Event {} - -// Implement Model trait -impl Model for Event { - fn get_id(&self) -> u32 { - self.id - } - - fn db_prefix() -> &'static str { + + /// Get the database prefix for this model type + pub fn db_prefix() -> &'static str { "event" } -} \ No newline at end of file +} + +// Automatically implement GetId trait for Event +impl_get_id!(Event); \ No newline at end of file diff --git a/herodb/src/models/mcc/lib.rs b/herodb/src/models/mcc/lib.rs index 8780078..711a244 100644 --- a/herodb/src/models/mcc/lib.rs +++ b/herodb/src/models/mcc/lib.rs @@ -10,6 +10,3 @@ pub use event::{Event, EventMeta}; pub use mail::{Email, Attachment, Envelope}; pub use contacts::Contact; pub use message::{Message, MessageMeta, MessageStatus}; - -// Re-export database components from db module -pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult}; \ No newline at end of file diff --git a/herodb/src/models/mcc/mail.rs b/herodb/src/models/mcc/mail.rs index e3a258a..df2d6a5 100644 --- a/herodb/src/models/mcc/mail.rs +++ b/herodb/src/models/mcc/mail.rs @@ -1,23 +1,17 @@ use serde::{Deserialize, Serialize}; -use crate::db::{Model, Storable, DB, DbError, DbResult}; +use crate::db::model::impl_get_id; use chrono::Utc; /// Email represents an email message with all its metadata and content #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Email { // Database ID - pub id: u32, // Database ID (assigned by DBHandler) - // Content fields - pub uid: u32, // Unique identifier of the message (in the circle) - pub seq_num: u32, // IMAP sequence number (in the mailbox) - pub mailbox: String, // The mailbox this email belongs to + pub id: u32, pub message: String, // The email body content pub attachments: Vec, // Any file attachments - // IMAP specific fields pub flags: Vec, // IMAP flags like \Seen, \Deleted, etc. pub receivetime: i64, // Unix timestamp when the email was received - pub envelope: Option, // IMAP envelope information (contains From, To, Subject, etc.) - pub groups: Vec, // Groups this email belongs to (references Circle IDs) + pub envelope: Option, // IMAP envelope structure } /// Attachment represents an email attachment @@ -41,7 +35,6 @@ pub struct Envelope { pub cc: Vec, pub bcc: Vec, pub in_reply_to: String, - pub message_id: String, } impl Email { @@ -49,15 +42,11 @@ impl Email { pub fn new(id: u32, uid: u32, seq_num: u32, mailbox: String, message: String) -> Self { Self { id, - uid, - seq_num, - mailbox, message, attachments: Vec::new(), flags: Vec::new(), receivetime: chrono::Utc::now().timestamp(), envelope: None, - groups: Vec::new(), } } @@ -66,23 +55,6 @@ impl Email { self.attachments.push(attachment); } - /// Add a group to this email - pub fn add_group(&mut self, group_id: u32) { - if !self.groups.contains(&group_id) { - self.groups.push(group_id); - } - } - - /// Remove a group from this email - pub fn remove_group(&mut self, group_id: u32) { - self.groups.retain(|&id| id != group_id); - } - - /// Filter by groups - returns true if this email belongs to any of the specified groups - pub fn filter_by_groups(&self, groups: &[u32]) -> bool { - groups.iter().any(|g| self.groups.contains(g)) - } - /// Search by subject - returns true if the subject contains the query (case-insensitive) pub fn search_by_subject(&self, query: &str) -> bool { if let Some(env) = &self.envelope { @@ -146,18 +118,12 @@ impl Email { message } -} - -// Implement Storable trait (provides default dump/load) -impl Storable for Email {} - -// Implement Model trait -impl Model for Email { - fn get_id(&self) -> u32 { - self.id - } - - fn db_prefix() -> &'static str { + + /// Get the database prefix for this model type + pub fn db_prefix() -> &'static str { "email" } -} \ No newline at end of file +} + +// Automatically implement GetId trait for Email +impl_get_id!(Email); \ No newline at end of file diff --git a/herodb/src/models/mcc/message.rs b/herodb/src/models/mcc/message.rs index 6d490af..2c7a06f 100644 --- a/herodb/src/models/mcc/message.rs +++ b/herodb/src/models/mcc/message.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::db::{Model, Storable, DB, DbError, DbResult}; +use crate::impl_get_id; use chrono::{DateTime, Utc}; /// MessageStatus represents the status of a message @@ -30,7 +30,6 @@ pub struct Message { pub recipients: Vec, // List of recipient identifiers pub content: String, // Message content pub attachments: Vec, // References to attachments - pub groups: Vec, // Groups this message belongs to (references Circle IDs) pub meta: MessageMeta, // Message metadata } @@ -45,7 +44,6 @@ impl Message { recipients: Vec::new(), content, attachments: Vec::new(), - groups: Vec::new(), meta: MessageMeta { created_at: now, updated_at: now, @@ -107,28 +105,18 @@ impl Message { self.meta.updated_at = Utc::now(); } - /// Get all messages in the same thread - pub fn get_thread_messages(&self, db: &DB) -> DbResult> { - let all_messages = db.list::()?; - let thread_messages = all_messages - .into_iter() + /// Filter messages that are in the same thread as this message + pub fn filter_thread_messages<'a>(&self, messages: &'a [Message]) -> Vec<&'a Message> { + messages.iter() .filter(|msg| msg.thread_id == self.thread_id) - .collect(); - - Ok(thread_messages) + .collect() + } + + /// Get the database prefix for this model type + pub fn db_prefix() -> &'static str { + "message" } } -// Implement Storable trait (provides default dump/load) -impl Storable for Message {} - -// Implement Model trait -impl Model for Message { - fn get_id(&self) -> u32 { - self.id - } - - fn db_prefix() -> &'static str { - "message" - } -} \ No newline at end of file +// Automatically implement GetId trait for Message +impl_get_id!(Message); \ No newline at end of file diff --git a/herodb/src/models/mcc/mod.rs b/herodb/src/models/mcc/mod.rs index 8780078..1f8eaf6 100644 --- a/herodb/src/models/mcc/mod.rs +++ b/herodb/src/models/mcc/mod.rs @@ -9,7 +9,4 @@ pub use calendar::Calendar; pub use event::{Event, EventMeta}; pub use mail::{Email, Attachment, Envelope}; pub use contacts::Contact; -pub use message::{Message, MessageMeta, MessageStatus}; - -// Re-export database components from db module -pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult}; \ No newline at end of file +pub use message::{Message, MessageMeta, MessageStatus}; \ No newline at end of file diff --git a/heromodels/Cargo.toml b/heromodels/Cargo.toml new file mode 100644 index 0000000..eb9fc22 --- /dev/null +++ b/heromodels/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "heromodels" +version = "0.1.0" +edition = "2021" +description = "A library for hero models with base model trait implementation" +authors = ["Your Name "] + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +bincode = "1.3" +chrono = { version = "0.4", features = ["serde"] } \ No newline at end of file diff --git a/heromodels/src/comment.rs b/heromodels/src/comment.rs new file mode 100644 index 0000000..50b0add --- /dev/null +++ b/heromodels/src/comment.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; +use crate::model::{BaseModel, BaseModelData, IndexKey}; + +/// Represents a comment on a model +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Comment { + /// Base model data + pub base_data: BaseModelData, + + /// The ID of the user who created the comment + pub user_id: u32, + + /// The ID of the model this comment is attached to + pub model_id: u32, + + /// The type of model this comment is attached to + pub model_type: String, + + /// The content of the comment + pub content: String, +} + +impl Comment { + /// Create a new comment + pub fn new(id: u32, user_id: u32, model_id: u32, model_type: String, content: String) -> Self { + Self { + base_data: BaseModelData::new(id), + user_id, + model_id, + model_type, + content, + } + } + + /// Update the comment content + pub fn update_content(&mut self, content: String) { + self.content = content; + self.base_data.update_modified(); + } +} + +impl BaseModel for Comment { + fn db_prefix() -> &'static str { + "comment" + } + + fn get_id(&self) -> u32 { + self.base_data.id + } + + fn db_keys(&self) -> Vec { + vec![ + IndexKey { + name: "user_id", + value: self.user_id.to_string(), + }, + IndexKey { + name: "model_id", + value: format!("{}:{}", self.model_type, self.model_id), + }, + ] + } +} \ No newline at end of file diff --git a/heromodels/src/lib.rs b/heromodels/src/lib.rs new file mode 100644 index 0000000..bb59b4c --- /dev/null +++ b/heromodels/src/lib.rs @@ -0,0 +1,47 @@ +//! # Hero Models +//! +//! A library for hero models with base model trait implementation. +//! +//! This crate provides a base model trait and implementation that other models can inherit from. +//! It also provides a Comment model that can be used to add comments to any model. + +pub mod model; +pub mod comment; +pub mod user; + +// Re-export key types for convenience +pub use model::{BaseModel, BaseModelData, IndexKey, impl_base_model}; +pub use comment::Comment; +pub use user::User; + +/// Example of how to use the heromodels crate +/// +/// ```rust +/// use heromodels::{BaseModel, User, Comment}; +/// +/// // Create a new user +/// let mut user = User::new( +/// 1, +/// "johndoe".to_string(), +/// "john.doe@example.com".to_string(), +/// "John Doe".to_string() +/// ); +/// +/// // Create a comment for the user +/// let comment = Comment::new( +/// 1, +/// 2, // commenter's user ID +/// user.get_id(), +/// User::db_prefix().to_string(), +/// "This is a comment on the user".to_string() +/// ); +/// +/// // Add the comment to the user +/// user.base_data.add_comment(comment.get_id()); +/// +/// // Get the database prefix for the User model +/// assert_eq!(User::db_prefix(), "user"); +/// +/// // Get the database keys for the user +/// let keys = user.db_keys(); +/// assert!(keys.iter().any(|k| k.name == "username" && k.value == "johndoe")); \ No newline at end of file diff --git a/heromodels/src/model.rs b/heromodels/src/model.rs new file mode 100644 index 0000000..a45a804 --- /dev/null +++ b/heromodels/src/model.rs @@ -0,0 +1,181 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +/// Represents an index key for a model +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IndexKey { + /// The name of the index key + pub name: &'static str, + + /// The value of the index key for a specific model instance + pub value: String, +} + +/// Builder for IndexKey +pub struct IndexKeyBuilder { + name: &'static str, + value: String, +} + +impl IndexKeyBuilder { + /// Create a new IndexKeyBuilder + pub fn new(name: &'static str) -> Self { + Self { + name, + value: String::new(), + } + } + + /// Set the value for this index key + pub fn value(mut self, value: impl ToString) -> Self { + self.value = value.to_string(); + self + } + + /// Build the IndexKey + pub fn build(self) -> IndexKey { + IndexKey { + name: self.name, + value: self.value, + } + } +} + +/// Base trait for all models +pub trait BaseModel: Debug + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static { + /// Get the database prefix for this model type + fn db_prefix() -> &'static str where Self: Sized; + + /// Returns a list of index keys for this model instance + /// These keys will be used to create additional indexes in the TST + /// The default implementation returns an empty vector + /// Override this method to provide custom indexes + fn db_keys(&self) -> Vec { + Vec::new() + } + + /// Get the unique ID for this model + fn get_id(&self) -> u32; +} + +/// Base struct that all models should include +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BaseModelData { + /// Unique incremental ID per circle + pub id: u32, + + /// Unix epoch timestamp for creation time + pub created_at: i64, + + /// Unix epoch timestamp for last modification time + pub modified_at: i64, + + /// List of comment IDs referencing Comment objects + pub comments: Vec, +} + +impl BaseModelData { + /// Create a new BaseModelData instance + pub fn new(id: u32) -> Self { + let now = chrono::Utc::now().timestamp(); + Self { + id, + created_at: now, + modified_at: now, + comments: Vec::new(), + } + } + + /// Create a new BaseModelDataBuilder + pub fn builder(id: u32) -> BaseModelDataBuilder { + BaseModelDataBuilder::new(id) + } + + /// Add a comment to this model + pub fn add_comment(&mut self, comment_id: u32) { + self.comments.push(comment_id); + self.modified_at = chrono::Utc::now().timestamp(); + } + + /// Remove a comment from this model + pub fn remove_comment(&mut self, comment_id: u32) { + self.comments.retain(|&id| id != comment_id); + self.modified_at = chrono::Utc::now().timestamp(); + } + + /// Update the modified timestamp + pub fn update_modified(&mut self) { + self.modified_at = chrono::Utc::now().timestamp(); + } +} + +/// Builder for BaseModelData +pub struct BaseModelDataBuilder { + id: u32, + created_at: Option, + modified_at: Option, + comments: Vec, +} + +impl BaseModelDataBuilder { + /// Create a new BaseModelDataBuilder + pub fn new(id: u32) -> Self { + Self { + id, + created_at: None, + modified_at: None, + comments: Vec::new(), + } + } + + /// Set the created_at timestamp + pub fn created_at(mut self, timestamp: i64) -> Self { + self.created_at = Some(timestamp); + self + } + + /// Set the modified_at timestamp + pub fn modified_at(mut self, timestamp: i64) -> Self { + self.modified_at = Some(timestamp); + self + } + + /// Add a comment ID + pub fn add_comment(mut self, comment_id: u32) -> Self { + self.comments.push(comment_id); + self + } + + /// Add multiple comment IDs + pub fn add_comments(mut self, comment_ids: Vec) -> Self { + self.comments.extend(comment_ids); + self + } + + /// Build the BaseModelData + pub fn build(self) -> BaseModelData { + let now = chrono::Utc::now().timestamp(); + BaseModelData { + id: self.id, + created_at: self.created_at.unwrap_or(now), + modified_at: self.modified_at.unwrap_or(now), + comments: self.comments, + } + } +} + +/// Macro to implement BaseModel for a struct that contains a base_data field of type BaseModelData +#[macro_export] +macro_rules! impl_base_model { + ($type:ty, $prefix:expr) => { + impl BaseModel for $type { + fn db_prefix() -> &'static str { + $prefix + } + + fn get_id(&self) -> u32 { + self.base_data.id + } + } + }; +} \ No newline at end of file diff --git a/heromodels/src/user.rs b/heromodels/src/user.rs new file mode 100644 index 0000000..664d403 --- /dev/null +++ b/heromodels/src/user.rs @@ -0,0 +1,86 @@ +use serde::{Deserialize, Serialize}; +use crate::model::{BaseModel, BaseModelData, IndexKey}; + +/// Represents a user in the system +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + /// Base model data + pub base_data: BaseModelData, + + /// User's username + pub username: String, + + /// User's email address + pub email: String, + + /// User's full name + pub full_name: String, + + /// Whether the user is active + pub is_active: bool, +} + +impl User { + /// Create a new user + pub fn new(id: u32, username: String, email: String, full_name: String) -> Self { + Self { + base_data: BaseModelData::new(id), + username, + email, + full_name, + is_active: true, + } + } + + /// Deactivate the user + pub fn deactivate(&mut self) { + self.is_active = false; + self.base_data.update_modified(); + } + + /// Activate the user + pub fn activate(&mut self) { + self.is_active = true; + self.base_data.update_modified(); + } + + /// Update user's email + pub fn update_email(&mut self, email: String) { + self.email = email; + self.base_data.update_modified(); + } + + /// Update user's full name + pub fn update_full_name(&mut self, full_name: String) { + self.full_name = full_name; + self.base_data.update_modified(); + } +} + +// Implement BaseModel for User +impl BaseModel for User { + fn db_prefix() -> &'static str { + "user" + } + + fn get_id(&self) -> u32 { + self.base_data.id + } + + fn db_keys(&self) -> Vec { + vec![ + IndexKey { + name: "username", + value: self.username.clone(), + }, + IndexKey { + name: "email", + value: self.email.clone(), + }, + IndexKey { + name: "is_active", + value: self.is_active.to_string(), + }, + ] + } +} \ No newline at end of file diff --git a/mcc_models_standalone_plan.md b/mcc_models_standalone_plan.md new file mode 100644 index 0000000..049c767 --- /dev/null +++ b/mcc_models_standalone_plan.md @@ -0,0 +1,161 @@ +# MCC Models Standalone Implementation Plan + +## Overview + +This document outlines the plan to make the MCC models in `herodb/src/models/mcc` completely standalone without dependencies on the database implementation, while ensuring that examples like `herodb/src/cmd/dbexample_mcc` can still work. + +## Current Architecture Analysis + +```mermaid +graph TD + subgraph "Before" + A1[MCC Models] -->|Depends on| B1[DB Implementation] + A1 -->|Implements| C1[Model Trait] + A1 -->|Implements| D1[Storable Trait] + E1[dbexample_mcc] -->|Uses| A1 + E1 -->|Uses| B1 + end + + subgraph "After" + A2[Standalone MCC Models] -.->|No dependency on| B2[DB Implementation] + B2 -->|Works with| F2[Any Serializable Struct] + B2 -->|Optional trait| G2[ModelAdapter Trait] + E2[dbexample_mcc] -->|Uses| A2 + E2 -->|Uses| B2 + end +``` + +## Implementation Plan + +### Phase 1: Make MCC Models Standalone + +1. For each MCC model file (calendar.rs, event.rs, mail.rs, contacts.rs, message.rs): + - Remove `use crate::db::{Model, Storable, DB, DbError, DbResult}` + - Remove `impl Model for X` and `impl Storable for X` blocks + - Replace methods that use DB (like `get_events(&self, db: &DB)`) with standalone methods + +2. For mod.rs and lib.rs: + - Remove `pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult}` + +### Phase 2: Modify DB Implementation + +1. Create a new ModelAdapter trait in db/model.rs: +```rust +pub trait ModelAdapter { + fn get_id(&self) -> u32; + fn db_prefix() -> &'static str; + fn db_keys(&self) -> Vec { Vec::new() } +} +``` + +2. Modify DB methods in db.rs to work with any serializable struct: +```rust +impl DB { + // Generic set method for any serializable type + pub fn set(&self, item: &T, id: u32, prefix: &str) -> DbResult<()> { + // Implementation + } + + // Enhanced version for types implementing ModelAdapter + pub fn set_model(&self, item: &T) -> DbResult<()> { + self.set(item, item.get_id(), T::db_prefix()) + } + + // Similar changes for get, delete, list methods +} +``` + +3. Update DBBuilder to register any serializable type: +```rust +impl DBBuilder { + pub fn register_type(&mut self, prefix: &'static str) -> &mut Self { + // Implementation + } + + pub fn register_model(&mut self) -> &mut Self { + self.register_type::(T::db_prefix()) + } +} +``` + +### Phase 3: Update Examples and Tests + +1. Update dbexample_mcc/main.rs: +```rust +let db = DBBuilder::new(&db_path) + .register_type::("calendar") + .register_type::("event") + // etc. + .build()?; +``` + +2. Create a new standalone example for MCC models similar to circle_standalone.rs + +## Detailed Changes Required + +### MCC Models Changes + +#### calendar.rs +- Remove database-related imports +- Remove Model and Storable trait implementations +- Replace `get_events(&self, db: &DB)` with a standalone method like: + ```rust + pub fn filter_events(&self, events: &[Event]) -> Vec<&Event> { + events.iter() + .filter(|event| event.calendar_id == self.id) + .collect() + } + ``` + +#### event.rs +- Remove database-related imports +- Remove Model and Storable trait implementations +- Add standalone methods for event operations + +#### mail.rs +- Remove database-related imports +- Remove Model and Storable trait implementations +- Add standalone methods for mail operations + +#### contacts.rs +- Remove database-related imports +- Remove Model and Storable trait implementations +- Add standalone methods for contact operations + +#### message.rs +- Remove database-related imports +- Remove Model and Storable trait implementations +- Add standalone methods for message operations + +#### mod.rs and lib.rs +- Remove re-exports of database components + +### DB Implementation Changes + +#### model.rs +- Create a new ModelAdapter trait +- Keep existing Model and Storable traits for backward compatibility +- Add helper methods for working with serializable structs + +#### db.rs +- Modify DB methods to work with any serializable struct +- Add overloaded methods for ModelAdapter types +- Ensure backward compatibility with existing code + +#### DBBuilder +- Update to register any serializable type +- Keep existing methods for backward compatibility + +## Testing Strategy + +1. Ensure all existing tests pass with the modified DB implementation +2. Create new tests for standalone MCC models +3. Verify dbexample_mcc works with the new implementation +4. Create a new standalone example for MCC models + +## Benefits + +1. MCC models become more reusable and can be used without database dependencies +2. DB implementation becomes more flexible and can work with any serializable struct +3. Cleaner separation of concerns between models and database operations +4. Easier to test models in isolation \ No newline at end of file