...
This commit is contained in:
parent
6443c6b647
commit
d75de1e73c
@ -1,5 +1,5 @@
|
|||||||
use chrono::{Utc, Duration};
|
use chrono::{Utc, Duration};
|
||||||
use herodb::db::DBBuilder;
|
use herodb::db::{DBBuilder, GetId};
|
||||||
use herodb::models::mcc::{
|
use herodb::models::mcc::{
|
||||||
Calendar, Event,
|
Calendar, Event,
|
||||||
Email, Attachment, Envelope,
|
Email, Attachment, Envelope,
|
||||||
@ -23,12 +23,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Create a database instance with our models registered
|
// Create a database instance with our models registered
|
||||||
let db = DBBuilder::new(&db_path)
|
let db = DBBuilder::new(&db_path)
|
||||||
.register_model::<Calendar>()
|
.register_type::<Calendar>("calendar")
|
||||||
.register_model::<Event>()
|
.register_type::<Event>("event")
|
||||||
.register_model::<Email>()
|
.register_type::<Email>("email")
|
||||||
.register_model::<Contact>()
|
.register_type::<Contact>("contact")
|
||||||
.register_model::<Message>()
|
.register_type::<Message>("message")
|
||||||
.register_model::<Circle>()
|
.register_model::<Circle>() // Circle still uses the Model trait
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
println!("\n1. Creating Circles (Groups)");
|
println!("\n1. Creating Circles (Groups)");
|
||||||
@ -48,8 +48,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let friends_circle = Circle::new(
|
let friends_circle = Circle::new(
|
||||||
3,
|
3,
|
||||||
"Friends".to_string(),
|
"Friends".to_string(),
|
||||||
"Friends communications".to_string()
|
"Friends communications".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -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
|
bob.add_group(work_circle.id); // Bob is both a friend and a work contact
|
||||||
|
|
||||||
// Insert contacts
|
// Insert contacts
|
||||||
db.set::<Contact>(&john)?;
|
db.set_any::<Contact>(&john, "contact")?;
|
||||||
db.set::<Contact>(&alice)?;
|
db.set_any::<Contact>(&alice, "contact")?;
|
||||||
db.set::<Contact>(&bob)?;
|
db.set_any::<Contact>(&bob, "contact")?;
|
||||||
|
|
||||||
println!("Created contacts:");
|
println!("Created contacts:");
|
||||||
println!(" - {}: {} (Groups: {:?})", john.full_name(), john.email, john.groups);
|
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);
|
personal_calendar.add_group(friends_circle.id);
|
||||||
|
|
||||||
// Insert calendars
|
// Insert calendars
|
||||||
db.set::<Calendar>(&work_calendar)?;
|
db.set_any::<Calendar>(&work_calendar, "calendar")?;
|
||||||
db.set::<Calendar>(&personal_calendar)?;
|
db.set_any::<Calendar>(&personal_calendar, "calendar")?;
|
||||||
|
|
||||||
println!("Created calendars:");
|
println!("Created calendars:");
|
||||||
println!(" - {}: {} (Groups: {:?})", work_calendar.id, work_calendar.title, work_calendar.groups);
|
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());
|
family_dinner.add_attendee(alice.email.clone());
|
||||||
|
|
||||||
// Insert events
|
// Insert events
|
||||||
db.set::<Event>(&work_meeting)?;
|
db.set_any::<Event>(&work_meeting, "event")?;
|
||||||
db.set::<Event>(&family_dinner)?;
|
db.set_any::<Event>(&family_dinner, "event")?;
|
||||||
|
|
||||||
println!("Created events:");
|
println!("Created events:");
|
||||||
println!(" - {}: {} on {} (Groups: {:?})",
|
println!(" - {}: {} on {} (Groups: {:?})",
|
||||||
@ -244,8 +244,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
family_email.set_envelope(family_envelope);
|
family_email.set_envelope(family_envelope);
|
||||||
|
|
||||||
// Insert emails
|
// Insert emails
|
||||||
db.set::<Email>(&work_email)?;
|
db.set_any::<Email>(&work_email, "email")?;
|
||||||
db.set::<Email>(&family_email)?;
|
db.set_any::<Email>(&family_email, "email")?;
|
||||||
|
|
||||||
println!("Created emails:");
|
println!("Created emails:");
|
||||||
println!(" - From: {}, Subject: {} (Groups: {:?})",
|
println!(" - From: {}, Subject: {} (Groups: {:?})",
|
||||||
@ -284,8 +284,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
friends_chat.add_reaction("👍".to_string());
|
friends_chat.add_reaction("👍".to_string());
|
||||||
|
|
||||||
// Insert messages
|
// Insert messages
|
||||||
db.set::<Message>(&work_chat)?;
|
db.set_any::<Message>(&work_chat, "message")?;
|
||||||
db.set::<Message>(&friends_chat)?;
|
db.set_any::<Message>(&friends_chat, "message")?;
|
||||||
|
|
||||||
println!("Created messages:");
|
println!("Created messages:");
|
||||||
println!(" - From: {}, Content: {} (Groups: {:?})",
|
println!(" - From: {}, Content: {} (Groups: {:?})",
|
||||||
@ -305,7 +305,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Filter contacts by group
|
// Filter contacts by group
|
||||||
println!("\nFiltering contacts by work group (ID: {}):", work_circle.id);
|
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 {
|
for contact in all_contacts {
|
||||||
if contact.filter_by_groups(&[work_circle.id]) {
|
if contact.filter_by_groups(&[work_circle.id]) {
|
||||||
println!(" - {} ({})", contact.full_name(), contact.email);
|
println!(" - {} ({})", contact.full_name(), contact.email);
|
||||||
@ -314,7 +314,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Search emails by subject
|
// Search emails by subject
|
||||||
println!("\nSearching emails with subject containing 'Meeting':");
|
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 {
|
for email in all_emails {
|
||||||
if email.search_by_subject("Meeting") {
|
if email.search_by_subject("Meeting") {
|
||||||
println!(" - Subject: {}, From: {}",
|
println!(" - Subject: {}, From: {}",
|
||||||
@ -326,7 +326,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Get events for a calendar
|
// Get events for a calendar
|
||||||
println!("\nGetting events for Work Calendar (ID: {}):", work_calendar.id);
|
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
|
let work_events: Vec<Event> = all_events
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|event| event.calendar_id == work_calendar.id)
|
.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
|
// Get attendee contacts for an event
|
||||||
println!("\nGetting attendee contacts for Team Meeting (ID: {}):", work_meeting.id);
|
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
|
let attendee_contacts: Vec<Contact> = all_contacts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|contact| work_meeting.attendees.contains(&contact.email))
|
.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);
|
println!(" - Converted Message Groups: {:?}", email_to_message.groups);
|
||||||
|
|
||||||
// Insert the converted message
|
// Insert the converted message
|
||||||
db.set::<Message>(&email_to_message)?;
|
db.set_any::<Message>(&email_to_message, "message")?;
|
||||||
|
|
||||||
println!("\n8. Relationship Management");
|
println!("\n8. Relationship Management");
|
||||||
println!("------------------------");
|
println!("------------------------");
|
||||||
|
|
||||||
// Get the calendar for an event
|
// Get the calendar for an event
|
||||||
println!("\nGetting calendar for Family Dinner event (ID: {}):", family_dinner.id);
|
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);
|
println!(" - Calendar: {} ({})", event_calendar.title, event_calendar.description);
|
||||||
|
|
||||||
// Get events for a contact
|
// Get events for a contact
|
||||||
println!("\nGetting events where John Doe is an attendee:");
|
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
|
let john_events: Vec<Event> = all_events
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|event| event.attendees.contains(&john.email))
|
.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
|
// Get messages in the same thread
|
||||||
println!("\nGetting all messages in the work chat 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
|
let thread_messages: Vec<Message> = all_messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|message| message.thread_id == work_chat.thread_id)
|
.filter(|message| message.thread_id == work_chat.thread_id)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::db::error::{DbError, DbResult};
|
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::store::{DbOperations, OurDbStore};
|
||||||
|
use crate::db::generic_store::{GenericStore, GetId};
|
||||||
use crate::db::tst_index::TSTIndexManager;
|
use crate::db::tst_index::TSTIndexManager;
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -8,6 +9,7 @@ use std::fmt::Debug;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
/// Represents a single database operation in a transaction
|
/// Represents a single database operation in a transaction
|
||||||
#[derive(Debug, Clone)]
|
#[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> {
|
impl<T: Model> ModelRegistration for ModelRegistrar<T> {
|
||||||
fn register(&self, path: &Path) -> DbResult<(TypeId, Arc<RwLock<dyn DbOperations>>)> {
|
fn register(&self, path: &Path) -> DbResult<(TypeId, Arc<RwLock<dyn DbOperations>>)> {
|
||||||
let store = OurDbStore::<T>::open(path.join(T::db_prefix()))?;
|
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 {
|
impl DBBuilder {
|
||||||
/// Create a new DB builder
|
/// Create a new DB builder
|
||||||
pub fn new<P: Into<PathBuf>>(base_path: P) -> Self {
|
pub fn new<P: Into<PathBuf>>(base_path: P) -> Self {
|
||||||
@ -112,6 +136,16 @@ impl DBBuilder {
|
|||||||
self
|
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
|
/// Build the DB with the registered models
|
||||||
pub fn build(self) -> Result<DB, Box<EvalAltResult>> {
|
pub fn build(self) -> Result<DB, Box<EvalAltResult>> {
|
||||||
let base_path = self.base_path;
|
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
|
/// Check the transaction state for the given type and id
|
||||||
fn check_transaction<T: Model>(&self, id: u32) -> Option<Result<Option<T>, DbError>> {
|
fn check_transaction<T: Model>(&self, id: u32) -> Option<Result<Option<T>, DbError>> {
|
||||||
// Try to acquire a read lock on the transaction
|
// 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) {
|
if let Some(tx_result) = self.check_transaction::<T>(id) {
|
||||||
match tx_result {
|
match tx_result {
|
||||||
Ok(Some(model)) => return Ok(model),
|
Ok(Some(model)) => return Ok(model),
|
||||||
|
Ok(None) => return Err(DbError::NotFound(id)),
|
||||||
Err(e) => return Err(e),
|
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>()) {
|
match self.type_map.get(&TypeId::of::<T>()) {
|
||||||
Some(db_ops) => {
|
Some(db_ops) => {
|
||||||
let mut db_ops_guard = db_ops.write().unwrap();
|
let mut db_ops_guard = db_ops.write().unwrap();
|
||||||
let result_any = db_ops_guard.get(id)?;
|
let any_result = 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>() {
|
// Try to downcast to T
|
||||||
Ok(t) => Ok(*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),
|
Err(_) => Err(DbError::TypeError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -421,17 +528,13 @@ impl DB {
|
|||||||
|
|
||||||
/// Delete a model instance by its ID and type
|
/// Delete a model instance by its ID and type
|
||||||
pub fn delete<T: Model>(&self, id: u32) -> DbResult<()> {
|
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
|
// Try to acquire a write lock on the transaction
|
||||||
let mut tx_guard = self.transaction.write().unwrap();
|
let mut tx_guard = self.transaction.write().unwrap();
|
||||||
|
|
||||||
// Check if there's an active transaction
|
// Check if there's an active transaction
|
||||||
if let Some(tx_state) = tx_guard.as_mut() {
|
if let Some(tx_state) = tx_guard.as_mut() {
|
||||||
if tx_state.active {
|
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 {
|
tx_state.operations.push(DbOperation::Delete {
|
||||||
model_type: TypeId::of::<T>(),
|
model_type: TypeId::of::<T>(),
|
||||||
id,
|
id,
|
||||||
@ -452,45 +555,48 @@ impl DB {
|
|||||||
let mut db_ops_guard = db_ops.write().unwrap();
|
let mut db_ops_guard = db_ops.write().unwrap();
|
||||||
db_ops_guard.delete(id)?;
|
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 mut tst_index = self.tst_index.write().unwrap();
|
||||||
let prefix = T::db_prefix();
|
tst_index.delete(T::db_prefix(), id)?;
|
||||||
tst_index.delete_with_indexes(prefix, id, &index_keys)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
None => Err(DbError::TypeError),
|
None => Err(DbError::TypeError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List all model instances of a specific type
|
/// Delete any serializable struct by its ID and type
|
||||||
pub fn list<T: Model>(&self) -> DbResult<Vec<T>> {
|
pub fn delete_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
|
||||||
// Get the prefix for this model type
|
&self,
|
||||||
let prefix = T::db_prefix();
|
id: u32,
|
||||||
|
prefix: &str
|
||||||
// Use the TST index to get all objects with this prefix
|
) -> DbResult<()> {
|
||||||
let mut tst_index = self.tst_index.write().unwrap();
|
// Execute directly
|
||||||
let items = tst_index.list(prefix)?;
|
match self.type_map.get(&TypeId::of::<T>()) {
|
||||||
|
Some(db_ops) => {
|
||||||
// Deserialize the objects
|
let mut db_ops_guard = db_ops.write().unwrap();
|
||||||
let mut result = Vec::with_capacity(items.len());
|
db_ops_guard.delete(id)?;
|
||||||
for (_, data) in items {
|
|
||||||
let model = T::from_bytes(&data)?;
|
// Also delete from the TST index
|
||||||
result.push(model);
|
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)
|
/// List all model instances of a given type
|
||||||
fn list_from_ourdb<T: Model>(&self) -> DbResult<Vec<T>> {
|
pub fn list<T: Model>(&self) -> DbResult<Vec<T>> {
|
||||||
match self.type_map.get(&TypeId::of::<T>()) {
|
match self.type_map.get(&TypeId::of::<T>()) {
|
||||||
Some(db_ops) => {
|
Some(db_ops) => {
|
||||||
let db_ops_guard = db_ops.read().unwrap();
|
let db_ops_guard = db_ops.read().unwrap();
|
||||||
let result_any = db_ops_guard.list()?;
|
let any_result = 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>>() {
|
// Try to downcast to Vec<T>
|
||||||
Ok(vec_t) => Ok(*vec_t),
|
match any_result.downcast::<Vec<T>>() {
|
||||||
|
Ok(boxed_vec) => Ok(*boxed_vec),
|
||||||
Err(_) => Err(DbError::TypeError),
|
Err(_) => Err(DbError::TypeError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,105 +604,43 @@ impl DB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronize the TST index with OurDB for a specific model type
|
/// List all instances of any serializable type
|
||||||
pub fn synchronize_tst_index<T: Model>(&self) -> DbResult<()> {
|
pub fn list_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
|
||||||
// Get all models from OurDB
|
&self
|
||||||
let models = self.list_from_ourdb::<T>()?;
|
) -> DbResult<Vec<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
|
|
||||||
match self.type_map.get(&TypeId::of::<T>()) {
|
match self.type_map.get(&TypeId::of::<T>()) {
|
||||||
Some(db_ops) => {
|
Some(db_ops) => {
|
||||||
let mut db_ops_guard = db_ops.write().unwrap();
|
let db_ops_guard = db_ops.read().unwrap();
|
||||||
let result_any = db_ops_guard.get_history(id, depth)?;
|
let any_result = db_ops_guard.list()?;
|
||||||
let mut result = Vec::with_capacity(result_any.len());
|
|
||||||
|
|
||||||
for item in result_any {
|
// Try to downcast to Vec<T>
|
||||||
match item.downcast::<T>() {
|
match any_result.downcast::<Vec<T>>() {
|
||||||
Ok(t) => result.push(*t),
|
Ok(boxed_vec) => Ok(*boxed_vec),
|
||||||
Err(_) => return Err(DbError::TypeError),
|
Err(_) => Err(DbError::TypeError),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
None => Err(DbError::TypeError),
|
None => Err(DbError::TypeError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register a model type with this DB instance
|
/// Get the history of a model instance
|
||||||
pub fn register<T: Model>(&mut self) -> DbResult<()> {
|
pub fn get_history<T: Model>(&self, id: u32, depth: u8) -> DbResult<Vec<T>> {
|
||||||
// Create the OurDB store
|
match self.type_map.get(&TypeId::of::<T>()) {
|
||||||
let store = OurDbStore::<T>::open(&self.db_path)?;
|
Some(db_ops) => {
|
||||||
self.type_map.insert(TypeId::of::<T>(), Arc::new(RwLock::new(store)));
|
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 results = Vec::with_capacity(any_results.len());
|
||||||
let mut tst_index = self.tst_index.write().unwrap();
|
for any_result in any_results {
|
||||||
|
match any_result.downcast::<T>() {
|
||||||
// Ensure the TST for this prefix exists
|
Ok(boxed_t) => results.push(*boxed_t),
|
||||||
tst_index.get_tst(prefix)?;
|
Err(_) => return Err(DbError::TypeError),
|
||||||
|
}
|
||||||
Ok(())
|
}
|
||||||
}
|
|
||||||
|
Ok(results)
|
||||||
/// 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),
|
|
||||||
}
|
}
|
||||||
|
None => Err(DbError::TypeError),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
140
herodb/src/db/generic_store.rs
Normal file
140
herodb/src/db/generic_store.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -4,12 +4,16 @@ pub use error::{DbError, DbResult};
|
|||||||
|
|
||||||
// Export the model module
|
// Export the model module
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub use model::{Model, Storable, IndexKey};
|
pub use model::{Model, Storable, IndexKey, GetId};
|
||||||
|
|
||||||
// Export the store module
|
// Export the store module
|
||||||
pub mod store;
|
pub mod store;
|
||||||
pub use store::{DbOperations, OurDbStore};
|
pub use store::{DbOperations, OurDbStore};
|
||||||
|
|
||||||
|
// Export the generic store module
|
||||||
|
pub mod generic_store;
|
||||||
|
pub use generic_store::GenericStore;
|
||||||
|
|
||||||
// Export the db module
|
// Export the db module
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub use db::{DB, DBBuilder, ModelRegistration, ModelRegistrar};
|
pub use db::{DB, DBBuilder, ModelRegistration, ModelRegistrar};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::db::error::{DbError, DbResult};
|
use crate::db::error::{DbError, DbResult};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
/// Trait for models that can be serialized and deserialized
|
/// 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
|
// Note: We don't provide a blanket implementation of Storable
|
||||||
// Each model type must implement Storable explicitly
|
// Each model type must implement Storable explicitly
|
@ -13,7 +13,7 @@ pub mod cmd;
|
|||||||
|
|
||||||
// Re-exports
|
// Re-exports
|
||||||
pub use error::Error;
|
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
|
/// Re-export ourdb for advanced usage
|
||||||
pub use ourdb;
|
pub use ourdb;
|
||||||
|
@ -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
|
|
@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{Model, Storable, DB, DbError, DbResult};
|
|
||||||
use crate::models::mcc::event::Event;
|
use crate::models::mcc::event::Event;
|
||||||
|
use crate::db::model::impl_get_id;
|
||||||
|
|
||||||
/// Calendar represents a calendar container for events
|
/// Calendar represents a calendar container for events
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@ -39,27 +39,18 @@ impl Calendar {
|
|||||||
groups.iter().any(|g| self.groups.contains(g))
|
groups.iter().any(|g| self.groups.contains(g))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all events associated with this calendar
|
/// Filter events by this calendar's ID
|
||||||
pub fn get_events(&self, db: &DB) -> DbResult<Vec<Event>> {
|
pub fn filter_events<'a>(&self, events: &'a [Event]) -> Vec<&'a Event> {
|
||||||
let all_events = db.list::<Event>()?;
|
events.iter()
|
||||||
let calendar_events = all_events
|
|
||||||
.into_iter()
|
|
||||||
.filter(|event| event.calendar_id == self.id)
|
.filter(|event| event.calendar_id == self.id)
|
||||||
.collect();
|
.collect()
|
||||||
|
}
|
||||||
Ok(calendar_events)
|
|
||||||
|
/// Get the database prefix for this model type
|
||||||
|
pub fn db_prefix() -> &'static str {
|
||||||
|
"calendar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Storable for Calendar{}
|
// Automatically implement GetId trait for Calendar
|
||||||
|
impl_get_id!(Calendar);
|
||||||
// Implement Model trait
|
|
||||||
impl Model for Calendar {
|
|
||||||
fn get_id(&self) -> u32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_prefix() -> &'static str {
|
|
||||||
"calendar"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{Model, Storable, DB, DbError, DbResult};
|
|
||||||
use crate::models::mcc::event::Event;
|
use crate::models::mcc::event::Event;
|
||||||
|
use crate::db::model::impl_get_id;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
/// Contact represents a contact entry in an address book
|
/// Contact represents a contact entry in an address book
|
||||||
@ -13,14 +13,12 @@ pub struct Contact {
|
|||||||
pub modified_at: i64, // Unix epoch timestamp
|
pub modified_at: i64, // Unix epoch timestamp
|
||||||
pub first_name: String,
|
pub first_name: String,
|
||||||
pub last_name: String,
|
pub last_name: String,
|
||||||
pub email: String,
|
pub emails: Vec<String>, // Changed from []String to Vec<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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Contact {
|
impl Contact {
|
||||||
/// Create a new 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();
|
let now = Utc::now().timestamp();
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
@ -28,29 +26,10 @@ impl Contact {
|
|||||||
modified_at: now,
|
modified_at: now,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
email,
|
emails : emails,
|
||||||
group,
|
|
||||||
groups: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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)
|
/// Search by name - returns true if the name contains the query (case-insensitive)
|
||||||
pub fn search_by_name(&self, query: &str) -> bool {
|
pub fn search_by_name(&self, query: &str) -> bool {
|
||||||
let full_name = self.full_name().to_lowercase();
|
let full_name = self.full_name().to_lowercase();
|
||||||
@ -62,15 +41,11 @@ impl Contact {
|
|||||||
self.email.to_lowercase().contains(&query.to_lowercase())
|
self.email.to_lowercase().contains(&query.to_lowercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get events where this contact is an attendee
|
/// Filter events where this contact is an attendee
|
||||||
pub fn get_events(&self, db: &DB) -> DbResult<Vec<Event>> {
|
pub fn filter_events<'a>(&self, events: &'a [Event]) -> Vec<&'a Event> {
|
||||||
let all_events = db.list::<Event>()?;
|
events.iter()
|
||||||
let contact_events = all_events
|
|
||||||
.into_iter()
|
|
||||||
.filter(|event| event.attendees.contains(&self.email))
|
.filter(|event| event.attendees.contains(&self.email))
|
||||||
.collect();
|
.collect()
|
||||||
|
|
||||||
Ok(contact_events)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the contact's information
|
/// Update the contact's information
|
||||||
@ -104,18 +79,12 @@ impl Contact {
|
|||||||
pub fn full_name(&self) -> String {
|
pub fn full_name(&self) -> String {
|
||||||
format!("{} {}", self.first_name, self.last_name)
|
format!("{} {}", self.first_name, self.last_name)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// Get the database prefix for this model type
|
||||||
// Implement Storable trait (provides default dump/load)
|
pub fn db_prefix() -> &'static str {
|
||||||
impl Storable for Contact {}
|
|
||||||
|
|
||||||
// Implement Model trait
|
|
||||||
impl Model for Contact {
|
|
||||||
fn get_id(&self) -> u32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_prefix() -> &'static str {
|
|
||||||
"contact"
|
"contact"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatically implement GetId trait for Contact
|
||||||
|
impl_get_id!(Contact);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{Model, Storable, DB, DbError, DbResult};
|
|
||||||
use crate::models::mcc::calendar::Calendar;
|
use crate::models::mcc::calendar::Calendar;
|
||||||
use crate::models::mcc::contacts::Contact;
|
use crate::models::mcc::contacts::Contact;
|
||||||
|
use crate::db::model::impl_get_id;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
/// EventMeta contains additional metadata for a calendar event
|
/// EventMeta contains additional metadata for a calendar event
|
||||||
@ -84,20 +84,16 @@ impl Event {
|
|||||||
groups.iter().any(|g| self.groups.contains(g))
|
groups.iter().any(|g| self.groups.contains(g))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the calendar this event belongs to
|
/// Find the calendar this event belongs to
|
||||||
pub fn get_calendar(&self, db: &DB) -> DbResult<Calendar> {
|
pub fn find_calendar<'a>(&self, calendars: &'a [Calendar]) -> Option<&'a Calendar> {
|
||||||
db.get::<Calendar>(self.calendar_id)
|
calendars.iter().find(|cal| cal.id == self.calendar_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get contacts for all attendees of this event
|
/// Filter contacts that are attendees of this event
|
||||||
pub fn get_attendee_contacts(&self, db: &DB) -> DbResult<Vec<Contact>> {
|
pub fn filter_attendee_contacts<'a>(&self, contacts: &'a [Contact]) -> Vec<&'a Contact> {
|
||||||
let all_contacts = db.list::<Contact>()?;
|
contacts.iter()
|
||||||
let attendee_contacts = all_contacts
|
|
||||||
.into_iter()
|
|
||||||
.filter(|contact| self.attendees.contains(&contact.email))
|
.filter(|contact| self.attendees.contains(&contact.email))
|
||||||
.collect();
|
.collect()
|
||||||
|
|
||||||
Ok(attendee_contacts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an attendee to this event
|
/// Add an attendee to this event
|
||||||
@ -124,18 +120,12 @@ impl Event {
|
|||||||
pub fn search_by_description(&self, query: &str) -> bool {
|
pub fn search_by_description(&self, query: &str) -> bool {
|
||||||
self.description.to_lowercase().contains(&query.to_lowercase())
|
self.description.to_lowercase().contains(&query.to_lowercase())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// Get the database prefix for this model type
|
||||||
// Implement Storable trait (provides default dump/load)
|
pub fn db_prefix() -> &'static str {
|
||||||
impl Storable for Event {}
|
|
||||||
|
|
||||||
// Implement Model trait
|
|
||||||
impl Model for Event {
|
|
||||||
fn get_id(&self) -> u32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_prefix() -> &'static str {
|
|
||||||
"event"
|
"event"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatically implement GetId trait for Event
|
||||||
|
impl_get_id!(Event);
|
@ -10,6 +10,3 @@ pub use event::{Event, EventMeta};
|
|||||||
pub use mail::{Email, Attachment, Envelope};
|
pub use mail::{Email, Attachment, Envelope};
|
||||||
pub use contacts::Contact;
|
pub use contacts::Contact;
|
||||||
pub use message::{Message, MessageMeta, MessageStatus};
|
pub use message::{Message, MessageMeta, MessageStatus};
|
||||||
|
|
||||||
// Re-export database components from db module
|
|
||||||
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult};
|
|
@ -1,23 +1,17 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{Model, Storable, DB, DbError, DbResult};
|
use crate::db::model::impl_get_id;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
/// Email represents an email message with all its metadata and content
|
/// Email represents an email message with all its metadata and content
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Email {
|
pub struct Email {
|
||||||
// Database ID
|
// Database ID
|
||||||
pub id: u32, // Database ID (assigned by DBHandler)
|
pub id: u32,
|
||||||
// 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 message: String, // The email body content
|
pub message: String, // The email body content
|
||||||
pub attachments: Vec<Attachment>, // Any file attachments
|
pub attachments: Vec<Attachment>, // Any file attachments
|
||||||
// IMAP specific fields
|
|
||||||
pub flags: Vec<String>, // IMAP flags like \Seen, \Deleted, etc.
|
pub flags: Vec<String>, // IMAP flags like \Seen, \Deleted, etc.
|
||||||
pub receivetime: i64, // Unix timestamp when the email was received
|
pub receivetime: i64, // Unix timestamp when the email was received
|
||||||
pub envelope: Option<Envelope>, // IMAP envelope information (contains From, To, Subject, etc.)
|
pub envelope: Option<Envelope>, // IMAP envelope structure
|
||||||
pub groups: Vec<u32>, // Groups this email belongs to (references Circle IDs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attachment represents an email attachment
|
/// Attachment represents an email attachment
|
||||||
@ -41,7 +35,6 @@ pub struct Envelope {
|
|||||||
pub cc: Vec<String>,
|
pub cc: Vec<String>,
|
||||||
pub bcc: Vec<String>,
|
pub bcc: Vec<String>,
|
||||||
pub in_reply_to: String,
|
pub in_reply_to: String,
|
||||||
pub message_id: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Email {
|
impl Email {
|
||||||
@ -49,15 +42,11 @@ impl Email {
|
|||||||
pub fn new(id: u32, uid: u32, seq_num: u32, mailbox: String, message: String) -> Self {
|
pub fn new(id: u32, uid: u32, seq_num: u32, mailbox: String, message: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
uid,
|
|
||||||
seq_num,
|
|
||||||
mailbox,
|
|
||||||
message,
|
message,
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(),
|
||||||
flags: Vec::new(),
|
flags: Vec::new(),
|
||||||
receivetime: chrono::Utc::now().timestamp(),
|
receivetime: chrono::Utc::now().timestamp(),
|
||||||
envelope: None,
|
envelope: None,
|
||||||
groups: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,23 +55,6 @@ impl Email {
|
|||||||
self.attachments.push(attachment);
|
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)
|
/// Search by subject - returns true if the subject contains the query (case-insensitive)
|
||||||
pub fn search_by_subject(&self, query: &str) -> bool {
|
pub fn search_by_subject(&self, query: &str) -> bool {
|
||||||
if let Some(env) = &self.envelope {
|
if let Some(env) = &self.envelope {
|
||||||
@ -146,18 +118,12 @@ impl Email {
|
|||||||
|
|
||||||
message
|
message
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// Get the database prefix for this model type
|
||||||
// Implement Storable trait (provides default dump/load)
|
pub fn db_prefix() -> &'static str {
|
||||||
impl Storable for Email {}
|
|
||||||
|
|
||||||
// Implement Model trait
|
|
||||||
impl Model for Email {
|
|
||||||
fn get_id(&self) -> u32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_prefix() -> &'static str {
|
|
||||||
"email"
|
"email"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatically implement GetId trait for Email
|
||||||
|
impl_get_id!(Email);
|
@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{Model, Storable, DB, DbError, DbResult};
|
use crate::impl_get_id;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
/// MessageStatus represents the status of a message
|
/// MessageStatus represents the status of a message
|
||||||
@ -30,7 +30,6 @@ pub struct Message {
|
|||||||
pub recipients: Vec<String>, // List of recipient identifiers
|
pub recipients: Vec<String>, // List of recipient identifiers
|
||||||
pub content: String, // Message content
|
pub content: String, // Message content
|
||||||
pub attachments: Vec<String>, // References to attachments
|
pub attachments: Vec<String>, // References to attachments
|
||||||
pub groups: Vec<u32>, // Groups this message belongs to (references Circle IDs)
|
|
||||||
pub meta: MessageMeta, // Message metadata
|
pub meta: MessageMeta, // Message metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +44,6 @@ impl Message {
|
|||||||
recipients: Vec::new(),
|
recipients: Vec::new(),
|
||||||
content,
|
content,
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(),
|
||||||
groups: Vec::new(),
|
|
||||||
meta: MessageMeta {
|
meta: MessageMeta {
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
@ -107,28 +105,18 @@ impl Message {
|
|||||||
self.meta.updated_at = Utc::now();
|
self.meta.updated_at = Utc::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all messages in the same thread
|
/// Filter messages that are in the same thread as this message
|
||||||
pub fn get_thread_messages(&self, db: &DB) -> DbResult<Vec<Message>> {
|
pub fn filter_thread_messages<'a>(&self, messages: &'a [Message]) -> Vec<&'a Message> {
|
||||||
let all_messages = db.list::<Message>()?;
|
messages.iter()
|
||||||
let thread_messages = all_messages
|
|
||||||
.into_iter()
|
|
||||||
.filter(|msg| msg.thread_id == self.thread_id)
|
.filter(|msg| msg.thread_id == self.thread_id)
|
||||||
.collect();
|
.collect()
|
||||||
|
}
|
||||||
Ok(thread_messages)
|
|
||||||
|
/// Get the database prefix for this model type
|
||||||
|
pub fn db_prefix() -> &'static str {
|
||||||
|
"message"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement Storable trait (provides default dump/load)
|
// Automatically implement GetId trait for Message
|
||||||
impl Storable for Message {}
|
impl_get_id!(Message);
|
||||||
|
|
||||||
// Implement Model trait
|
|
||||||
impl Model for Message {
|
|
||||||
fn get_id(&self) -> u32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn db_prefix() -> &'static str {
|
|
||||||
"message"
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,7 +9,4 @@ pub use calendar::Calendar;
|
|||||||
pub use event::{Event, EventMeta};
|
pub use event::{Event, EventMeta};
|
||||||
pub use mail::{Email, Attachment, Envelope};
|
pub use mail::{Email, Attachment, Envelope};
|
||||||
pub use contacts::Contact;
|
pub use contacts::Contact;
|
||||||
pub use message::{Message, MessageMeta, MessageStatus};
|
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
11
heromodels/Cargo.toml
Normal 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
63
heromodels/src/comment.rs
Normal 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
47
heromodels/src/lib.rs
Normal 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
181
heromodels/src/model.rs
Normal 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
86
heromodels/src/user.rs
Normal 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(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
161
mcc_models_standalone_plan.md
Normal file
161
mcc_models_standalone_plan.md
Normal 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
|
Loading…
Reference in New Issue
Block a user