Files
osiris/src/store/generic_store.rs
Timur Gordon 87c556df7a wip
2025-10-29 16:52:33 +01:00

136 lines
4.2 KiB
Rust

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(&note).await.unwrap();
let retrieved: Note = store.get("test", note.id()).await.unwrap();
assert_eq!(retrieved.title, note.title);
}
}