This commit is contained in:
despiegk 2025-04-22 07:50:03 +04:00
parent 6443c6b647
commit d75de1e73c
20 changed files with 1007 additions and 640 deletions

View File

@ -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<dyn std::error::Error>> {
// Create a database instance with our models registered
let db = DBBuilder::new(&db_path)
.register_model::<Calendar>()
.register_model::<Event>()
.register_model::<Email>()
.register_model::<Contact>()
.register_model::<Message>()
.register_model::<Circle>()
.register_type::<Calendar>("calendar")
.register_type::<Event>("event")
.register_type::<Email>("email")
.register_type::<Contact>("contact")
.register_type::<Message>("message")
.register_model::<Circle>() // Circle still uses the Model trait
.build()?;
println!("\n1. Creating Circles (Groups)");
@ -96,9 +96,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
bob.add_group(work_circle.id); // Bob is both a friend and a work contact
// Insert contacts
db.set::<Contact>(&john)?;
db.set::<Contact>(&alice)?;
db.set::<Contact>(&bob)?;
db.set_any::<Contact>(&john, "contact")?;
db.set_any::<Contact>(&alice, "contact")?;
db.set_any::<Contact>(&bob, "contact")?;
println!("Created contacts:");
println!(" - {}: {} (Groups: {:?})", john.full_name(), john.email, john.groups);
@ -125,8 +125,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
personal_calendar.add_group(friends_circle.id);
// Insert calendars
db.set::<Calendar>(&work_calendar)?;
db.set::<Calendar>(&personal_calendar)?;
db.set_any::<Calendar>(&work_calendar, "calendar")?;
db.set_any::<Calendar>(&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<dyn std::error::Error>> {
family_dinner.add_attendee(alice.email.clone());
// Insert events
db.set::<Event>(&work_meeting)?;
db.set::<Event>(&family_dinner)?;
db.set_any::<Event>(&work_meeting, "event")?;
db.set_any::<Event>(&family_dinner, "event")?;
println!("Created events:");
println!(" - {}: {} on {} (Groups: {:?})",
@ -244,8 +244,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
family_email.set_envelope(family_envelope);
// Insert emails
db.set::<Email>(&work_email)?;
db.set::<Email>(&family_email)?;
db.set_any::<Email>(&work_email, "email")?;
db.set_any::<Email>(&family_email, "email")?;
println!("Created emails:");
println!(" - From: {}, Subject: {} (Groups: {:?})",
@ -284,8 +284,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
friends_chat.add_reaction("👍".to_string());
// Insert messages
db.set::<Message>(&work_chat)?;
db.set::<Message>(&friends_chat)?;
db.set_any::<Message>(&work_chat, "message")?;
db.set_any::<Message>(&friends_chat, "message")?;
println!("Created messages:");
println!(" - From: {}, Content: {} (Groups: {:?})",
@ -305,7 +305,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Filter contacts by group
println!("\nFiltering contacts by work group (ID: {}):", work_circle.id);
let all_contacts = db.list::<Contact>()?;
let all_contacts = db.list_any::<Contact>()?;
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<dyn std::error::Error>> {
// Search emails by subject
println!("\nSearching emails with subject containing 'Meeting':");
let all_emails = db.list::<Email>()?;
let all_emails = db.list_any::<Email>()?;
for email in all_emails {
if email.search_by_subject("Meeting") {
println!(" - Subject: {}, From: {}",
@ -326,7 +326,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Get events for a calendar
println!("\nGetting events for Work Calendar (ID: {}):", work_calendar.id);
let all_events = db.list::<Event>()?;
let all_events = db.list_any::<Event>()?;
let work_events: Vec<Event> = all_events
.into_iter()
.filter(|event| event.calendar_id == work_calendar.id)
@ -341,7 +341,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Get attendee contacts for an event
println!("\nGetting attendee contacts for Team Meeting (ID: {}):", work_meeting.id);
let all_contacts = db.list::<Contact>()?;
let all_contacts = db.list_any::<Contact>()?;
let attendee_contacts: Vec<Contact> = all_contacts
.into_iter()
.filter(|contact| work_meeting.attendees.contains(&contact.email))
@ -358,19 +358,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!(" - Converted Message Groups: {:?}", email_to_message.groups);
// Insert the converted message
db.set::<Message>(&email_to_message)?;
db.set_any::<Message>(&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::<Calendar>(family_dinner.calendar_id)?;
let event_calendar = db.get_any::<Calendar>(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::<Event>()?;
let all_events = db.list_any::<Event>()?;
let john_events: Vec<Event> = all_events
.into_iter()
.filter(|event| event.attendees.contains(&john.email))
@ -385,7 +385,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Get messages in the same thread
println!("\nGetting all messages in the work chat thread:");
let all_messages = db.list::<Message>()?;
let all_messages = db.list_any::<Message>()?;
let thread_messages: Vec<Message> = all_messages
.into_iter()
.filter(|message| message.thread_id == work_chat.thread_id)

View File

@ -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<T: Model> ModelRegistrar<T> {
}
}
/// Implementation of ModelRegistration for any serializable type that implements GetId
pub struct TypeRegistrar<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> {
prefix: &'static str,
phantom: std::marker::PhantomData<T>,
}
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> TypeRegistrar<T> {
pub fn new(prefix: &'static str) -> Self {
Self {
prefix,
phantom: std::marker::PhantomData,
}
}
}
impl<T: Model> ModelRegistration for ModelRegistrar<T> {
fn register(&self, path: &Path) -> DbResult<(TypeId, Arc<RwLock<dyn DbOperations>>)> {
let store = OurDbStore::<T>::open(path.join(T::db_prefix()))?;
@ -89,6 +106,13 @@ impl<T: Model> ModelRegistration for ModelRegistrar<T> {
}
}
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> ModelRegistration for TypeRegistrar<T> {
fn register(&self, path: &Path) -> DbResult<(TypeId, Arc<RwLock<dyn DbOperations>>)> {
let store = GenericStore::<T>::open(path, self.prefix)?;
Ok((TypeId::of::<T>(), Arc::new(RwLock::new(store)) as Arc<RwLock<dyn DbOperations>>))
}
}
impl DBBuilder {
/// Create a new DB builder
pub fn new<P: Into<PathBuf>>(base_path: P) -> Self {
@ -112,6 +136,16 @@ impl DBBuilder {
self
}
/// Register any serializable type with the DB
pub fn register_type<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
mut self,
prefix: &'static str
) -> Self {
self.model_registrations
.push(Arc::new(TypeRegistrar::<T>::new(prefix)));
self
}
/// Build the DB with the registered models
pub fn build(self) -> Result<DB, Box<EvalAltResult>> {
let base_path = self.base_path;
@ -342,6 +376,57 @@ impl DB {
}
}
/// Insert any serializable struct that implements GetId
pub fn set_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
&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::<T>(),
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::<T>()) {
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<T: Model>(&self, id: u32) -> Option<Result<Option<T>, DbError>> {
// Try to acquire a read lock on the transaction
@ -399,19 +484,41 @@ impl DB {
if let Some(tx_result) = self.check_transaction::<T>(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::<T>()) {
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::<T>() {
Ok(t) => Ok(*t),
let any_result = db_ops_guard.get(id)?;
// Try to downcast to T
match any_result.downcast::<T>() {
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<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
&self,
id: u32
) -> DbResult<T> {
// If not found in transaction, get from database
match self.type_map.get(&TypeId::of::<T>()) {
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::<T>() {
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<T: Model>(&self, id: u32) -> DbResult<()> {
// First, get the model to extract its index keys
let model = self.get::<T>(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::<T>(),
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<T: Model>(&self) -> DbResult<Vec<T>> {
// Get the prefix for this model type
let prefix = T::db_prefix();
/// Delete any serializable struct by its ID and type
pub fn delete_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
&self,
id: u32,
prefix: &str
) -> DbResult<()> {
// Execute directly
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let mut db_ops_guard = db_ops.write().unwrap();
db_ops_guard.delete(id)?;
// Use the TST index to get all objects with this prefix
// Also delete from the TST index
let mut tst_index = self.tst_index.write().unwrap();
let items = tst_index.list(prefix)?;
tst_index.delete(prefix, id)?;
// Deserialize the objects
let mut result = Vec::with_capacity(items.len());
for (_, data) in items {
let model = T::from_bytes(&data)?;
result.push(model);
Ok(())
}
None => Err(DbError::TypeError),
}
}
Ok(result)
}
/// Helper method to list models directly from OurDB (not using TST)
fn list_from_ourdb<T: Model>(&self) -> DbResult<Vec<T>> {
/// List all model instances of a given type
pub fn list<T: Model>(&self) -> DbResult<Vec<T>> {
match self.type_map.get(&TypeId::of::<T>()) {
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<T> since we looked it up by TypeId
match result_any.downcast::<Vec<T>>() {
Ok(vec_t) => Ok(*vec_t),
let any_result = db_ops_guard.list()?;
// Try to downcast to Vec<T>
match any_result.downcast::<Vec<T>>() {
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<T: Model>(&self) -> DbResult<()> {
// Get all models from OurDB
let models = self.list_from_ourdb::<T>()?;
// 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<T: Model>(&self, id: u32, depth: u8) -> DbResult<Vec<T>> {
// Look up the correct DB operations for type T in our type map
/// List all instances of any serializable type
pub fn list_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
&self
) -> DbResult<Vec<T>> {
match self.type_map.get(&TypeId::of::<T>()) {
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::<T>() {
Ok(t) => result.push(*t),
Err(_) => return Err(DbError::TypeError),
// Try to downcast to Vec<T>
match any_result.downcast::<Vec<T>>() {
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<T: Model>(&mut self) -> DbResult<()> {
// Create the OurDB store
let store = OurDbStore::<T>::open(&self.db_path)?;
self.type_map.insert(TypeId::of::<T>(), Arc::new(RwLock::new(store)));
/// Get the history of a model instance
pub fn get_history<T: Model>(&self, id: u32, depth: u8) -> DbResult<Vec<T>> {
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let mut db_ops_guard = db_ops.write().unwrap();
let any_results = db_ops_guard.get_history(id, depth)?;
// 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<T: Model>(&self, index_name: &str, index_value: &str) -> DbResult<Vec<T>> {
// 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::<T>(id) {
Ok(model) => result.push(model),
Err(DbError::NotFound(_)) => continue, // Skip if not found
Err(e) => return Err(e),
let mut results = Vec::with_capacity(any_results.len());
for any_result in any_results {
match any_result.downcast::<T>() {
Ok(boxed_t) => results.push(*boxed_t),
Err(_) => return Err(DbError::TypeError),
}
}
Ok(result)
Ok(results)
}
/// Find models by a prefix of an index key
pub fn find_by_index_prefix<T: Model>(&self, index_name: &str, index_value_prefix: &str) -> DbResult<Vec<T>> {
// 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::<T>(id) {
Ok(model) => result.push(model),
Err(DbError::NotFound(_)) => continue, // Skip if not found
Err(e) => return Err(e),
None => Err(DbError::TypeError),
}
}
Ok(result)
}
}

View File

@ -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<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> {
db: OurDB,
path: PathBuf,
prefix: String,
_phantom: PhantomData<T>,
}
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> GenericStore<T> {
/// Opens or creates an OurDB database at the specified path
pub fn open<P: AsRef<Path>>(path: P, prefix: &str) -> DbResult<Self> {
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<Vec<u8>> {
bincode::serialize(item).map_err(DbError::SerializationError)
}
/// Deserializes bytes to an item
fn deserialize(data: &[u8]) -> DbResult<T> {
bincode::deserialize(data).map_err(DbError::SerializationError)
}
/// Gets the raw bytes for an item by ID
pub fn get_raw(&self, id: u32) -> DbResult<Vec<u8>> {
self.db.get(id).map_err(DbError::OurDbError)
}
/// Lists all raw items as bytes
pub fn list_raw(&self) -> DbResult<Vec<Vec<u8>>> {
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<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> DbOperations for GenericStore<T> {
fn delete(&mut self, id: u32) -> DbResult<()> {
self.db.delete(id).map_err(DbError::OurDbError)
}
fn get(&mut self, id: u32) -> DbResult<Box<dyn Any>> {
let data = self.db.get(id).map_err(DbError::OurDbError)?;
let item = Self::deserialize(&data)?;
Ok(Box::new(item))
}
fn list(&self) -> DbResult<Box<dyn Any>> {
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::<T>() {
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<Vec<Box<dyn Any>>> {
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)
}
}

View File

@ -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};

View File

@ -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<IndexKey> {
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<T: Serialize>(value: &T) -> DbResult<Vec<u8>> {
bincode::serialize(value).map_err(DbError::SerializationError)
}
/// Deserialize bytes to any deserializable type
pub fn from_bytes<T: DeserializeOwned>(data: &[u8]) -> DbResult<T> {
bincode::deserialize(data).map_err(DbError::SerializationError)
}
}
// Note: We don't provide a blanket implementation of Storable
// Each model type must implement Storable explicitly

View File

@ -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;

View File

@ -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<u32>` 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<u32>` 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<u32>` field to `Calendar` struct
- Add utility methods:
- `filter_by_groups(groups: &[u32]) -> bool`
- `get_events(&self, db: &SledDB<Event>) -> SledDBResult<Vec<Event>>` (relationship method)
#### 3.1.3 Event Model (`event.rs`)
- Add `groups: Vec<u32>` field to `Event` struct
- Add utility methods:
- `filter_by_groups(groups: &[u32]) -> bool`
- `get_calendar(&self, db: &SledDB<Calendar>) -> SledDBResult<Calendar>` (relationship method)
- `get_attendee_contacts(&self, db: &SledDB<Contact>) -> SledDBResult<Vec<Contact>>` (relationship method)
#### 3.1.4 Contacts Model (`contacts.rs`)
- Add `groups: Vec<u32>` 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<Event>) -> SledDBResult<Vec<Event>>` (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<Utc>,
pub updated_at: DateTime<Utc>,
pub status: MessageStatus,
pub is_edited: bool,
pub reactions: Vec<String>,
}
/// 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<String>, // List of recipient identifiers
pub content: String, // Message content
pub attachments: Vec<String>, // References to attachments
pub groups: Vec<u32>, // 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

View File

@ -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<Vec<Event>> {
let all_events = db.list::<Event>()?;
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)
}
}
impl Storable for Calendar{}
// Implement Model trait
impl Model for Calendar {
fn get_id(&self) -> u32 {
self.id
.collect()
}
fn db_prefix() -> &'static str {
/// Get the database prefix for this model type
pub fn db_prefix() -> &'static str {
"calendar"
}
}
// Automatically implement GetId trait for Calendar
impl_get_id!(Calendar);

View File

@ -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<u32>, // Groups this contact belongs to (references Circle IDs)
pub emails: Vec<String>, // Changed from []String to Vec<String>
}
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<String>) -> 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<Vec<Event>> {
let all_events = db.list::<Event>()?;
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);

View File

@ -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<Calendar> {
db.get::<Calendar>(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<Vec<Contact>> {
let all_contacts = db.list::<Contact>()?;
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"
}
}
// Automatically implement GetId trait for Event
impl_get_id!(Event);

View File

@ -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};

View File

@ -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<Attachment>, // Any file attachments
// IMAP specific fields
pub flags: Vec<String>, // IMAP flags like \Seen, \Deleted, etc.
pub receivetime: i64, // Unix timestamp when the email was received
pub envelope: Option<Envelope>, // IMAP envelope information (contains From, To, Subject, etc.)
pub groups: Vec<u32>, // Groups this email belongs to (references Circle IDs)
pub envelope: Option<Envelope>, // IMAP envelope structure
}
/// Attachment represents an email attachment
@ -41,7 +35,6 @@ pub struct Envelope {
pub cc: Vec<String>,
pub bcc: Vec<String>,
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"
}
}
// Automatically implement GetId trait for Email
impl_get_id!(Email);

View File

@ -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<String>, // List of recipient identifiers
pub content: String, // Message content
pub attachments: Vec<String>, // References to attachments
pub groups: Vec<u32>, // 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<Vec<Message>> {
let all_messages = db.list::<Message>()?;
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)
}
}
// 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
.collect()
}
fn db_prefix() -> &'static str {
/// Get the database prefix for this model type
pub fn db_prefix() -> &'static str {
"message"
}
}
// Automatically implement GetId trait for Message
impl_get_id!(Message);

View File

@ -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};

11
heromodels/Cargo.toml Normal file
View File

@ -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 <your.email@example.com>"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
bincode = "1.3"
chrono = { version = "0.4", features = ["serde"] }

63
heromodels/src/comment.rs Normal file
View File

@ -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<IndexKey> {
vec![
IndexKey {
name: "user_id",
value: self.user_id.to_string(),
},
IndexKey {
name: "model_id",
value: format!("{}:{}", self.model_type, self.model_id),
},
]
}
}

47
heromodels/src/lib.rs Normal file
View File

@ -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"));

181
heromodels/src/model.rs Normal file
View File

@ -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<IndexKey> {
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<u32>,
}
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<i64>,
modified_at: Option<i64>,
comments: Vec<u32>,
}
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<u32>) -> 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
}
}
};
}

86
heromodels/src/user.rs Normal file
View File

@ -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<IndexKey> {
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(),
},
]
}
}

View File

@ -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<IndexKey> { 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<T: Serialize + DeserializeOwned + 'static>(&self, item: &T, id: u32, prefix: &str) -> DbResult<()> {
// Implementation
}
// Enhanced version for types implementing ModelAdapter
pub fn set_model<T: Serialize + DeserializeOwned + ModelAdapter + 'static>(&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<T: Serialize + DeserializeOwned + 'static>(&mut self, prefix: &'static str) -> &mut Self {
// Implementation
}
pub fn register_model<T: Serialize + DeserializeOwned + ModelAdapter + 'static>(&mut self) -> &mut Self {
self.register_type::<T>(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>("calendar")
.register_type::<Event>("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