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
|