Files
osiris/src/context.rs
Timur Gordon e760a184b1 Refactor to use Rhai packages for efficient engine creation
- Created OsirisPackage with all OSIRIS types and functions registered in the package
- Functions now registered directly in package module (Note, Event, get_context)
- Created ZdfzPackage extending OsirisPackage
- Engine factory pattern: creates Engine::new_raw() + registers package (very cheap)
- Removed TypeRegistry (unused code)
- Simplified runner to use factory functions instead of passing packages
- Package is created once, then factory efficiently creates engines on demand
2025-10-28 12:20:17 +01:00

414 lines
15 KiB
Rust

/// OSIRIS Context
///
/// A complete context with HeroDB storage and participant-based access.
/// Each context is isolated with its own HeroDB connection.
///
/// Combines:
/// - HeroDB storage (via GenericStore)
/// - Participant list (public keys)
/// - Generic CRUD operations for any data
use crate::objects::Note;
use crate::store::{GenericStore, HeroDbClient};
use rhai::{CustomType, EvalAltResult, TypeBuilder};
use std::sync::Arc;
/// Convert serde_json::Value to rhai::Dynamic
fn json_to_rhai(value: serde_json::Value) -> Result<rhai::Dynamic, String> {
match value {
serde_json::Value::Null => Ok(rhai::Dynamic::UNIT),
serde_json::Value::Bool(b) => Ok(rhai::Dynamic::from(b)),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(rhai::Dynamic::from(i))
} else if let Some(f) = n.as_f64() {
Ok(rhai::Dynamic::from(f))
} else {
Err("Invalid number".to_string())
}
}
serde_json::Value::String(s) => Ok(rhai::Dynamic::from(s)),
serde_json::Value::Array(arr) => {
let rhai_arr: Result<Vec<rhai::Dynamic>, String> = arr
.into_iter()
.map(json_to_rhai)
.collect();
Ok(rhai::Dynamic::from(rhai_arr?))
}
serde_json::Value::Object(obj) => {
let mut rhai_map = rhai::Map::new();
for (k, v) in obj {
rhai_map.insert(k.into(), json_to_rhai(v)?);
}
Ok(rhai::Dynamic::from(rhai_map))
}
}
}
// ============================================================================
// OsirisContext - Main Context Type
// ============================================================================
/// OSIRIS Context - combines storage with participant-based access
///
/// This is the main context object that provides:
/// - HeroDB storage via GenericStore
/// - Participant list (public keys)
/// - Generic CRUD operations
#[derive(Clone, Debug)]
pub struct OsirisContext {
pub(crate) participants: Vec<String>, // Public keys of all participants in this context
pub(crate) store: Arc<GenericStore>,
}
// Keep OsirisInstance as an alias for backward compatibility
pub type OsirisInstance = OsirisContext;
impl OsirisContext {
/// Create a builder for OsirisContext
pub fn builder() -> OsirisContextBuilder {
OsirisContextBuilder::new()
}
/// Create a new OSIRIS context with minimal config (for backwards compatibility)
pub fn new(name: impl ToString, herodb_url: &str, db_id: u16) -> Result<Self, Box<dyn std::error::Error>> {
OsirisContextBuilder::new()
.name(name)
.herodb_url(herodb_url)
.db_id(db_id)
.build()
}
/// Get the context participants (public keys)
pub fn participants(&self) -> Vec<String> {
self.participants.clone()
}
/// Get the context ID (sorted, comma-separated participant keys)
pub fn context_id(&self) -> String {
let mut sorted = self.participants.clone();
sorted.sort();
sorted.join(",")
}
// ============================================================================
// Generic CRUD Operations
// ============================================================================
// These methods work with any Rhai Dynamic object and store in HeroDB
/// Generic save - saves any Rhai object to HeroDB
///
/// Usage in Rhai:
/// ```rhai
/// let resident = digital_resident()
/// .email("test@example.com")
/// .first_name("John");
/// let id = ctx.save("residents", "resident_123", resident);
/// ```
pub fn save(&self, collection: String, id: String, data: rhai::Dynamic) -> Result<String, Box<EvalAltResult>> {
let store = self.store.clone();
let id_clone = id.clone();
let collection_clone = collection.clone();
// Serialize Rhai object to JSON
let json_content = format!("{:?}", data); // Simple serialization for now
// Save as Note
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
let mut note = Note::new(collection_clone);
note.base_data.id = id_clone.clone();
note.content = Some(json_content);
store.put(&note).await
.map_err(|e| format!("Failed to save: {}", e))?;
Ok(id_clone)
})
}).map_err(|e: String| e.into())
}
/// Generic get - retrieves data from HeroDB and returns as Rhai object
///
/// Usage in Rhai:
/// ```rhai
/// let resident = ctx.get("residents", "resident_123");
/// print(resident); // Can use the data directly
/// ```
pub fn get(&self, collection: String, id: String) -> Result<rhai::Dynamic, Box<EvalAltResult>> {
let store = self.store.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
// Get raw JSON from HeroDB (generic)
let json_data = store.get_raw(&collection, &id).await
.map_err(|e| format!("Failed to get from HeroDB: {}", e))?;
// Parse JSON to Rhai Map
let parsed: serde_json::Value = serde_json::from_str(&json_data)
.map_err(|e| format!("Failed to parse JSON: {}", e))?;
// Convert serde_json::Value to rhai::Dynamic
json_to_rhai(parsed)
})
}).map_err(|e: String| e.into())
}
/// Generic delete - checks if exists in HeroDB and deletes
///
/// Usage in Rhai:
/// ```rhai
/// let deleted = ctx.delete("residents", "resident_123");
/// if deleted {
/// print("Deleted successfully");
/// }
/// ```
pub fn delete(&self, collection: String, id: String) -> Result<bool, Box<EvalAltResult>> {
let store = self.store.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
// Check if exists by trying to get it
match store.get::<Note>(&collection, &id).await {
Ok(note) => {
// Exists, now delete it
store.delete(&note).await
.map_err(|e| format!("Failed to delete from HeroDB: {}", e))
}
Err(_) => {
// Doesn't exist
Ok(false)
}
}
})
}).map_err(|e: String| e.into())
}
/// Check if an object exists in the context
pub fn exists(&self, collection: String, id: String) -> Result<bool, Box<EvalAltResult>> {
let store = self.store.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
// Check if exists by trying to get it
match store.get::<Note>(&collection, &id).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
})
}).map_err(|e: String| e.into())
}
/// List all IDs in a collection
pub fn list(&self, collection: String) -> Result<Vec<rhai::Dynamic>, Box<EvalAltResult>> {
let store = self.store.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
store.get_all_ids(&collection).await
.map(|ids| ids.into_iter().map(rhai::Dynamic::from).collect())
.map_err(|e| format!("Failed to list: {}", e))
})
}).map_err(|e: String| e.into())
}
/// Query objects by field value
pub fn query(&self, collection: String, field: String, value: String) -> Result<Vec<rhai::Dynamic>, Box<EvalAltResult>> {
let store = self.store.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
store.get_ids_by_index(&collection, &field, &value).await
.map(|ids| ids.into_iter().map(rhai::Dynamic::from).collect())
.map_err(|e| format!("Failed to query: {}", e))
})
}).map_err(|e: String| e.into())
}
}
impl OsirisContext {
/// Save a Note object (typed)
pub fn save_note(&self, note: Note) -> Result<String, Box<EvalAltResult>> {
let store = self.store.clone();
let id = note.base_data.id.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
store.put(&note).await
.map_err(|e| format!("Failed to save note: {}", e))?;
Ok(id)
})
}).map_err(|e: String| e.into())
}
/// Save an Event object (typed)
pub fn save_event(&self, event: crate::objects::Event) -> Result<String, Box<EvalAltResult>> {
let store = self.store.clone();
let id = event.base_data.id.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
store.put(&event).await
.map_err(|e| format!("Failed to save event: {}", e))?;
Ok(id)
})
}).map_err(|e: String| e.into())
}
}
impl CustomType for OsirisContext {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("OsirisContext")
.with_fn("participants", |ctx: &mut OsirisContext| ctx.participants())
.with_fn("context_id", |ctx: &mut OsirisContext| ctx.context_id())
// Generic CRUD (with collection name)
.with_fn("save", |ctx: &mut OsirisContext, collection: String, id: String, data: rhai::Dynamic| ctx.save(collection, id, data))
// Typed save methods (no collection name needed)
.with_fn("save", |ctx: &mut OsirisContext, note: Note| ctx.save_note(note))
.with_fn("save", |ctx: &mut OsirisContext, event: crate::objects::Event| ctx.save_event(event))
.with_fn("get", |ctx: &mut OsirisContext, collection: String, id: String| ctx.get(collection, id))
.with_fn("delete", |ctx: &mut OsirisContext, collection: String, id: String| ctx.delete(collection, id))
.with_fn("list", |ctx: &mut OsirisContext, collection: String| ctx.list(collection))
.with_fn("query", |ctx: &mut OsirisContext, collection: String, field: String, value: String| ctx.query(collection, field, value));
}
}
// ============================================================================
// OsirisContextBuilder
// ============================================================================
/// Builder for OsirisContext
pub struct OsirisContextBuilder {
participants: Option<Vec<String>>,
herodb_url: Option<String>,
db_id: Option<u16>,
}
impl OsirisContextBuilder {
/// Create a new builder
pub fn new() -> Self {
Self {
participants: None,
herodb_url: None,
db_id: None,
}
}
/// Set the context participants (public keys)
pub fn participants(mut self, participants: Vec<String>) -> Self {
self.participants = Some(participants);
self
}
/// Set a single participant (for backwards compatibility)
pub fn name(mut self, name: impl ToString) -> Self {
self.participants = Some(vec![name.to_string()]);
self
}
/// Set owner (deprecated, use participants instead)
#[deprecated(note = "Use participants() instead")]
pub fn owner(mut self, owner_id: impl ToString) -> Self {
self.participants = Some(vec![owner_id.to_string()]);
self
}
/// Set the HeroDB URL
pub fn herodb_url(mut self, url: impl ToString) -> Self {
self.herodb_url = Some(url.to_string());
self
}
/// Set the HeroDB database ID
pub fn db_id(mut self, db_id: u16) -> Self {
self.db_id = Some(db_id);
self
}
/// Build the OsirisContext
pub fn build(self) -> Result<OsirisContext, Box<dyn std::error::Error>> {
let participants = self.participants.ok_or("Context participants are required")?;
// HeroDB URL and DB ID are now optional - context can work without storage
let herodb_url = self.herodb_url.unwrap_or_else(|| "redis://localhost:6379".to_string());
let db_id = self.db_id.unwrap_or(1);
if participants.is_empty() {
return Err("At least one participant is required".into());
}
// Create HeroDB client
let client = HeroDbClient::new(&herodb_url, db_id)?;
// Create store
let store = GenericStore::new(client);
Ok(OsirisContext {
participants,
store: Arc::new(store),
})
}
}
impl Default for OsirisContextBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_creation() {
let ctx = OsirisContext::new("test_ctx", "redis://localhost:6379", 1);
assert!(ctx.is_ok());
let ctx = ctx.unwrap();
assert_eq!(ctx.participants(), vec!["test_ctx".to_string()]);
assert_eq!(ctx.context_id(), "test_ctx");
}
#[test]
fn test_builder_basic() {
let ctx = OsirisContextBuilder::new()
.participants(vec!["pk1".to_string()])
.herodb_url("redis://localhost:6379")
.db_id(1)
.build();
assert!(ctx.is_ok());
let ctx = ctx.unwrap();
assert_eq!(ctx.participants(), vec!["pk1".to_string()]);
assert_eq!(ctx.context_id(), "pk1");
}
#[test]
fn test_builder_with_multiple_participants() {
let ctx = OsirisContextBuilder::new()
.participants(vec!["pk1".to_string(), "pk2".to_string(), "pk3".to_string()])
.herodb_url("redis://localhost:6379")
.db_id(1)
.build();
assert!(ctx.is_ok());
let ctx = ctx.unwrap();
assert_eq!(ctx.participants().len(), 3);
// Context ID should be sorted
assert_eq!(ctx.context_id(), "pk1,pk2,pk3");
}
#[test]
fn test_builder_missing_participants() {
let ctx = OsirisContextBuilder::new()
.herodb_url("redis://localhost:6379")
.db_id(1)
.build();
assert!(ctx.is_err());
assert!(ctx.unwrap_err().to_string().contains("participants are required"));
}
}