427 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			427 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# 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<String>,
 | 
						|
    pub size: Option<u64>,
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### 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<IndexKey>;
 | 
						|
    
 | 
						|
    /// Get list of indexed field names
 | 
						|
    fn indexed_fields() -> Vec<&'static str> where Self: Sized;
 | 
						|
    
 | 
						|
    /// Get searchable text content
 | 
						|
    fn searchable_text(&self) -> Option<String>;
 | 
						|
    
 | 
						|
    /// Serialize to JSON
 | 
						|
    fn to_json(&self) -> Result<String>;
 | 
						|
    
 | 
						|
    /// Deserialize from JSON
 | 
						|
    fn from_json(json: &str) -> Result<Self> 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<String>,
 | 
						|
    
 | 
						|
    // Searchable content (not indexed)
 | 
						|
    pub content: Option<String>,
 | 
						|
    
 | 
						|
    // Indexed tags - marked with #[index]
 | 
						|
    #[index]
 | 
						|
    pub tags: BTreeMap<String, String>,
 | 
						|
}
 | 
						|
 | 
						|
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<IndexKey> {
 | 
						|
        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<String> {
 | 
						|
        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<String>,
 | 
						|
    
 | 
						|
    #[index]
 | 
						|
    pub start_time: OffsetDateTime,
 | 
						|
    
 | 
						|
    pub end_time: OffsetDateTime,
 | 
						|
    
 | 
						|
    #[index]
 | 
						|
    pub location: Option<String>,
 | 
						|
    
 | 
						|
    #[index]
 | 
						|
    pub status: EventStatus,
 | 
						|
    
 | 
						|
    pub all_day: bool,
 | 
						|
    
 | 
						|
    #[index]
 | 
						|
    pub category: Option<String>,
 | 
						|
}
 | 
						|
 | 
						|
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<IndexKey> {
 | 
						|
        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<String> {
 | 
						|
        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<T: Object>(&self, obj: &T) -> Result<()>;
 | 
						|
    
 | 
						|
    /// Get an object by ID
 | 
						|
    pub async fn get<T: Object>(&self, ns: &str, id: &str) -> Result<T>;
 | 
						|
    
 | 
						|
    /// Delete an object
 | 
						|
    pub async fn delete<T: Object>(&self, obj: &T) -> Result<bool>;
 | 
						|
    
 | 
						|
    /// Get IDs matching an index key
 | 
						|
    pub async fn get_ids_by_index(&self, ns: &str, field: &str, value: &str) -> Result<Vec<String>>;
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### 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:<ns>:<id>              → JSON serialized object
 | 
						|
idx:<ns>:<field>:<value>   → Set of object IDs
 | 
						|
scan:<ns>                  → 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:<ns>:<id>`
 | 
						|
3. **Generate index keys** by calling `obj.index_keys()`
 | 
						|
4. **Create indexes** for each key at `idx:<ns>:<field>:<value>`
 | 
						|
5. **Add to scan index** at `scan:<ns>`
 | 
						|
 | 
						|
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<String>,
 | 
						|
    
 | 
						|
    pub content: Option<String>,
 | 
						|
    
 | 
						|
    #[index]
 | 
						|
    pub tags: BTreeMap<String, String>,
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### 2. Query Builder
 | 
						|
 | 
						|
Type-safe query builder for indexed fields:
 | 
						|
 | 
						|
```rust
 | 
						|
let results = store
 | 
						|
    .query::<Note>("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<String>,
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### 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
 |