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
This commit is contained in:
		
							
								
								
									
										413
									
								
								src/context.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										413
									
								
								src/context.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,413 @@
 | 
			
		||||
/// 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"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user