# OSIRIS Architecture - Trait-Based Generic Objects ## Overview OSIRIS has been refactored to use a trait-based architecture similar to heromodels, allowing any object implementing the `Object` trait to be stored and indexed automatically based on field attributes. ## Core Concepts ### 1. BaseData Every OSIRIS object must include `BaseData`, which provides: - **id**: Unique identifier (UUID or user-assigned) - **ns**: Namespace the object belongs to - **created_at**: Creation timestamp - **modified_at**: Last modification timestamp - **mime**: Optional MIME type - **size**: Optional content size ```rust pub struct BaseData { pub id: String, pub ns: String, pub created_at: OffsetDateTime, pub modified_at: OffsetDateTime, pub mime: Option, pub size: Option, } ``` ### 2. Object Trait The `Object` trait is the core abstraction for all OSIRIS objects: ```rust pub trait Object: Debug + Clone + Serialize + Deserialize + Send + Sync { /// Get the object type name fn object_type() -> &'static str where Self: Sized; /// Get base data reference fn base_data(&self) -> &BaseData; /// Get mutable base data reference fn base_data_mut(&mut self) -> &mut BaseData; /// Get index keys for this object (auto-generated from #[index] fields) fn index_keys(&self) -> Vec; /// Get list of indexed field names fn indexed_fields() -> Vec<&'static str> where Self: Sized; /// Get searchable text content fn searchable_text(&self) -> Option; /// Serialize to JSON fn to_json(&self) -> Result; /// Deserialize from JSON fn from_json(json: &str) -> Result where Self: Sized; } ``` ### 3. IndexKey Represents an index entry for a field: ```rust pub struct IndexKey { pub name: &'static str, // Field name pub value: String, // Field value } ``` ## Example: Note Object ```rust #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Note { pub base_data: BaseData, // Indexed field - marked with #[index] #[index] pub title: Option, // Searchable content (not indexed) pub content: Option, // Indexed tags - marked with #[index] #[index] pub tags: BTreeMap, } impl Object for Note { fn object_type() -> &'static str { "note" } fn base_data(&self) -> &BaseData { &self.base_data } fn base_data_mut(&mut self) -> &mut BaseData { &mut self.base_data } fn index_keys(&self) -> Vec { let mut keys = Vec::new(); // Index title if let Some(title) = &self.title { keys.push(IndexKey::new("title", title)); } // Index tags for (key, value) in &self.tags { keys.push(IndexKey::new(&format!("tag:{}", key), value)); } keys } fn indexed_fields() -> Vec<&'static str> { vec!["title", "tags"] } fn searchable_text(&self) -> Option { let mut text = String::new(); if let Some(title) = &self.title { text.push_str(title); text.push(' '); } if let Some(content) = &self.content { text.push_str(content); } if text.is_empty() { None } else { Some(text) } } } ``` ## Example: Event Object ```rust #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Event { pub base_data: BaseData, #[index] pub title: String, pub description: Option, #[index] pub start_time: OffsetDateTime, pub end_time: OffsetDateTime, #[index] pub location: Option, #[index] pub status: EventStatus, pub all_day: bool, #[index] pub category: Option, } impl Object for Event { fn object_type() -> &'static str { "event" } fn base_data(&self) -> &BaseData { &self.base_data } fn base_data_mut(&mut self) -> &mut BaseData { &mut self.base_data } fn index_keys(&self) -> Vec { let mut keys = Vec::new(); keys.push(IndexKey::new("title", &self.title)); if let Some(location) = &self.location { keys.push(IndexKey::new("location", location)); } let status_str = match self.status { EventStatus::Draft => "draft", EventStatus::Published => "published", EventStatus::Cancelled => "cancelled", }; keys.push(IndexKey::new("status", status_str)); if let Some(category) = &self.category { keys.push(IndexKey::new("category", category)); } // Index by date for day-based queries let date_str = self.start_time.date().to_string(); keys.push(IndexKey::new("date", date_str)); keys } fn indexed_fields() -> Vec<&'static str> { vec!["title", "location", "status", "category", "start_time"] } fn searchable_text(&self) -> Option { let mut text = String::new(); text.push_str(&self.title); text.push(' '); if let Some(description) = &self.description { text.push_str(description); } Some(text) } } ``` ## Storage Layer ### GenericStore The `GenericStore` provides a type-safe storage layer for any object implementing `Object`: ```rust pub struct GenericStore { client: HeroDbClient, index: FieldIndex, } impl GenericStore { /// Store an object pub async fn put(&self, obj: &T) -> Result<()>; /// Get an object by ID pub async fn get(&self, ns: &str, id: &str) -> Result; /// Delete an object pub async fn delete(&self, obj: &T) -> Result; /// Get IDs matching an index key pub async fn get_ids_by_index(&self, ns: &str, field: &str, value: &str) -> Result>; } ``` ### Usage Example ```rust use osiris::objects::Note; use osiris::store::{GenericStore, HeroDbClient}; // Create store let client = HeroDbClient::new("redis://localhost:6379", 1)?; let store = GenericStore::new(client); // Create and store a note let note = Note::new("notes".to_string()) .set_title("My Note") .set_content("This is the content") .add_tag("topic", "rust") .add_tag("priority", "high"); store.put(¬e).await?; // Retrieve the note let retrieved: Note = store.get("notes", ¬e.id()).await?; // Search by index let ids = store.get_ids_by_index("notes", "tag:topic", "rust").await?; ``` ## Index Storage ### Keyspace Design ``` obj:: → JSON serialized object idx::: → Set of object IDs scan: → Set of all object IDs in namespace ``` ### Examples ``` obj:notes:abc123 → {"base_data":{...},"title":"My Note",...} idx:notes:title:My Note → {abc123, def456} idx:notes:tag:topic:rust → {abc123, xyz789} idx:notes:mime:text/plain → {abc123} scan:notes → {abc123, def456, xyz789} ``` ## Automatic Indexing When an object is stored: 1. **Serialize** the object to JSON 2. **Store** at `obj::` 3. **Generate index keys** by calling `obj.index_keys()` 4. **Create indexes** for each key at `idx:::` 5. **Add to scan index** at `scan:` When an object is deleted: 1. **Retrieve** the object 2. **Generate index keys** 3. **Remove** from all indexes 4. **Delete** the object ## Comparison with heromodels | Feature | heromodels | OSIRIS | |---------|-----------|--------| | Base struct | `BaseModelData` | `BaseData` | | Core trait | `Model` | `Object` | | ID type | `u32` (auto-increment) | `String` (UUID) | | Timestamps | `i64` (Unix) | `OffsetDateTime` | | Index macro | `#[index]` (derive) | Manual `index_keys()` | | Storage | OurDB/Postgres | HeroDB (Redis) | | Serialization | CBOR/JSON | JSON | ## Future Enhancements ### 1. Derive Macro for #[index] Create a proc macro to automatically generate `index_keys()` from field attributes: ```rust #[derive(Object)] pub struct Note { pub base_data: BaseData, #[index] pub title: Option, pub content: Option, #[index] pub tags: BTreeMap, } ``` ### 2. Query Builder Type-safe query builder for indexed fields: ```rust let results = store .query::("notes") .filter("tag:topic", "rust") .filter("tag:priority", "high") .limit(10) .execute() .await?; ``` ### 3. Relations Support for typed relations between objects: ```rust pub struct Note { pub base_data: BaseData, pub title: String, #[relation(target = "Note", label = "references")] pub references: Vec, } ``` ### 4. Validation Trait-based validation: ```rust pub trait Validate { fn validate(&self) -> Result<()>; } impl Validate for Note { fn validate(&self) -> Result<()> { if self.title.is_none() { return Err(Error::InvalidInput("Title required".into())); } Ok(()) } } ``` ## Migration from Old API The old `OsirisObject` API is still available for backwards compatibility: ```rust // Old API (still works) use osiris::store::OsirisObject; let obj = OsirisObject::new("notes".to_string(), Some("text".to_string())); // New API (recommended) use osiris::objects::Note; let note = Note::new("notes".to_string()) .set_title("Title") .set_content("text"); ``` ## Benefits of Trait-Based Architecture 1. **Type Safety**: Compile-time guarantees for object types 2. **Extensibility**: Easy to add new object types 3. **Automatic Indexing**: Index keys generated from object structure 4. **Consistency**: Same pattern as heromodels 5. **Flexibility**: Each object type controls its own indexing logic 6. **Testability**: Easy to mock and test individual object types ## Summary The trait-based architecture makes OSIRIS: - **More flexible**: Any type can be stored by implementing `Object` - **More consistent**: Follows heromodels patterns - **More powerful**: Automatic indexing based on object structure - **More maintainable**: Clear separation of concerns - **More extensible**: Easy to add new object types and features