10 KiB
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
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:
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:
pub struct IndexKey {
pub name: &'static str, // Field name
pub value: String, // Field value
}
Example: Note Object
#[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
#[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:
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
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:
- Serialize the object to JSON
- Store at
obj:<ns>:<id> - Generate index keys by calling
obj.index_keys() - Create indexes for each key at
idx:<ns>:<field>:<value> - Add to scan index at
scan:<ns>
When an object is deleted:
- Retrieve the object
- Generate index keys
- Remove from all indexes
- 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:
#[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:
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:
pub struct Note {
pub base_data: BaseData,
pub title: String,
#[relation(target = "Note", label = "references")]
pub references: Vec<String>,
}
4. Validation
Trait-based validation:
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:
// 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
- Type Safety: Compile-time guarantees for object types
- Extensibility: Easy to add new object types
- Automatic Indexing: Index keys generated from object structure
- Consistency: Same pattern as heromodels
- Flexibility: Each object type controls its own indexing logic
- 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