- 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
414 lines
15 KiB
Rust
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(¬e).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(¬e).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(¬e).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"));
|
|
}
|
|
}
|