move repos into monorepo
This commit is contained in:
91
lib/osiris/core/store/base_data.rs
Normal file
91
lib/osiris/core/store/base_data.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
/// Base data that all OSIRIS objects must include
|
||||
/// Similar to heromodels BaseModelData but adapted for OSIRIS
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct BaseData {
|
||||
/// Unique ID (auto-generated or user-assigned)
|
||||
pub id: u32,
|
||||
|
||||
/// Namespace this object belongs to
|
||||
pub ns: String,
|
||||
|
||||
/// Unix timestamp for creation time
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub created_at: OffsetDateTime,
|
||||
|
||||
/// Unix timestamp for last modification time
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub modified_at: OffsetDateTime,
|
||||
|
||||
/// Optional MIME type
|
||||
pub mime: Option<String>,
|
||||
|
||||
/// Content size in bytes
|
||||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
impl BaseData {
|
||||
/// Create new base data with ID 0 (no namespace required)
|
||||
pub fn new() -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
id: 0,
|
||||
ns: String::new(),
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
mime: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new base data with namespace
|
||||
pub fn with_ns(ns: impl ToString) -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
id: 0,
|
||||
ns: ns.to_string(),
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
mime: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new base data with specific ID
|
||||
pub fn with_id(id: u32, ns: String) -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
id,
|
||||
ns,
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
mime: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the modified timestamp
|
||||
pub fn update_modified(&mut self) {
|
||||
self.modified_at = OffsetDateTime::now_utc();
|
||||
}
|
||||
|
||||
/// Set the MIME type
|
||||
pub fn set_mime(&mut self, mime: Option<String>) {
|
||||
self.mime = mime;
|
||||
self.update_modified();
|
||||
}
|
||||
|
||||
/// Set the size
|
||||
pub fn set_size(&mut self, size: Option<u64>) {
|
||||
self.size = size;
|
||||
self.update_modified();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BaseData {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
135
lib/osiris/core/store/generic_store.rs
Normal file
135
lib/osiris/core/store/generic_store.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use crate::error::Result;
|
||||
use crate::index::FieldIndex;
|
||||
use crate::store::{HeroDbClient, Object};
|
||||
|
||||
/// Generic storage layer for OSIRIS objects
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GenericStore {
|
||||
client: HeroDbClient,
|
||||
index: FieldIndex,
|
||||
}
|
||||
|
||||
impl GenericStore {
|
||||
/// Create a new generic store
|
||||
pub fn new(client: HeroDbClient) -> Self {
|
||||
let index = FieldIndex::new(client.clone());
|
||||
Self {
|
||||
client,
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Store an object
|
||||
pub async fn put<T: Object>(&self, obj: &T) -> Result<()> {
|
||||
// Serialize object to JSON
|
||||
let json = obj.to_json()?;
|
||||
let key = format!("obj:{}:{}", obj.namespace(), obj.id());
|
||||
|
||||
// Store in HeroDB
|
||||
self.client.set(&key, &json).await?;
|
||||
|
||||
// Index the object
|
||||
self.index_object(obj).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get an object by ID
|
||||
pub async fn get<T: Object>(&self, ns: &str, id: &str) -> Result<T> {
|
||||
let key = format!("obj:{}:{}", ns, id);
|
||||
let json = self.client.get(&key).await?
|
||||
.ok_or_else(|| crate::error::Error::NotFound(format!("Object {}:{}", ns, id)))?;
|
||||
|
||||
T::from_json(&json)
|
||||
}
|
||||
|
||||
/// Get raw JSON data by ID (for generic access without type)
|
||||
pub async fn get_raw(&self, ns: &str, id: &str) -> Result<String> {
|
||||
let key = format!("obj:{}:{}", ns, id);
|
||||
self.client.get(&key).await?
|
||||
.ok_or_else(|| crate::error::Error::NotFound(format!("Object {}:{}", ns, id)))
|
||||
}
|
||||
|
||||
/// Delete an object
|
||||
pub async fn delete<T: Object>(&self, obj: &T) -> Result<bool> {
|
||||
let key = format!("obj:{}:{}", obj.namespace(), obj.id());
|
||||
|
||||
// Deindex first
|
||||
self.deindex_object(obj).await?;
|
||||
|
||||
// Delete from HeroDB
|
||||
self.client.del(&key).await
|
||||
}
|
||||
|
||||
/// Check if an object exists
|
||||
pub async fn exists(&self, ns: &str, id: &str) -> Result<bool> {
|
||||
let key = format!("obj:{}:{}", ns, id);
|
||||
self.client.exists(&key).await
|
||||
}
|
||||
|
||||
/// Index an object
|
||||
async fn index_object<T: Object>(&self, obj: &T) -> Result<()> {
|
||||
let index_keys = obj.index_keys();
|
||||
|
||||
for key in index_keys {
|
||||
let field_key = format!("idx:{}:{}:{}", obj.namespace(), key.name, key.value);
|
||||
self.client.sadd(&field_key, &obj.id().to_string()).await?;
|
||||
}
|
||||
|
||||
// Add to scan index for full-text search
|
||||
let scan_key = format!("scan:{}", obj.namespace());
|
||||
self.client.sadd(&scan_key, &obj.id().to_string()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deindex an object
|
||||
async fn deindex_object<T: Object>(&self, obj: &T) -> Result<()> {
|
||||
let index_keys = obj.index_keys();
|
||||
|
||||
for key in index_keys {
|
||||
let field_key = format!("idx:{}:{}:{}", obj.namespace(), key.name, key.value);
|
||||
self.client.srem(&field_key, &obj.id().to_string()).await?;
|
||||
}
|
||||
|
||||
// Remove from scan index
|
||||
let scan_key = format!("scan:{}", obj.namespace());
|
||||
self.client.srem(&scan_key, &obj.id().to_string()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all IDs matching an index key
|
||||
pub async fn get_ids_by_index(&self, ns: &str, field: &str, value: &str) -> Result<Vec<String>> {
|
||||
let field_key = format!("idx:{}:{}:{}", ns, field, value);
|
||||
self.client.smembers(&field_key).await
|
||||
}
|
||||
|
||||
/// Get all IDs in a namespace
|
||||
pub async fn get_all_ids(&self, ns: &str) -> Result<Vec<String>> {
|
||||
let scan_key = format!("scan:{}", ns);
|
||||
self.client.smembers(&scan_key).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::objects::Note;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_generic_store() {
|
||||
let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap();
|
||||
let store = GenericStore::new(client);
|
||||
|
||||
let note = Note::new("test".to_string())
|
||||
.set_title("Test Note")
|
||||
.set_content("This is a test");
|
||||
|
||||
store.put(¬e).await.unwrap();
|
||||
|
||||
let retrieved: Note = store.get("test", note.id()).await.unwrap();
|
||||
assert_eq!(retrieved.title, note.title);
|
||||
}
|
||||
}
|
||||
161
lib/osiris/core/store/herodb_client.rs
Normal file
161
lib/osiris/core/store/herodb_client.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::store::OsirisObject;
|
||||
use redis::aio::MultiplexedConnection;
|
||||
use redis::{AsyncCommands, Client};
|
||||
|
||||
/// HeroDB client wrapper for OSIRIS operations
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HeroDbClient {
|
||||
client: Client,
|
||||
pub db_id: u16,
|
||||
}
|
||||
|
||||
impl HeroDbClient {
|
||||
/// Create a new HeroDB client
|
||||
pub fn new(url: &str, db_id: u16) -> Result<Self> {
|
||||
let client = Client::open(url)?;
|
||||
Ok(Self { client, db_id })
|
||||
}
|
||||
|
||||
/// Get a connection to the database
|
||||
pub async fn get_connection(&self) -> Result<MultiplexedConnection> {
|
||||
let mut conn = self.client.get_multiplexed_async_connection().await?;
|
||||
|
||||
// Select the appropriate database
|
||||
if self.db_id > 0 {
|
||||
redis::cmd("SELECT")
|
||||
.arg(self.db_id)
|
||||
.query_async(&mut conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Store an object in HeroDB
|
||||
pub async fn put_object(&self, obj: &OsirisObject) -> Result<()> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let key = format!("meta:{}", obj.id);
|
||||
let value = serde_json::to_string(obj)?;
|
||||
|
||||
conn.set(&key, value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve an object from HeroDB
|
||||
pub async fn get_object(&self, id: &str) -> Result<OsirisObject> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let key = format!("meta:{}", id);
|
||||
|
||||
let value: Option<String> = conn.get(&key).await?;
|
||||
match value {
|
||||
Some(v) => {
|
||||
let obj: OsirisObject = serde_json::from_str(&v)?;
|
||||
Ok(obj)
|
||||
}
|
||||
None => Err(Error::NotFound(format!("Object not found: {}", id))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete an object from HeroDB
|
||||
pub async fn delete_object(&self, id: &str) -> Result<bool> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let key = format!("meta:{}", id);
|
||||
|
||||
let deleted: i32 = conn.del(&key).await?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
|
||||
/// Check if an object exists
|
||||
pub async fn exists(&self, id: &str) -> Result<bool> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let key = format!("meta:{}", id);
|
||||
|
||||
let exists: bool = conn.exists(&key).await?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Add an ID to a set (for field indexing)
|
||||
pub async fn sadd(&self, set_key: &str, member: &str) -> Result<()> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
conn.sadd(set_key, member).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove an ID from a set
|
||||
pub async fn srem(&self, set_key: &str, member: &str) -> Result<()> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
conn.srem(set_key, member).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all members of a set
|
||||
pub async fn smembers(&self, set_key: &str) -> Result<Vec<String>> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let members: Vec<String> = conn.smembers(set_key).await?;
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
/// Get the intersection of multiple sets
|
||||
pub async fn sinter(&self, keys: &[String]) -> Result<Vec<String>> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let members: Vec<String> = conn.sinter(keys).await?;
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
/// Get all keys matching a pattern
|
||||
pub async fn keys(&self, pattern: &str) -> Result<Vec<String>> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let keys: Vec<String> = conn.keys(pattern).await?;
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Set a key-value pair
|
||||
pub async fn set(&self, key: &str, value: &str) -> Result<()> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
conn.set(key, value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a value by key
|
||||
pub async fn get(&self, key: &str) -> Result<Option<String>> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let value: Option<String> = conn.get(key).await?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Delete a key
|
||||
pub async fn del(&self, key: &str) -> Result<bool> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let deleted: i32 = conn.del(key).await?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
|
||||
/// Get database size (number of keys)
|
||||
pub async fn dbsize(&self) -> Result<usize> {
|
||||
let mut conn = self.get_connection().await?;
|
||||
let size: usize = redis::cmd("DBSIZE").query_async(&mut conn).await?;
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Note: These tests require a running HeroDB instance
|
||||
// They are ignored by default
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_put_get_object() {
|
||||
let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap();
|
||||
let obj = OsirisObject::new("test".to_string(), Some("Hello".to_string()));
|
||||
|
||||
client.put_object(&obj).await.unwrap();
|
||||
let retrieved = client.get_object(&obj.id).await.unwrap();
|
||||
|
||||
assert_eq!(obj.id, retrieved.id);
|
||||
assert_eq!(obj.text, retrieved.text);
|
||||
}
|
||||
}
|
||||
11
lib/osiris/core/store/mod.rs
Normal file
11
lib/osiris/core/store/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod base_data;
|
||||
pub mod object_trait;
|
||||
pub mod herodb_client;
|
||||
pub mod generic_store;
|
||||
pub mod object; // Keep old implementation for backwards compat temporarily
|
||||
|
||||
pub use base_data::BaseData;
|
||||
pub use object_trait::{IndexKey, Object, Storable};
|
||||
pub use herodb_client::HeroDbClient;
|
||||
pub use generic_store::GenericStore;
|
||||
pub use object::{Metadata, OsirisObject}; // Old implementation
|
||||
160
lib/osiris/core/store/object.rs
Normal file
160
lib/osiris/core/store/object.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
/// Core OSIRIS object structure
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OsirisObject {
|
||||
/// Unique identifier (UUID or user-assigned)
|
||||
pub id: String,
|
||||
|
||||
/// Namespace (e.g., "notes", "calendar")
|
||||
pub ns: String,
|
||||
|
||||
/// Metadata
|
||||
pub meta: Metadata,
|
||||
|
||||
/// Optional plain text content
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
/// Object metadata
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
/// Optional human-readable title
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
|
||||
/// MIME type
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mime: Option<String>,
|
||||
|
||||
/// Key-value tags for categorization
|
||||
#[serde(default)]
|
||||
pub tags: BTreeMap<String, String>,
|
||||
|
||||
/// Creation timestamp
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created: OffsetDateTime,
|
||||
|
||||
/// Last update timestamp
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated: OffsetDateTime,
|
||||
|
||||
/// Content size in bytes
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
impl OsirisObject {
|
||||
/// Create a new object with generated UUID
|
||||
pub fn new(ns: String, text: Option<String>) -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
ns,
|
||||
meta: Metadata {
|
||||
title: None,
|
||||
mime: None,
|
||||
tags: BTreeMap::new(),
|
||||
created: now,
|
||||
updated: now,
|
||||
size: text.as_ref().map(|t| t.len() as u64),
|
||||
},
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new object with specific ID
|
||||
pub fn with_id(id: String, ns: String, text: Option<String>) -> Self {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Self {
|
||||
id,
|
||||
ns,
|
||||
meta: Metadata {
|
||||
title: None,
|
||||
mime: None,
|
||||
tags: BTreeMap::new(),
|
||||
created: now,
|
||||
updated: now,
|
||||
size: text.as_ref().map(|t| t.len() as u64),
|
||||
},
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the object's text content
|
||||
pub fn update_text(&mut self, text: Option<String>) {
|
||||
self.meta.updated = OffsetDateTime::now_utc();
|
||||
self.meta.size = text.as_ref().map(|t| t.len() as u64);
|
||||
self.text = text;
|
||||
}
|
||||
|
||||
/// Add or update a tag
|
||||
pub fn set_tag(&mut self, key: String, value: String) {
|
||||
self.meta.tags.insert(key, value);
|
||||
self.meta.updated = OffsetDateTime::now_utc();
|
||||
}
|
||||
|
||||
/// Remove a tag
|
||||
pub fn remove_tag(&mut self, key: &str) -> Option<String> {
|
||||
let result = self.meta.tags.remove(key);
|
||||
if result.is_some() {
|
||||
self.meta.updated = OffsetDateTime::now_utc();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Set the title
|
||||
pub fn set_title(&mut self, title: Option<String>) {
|
||||
self.meta.title = title;
|
||||
self.meta.updated = OffsetDateTime::now_utc();
|
||||
}
|
||||
|
||||
/// Set the MIME type
|
||||
pub fn set_mime(&mut self, mime: Option<String>) {
|
||||
self.meta.mime = mime;
|
||||
self.meta.updated = OffsetDateTime::now_utc();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_object() {
|
||||
let obj = OsirisObject::new("notes".to_string(), Some("Hello, world!".to_string()));
|
||||
assert_eq!(obj.ns, "notes");
|
||||
assert_eq!(obj.text, Some("Hello, world!".to_string()));
|
||||
assert_eq!(obj.meta.size, Some(13));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_text() {
|
||||
let mut obj = OsirisObject::new("notes".to_string(), Some("Initial".to_string()));
|
||||
let initial_updated = obj.meta.updated;
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
obj.update_text(Some("Updated".to_string()));
|
||||
|
||||
assert_eq!(obj.text, Some("Updated".to_string()));
|
||||
assert_eq!(obj.meta.size, Some(7));
|
||||
assert!(obj.meta.updated > initial_updated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tags() {
|
||||
let mut obj = OsirisObject::new("notes".to_string(), None);
|
||||
obj.set_tag("topic".to_string(), "rust".to_string());
|
||||
obj.set_tag("project".to_string(), "osiris".to_string());
|
||||
|
||||
assert_eq!(obj.meta.tags.get("topic"), Some(&"rust".to_string()));
|
||||
assert_eq!(obj.meta.tags.get("project"), Some(&"osiris".to_string()));
|
||||
|
||||
let removed = obj.remove_tag("topic");
|
||||
assert_eq!(removed, Some("rust".to_string()));
|
||||
assert_eq!(obj.meta.tags.get("topic"), None);
|
||||
}
|
||||
}
|
||||
113
lib/osiris/core/store/object_trait.rs
Normal file
113
lib/osiris/core/store/object_trait.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use crate::error::Result;
|
||||
use crate::store::BaseData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Represents an index key for an object field
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IndexKey {
|
||||
/// The name of the index key (field name)
|
||||
pub name: &'static str,
|
||||
|
||||
/// The value of the index key for this object instance
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl IndexKey {
|
||||
pub fn new(name: &'static str, value: impl ToString) -> Self {
|
||||
Self {
|
||||
name,
|
||||
value: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Core trait that all OSIRIS objects must implement
|
||||
/// Similar to heromodels Model trait but adapted for OSIRIS
|
||||
pub trait Object: Debug + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync {
|
||||
/// Get the object type name (used for routing/identification)
|
||||
fn object_type() -> &'static str
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Get a reference to the base data
|
||||
fn base_data(&self) -> &BaseData;
|
||||
|
||||
/// Get a mutable reference to the base data
|
||||
fn base_data_mut(&mut self) -> &mut BaseData;
|
||||
|
||||
/// Get the unique ID for this object
|
||||
fn id(&self) -> u32 {
|
||||
self.base_data().id
|
||||
}
|
||||
|
||||
/// Set the unique ID for this object
|
||||
fn set_id(&mut self, id: u32) {
|
||||
self.base_data_mut().id = id;
|
||||
}
|
||||
|
||||
/// Get the namespace for this object
|
||||
fn namespace(&self) -> &str {
|
||||
&self.base_data().ns
|
||||
}
|
||||
|
||||
/// Returns a list of index keys for this object instance
|
||||
/// These are generated from fields marked with #[index]
|
||||
/// The default implementation returns base_data indexes only
|
||||
fn index_keys(&self) -> Vec<IndexKey> {
|
||||
let base = self.base_data();
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Index MIME type if present
|
||||
if let Some(mime) = &base.mime {
|
||||
keys.push(IndexKey::new("mime", mime));
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
|
||||
/// Return a list of field names which have an index applied
|
||||
/// This should be implemented by the derive macro
|
||||
fn indexed_fields() -> Vec<&'static str>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Get the full-text searchable content for this object
|
||||
/// Override this to provide custom searchable text
|
||||
fn searchable_text(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Serialize the object to JSON
|
||||
fn to_json(&self) -> Result<String> {
|
||||
serde_json::to_string(self).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Deserialize the object from JSON
|
||||
fn from_json(json: &str) -> Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
serde_json::from_str(json).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update the modified timestamp
|
||||
fn touch(&mut self) {
|
||||
self.base_data_mut().update_modified();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for objects that can be stored in OSIRIS
|
||||
/// This is automatically implemented for all types that implement Object
|
||||
pub trait Storable: Object {
|
||||
/// Prepare the object for storage (update timestamps, etc.)
|
||||
fn prepare_for_storage(&mut self) {
|
||||
self.touch();
|
||||
}
|
||||
}
|
||||
|
||||
// Blanket implementation
|
||||
impl<T: Object> Storable for T {}
|
||||
Reference in New Issue
Block a user