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:
		@@ -1,93 +0,0 @@
 | 
				
			|||||||
# OSIRIS Type Registry Design
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Problem
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
We need applications (like ZDFZ API) to register custom types with OSIRIS so that:
 | 
					 | 
				
			||||||
1. The `save()` method can use the correct struct type instead of hardcoding `Note`
 | 
					 | 
				
			||||||
2. Each collection name maps to a specific Rust type
 | 
					 | 
				
			||||||
3. The type system properly deserializes, indexes, and stores data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Challenge
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The `Object` trait is not "dyn compatible" (object-safe) because it has:
 | 
					 | 
				
			||||||
- Associated functions (`object_type()`, `from_json()`)
 | 
					 | 
				
			||||||
- Generic methods
 | 
					 | 
				
			||||||
- Serialize/Deserialize bounds
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This means we **cannot** use `Box<dyn Object>` for dynamic dispatch.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Solution: Type Registry with Callbacks
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Instead of trying to return `Box<dyn Object>`, we use a callback-based approach:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```rust
 | 
					 | 
				
			||||||
pub struct TypeRegistry {
 | 
					 | 
				
			||||||
    // For each collection, store a function that:
 | 
					 | 
				
			||||||
    // 1. Takes JSON string
 | 
					 | 
				
			||||||
    // 2. Deserializes to the correct type
 | 
					 | 
				
			||||||
    // 3. Stores it using GenericStore
 | 
					 | 
				
			||||||
    // 4. Returns the ID
 | 
					 | 
				
			||||||
    savers: HashMap<String, Box<dyn Fn(&GenericStore, &str, &str) -> Result<()>>>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Usage in ZDFZ API:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```rust
 | 
					 | 
				
			||||||
// Create registry
 | 
					 | 
				
			||||||
let registry = TypeRegistry::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Register Resident type
 | 
					 | 
				
			||||||
registry.register_saver("residents", |store, id, json| {
 | 
					 | 
				
			||||||
    let mut resident: Resident = serde_json::from_str(json)?;
 | 
					 | 
				
			||||||
    resident.set_id(id);
 | 
					 | 
				
			||||||
    store.put(&resident).await
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Register Company type  
 | 
					 | 
				
			||||||
registry.register_saver("companies", |store, id, json| {
 | 
					 | 
				
			||||||
    let mut company: Company = serde_json::from_str(json)?;
 | 
					 | 
				
			||||||
    company.set_id(id);
 | 
					 | 
				
			||||||
    store.put(&company).await
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Create OSIRIS context with registry
 | 
					 | 
				
			||||||
let ctx = OsirisContext::new_with_registry(
 | 
					 | 
				
			||||||
    "my_context",
 | 
					 | 
				
			||||||
    "owner_id", 
 | 
					 | 
				
			||||||
    herodb_url,
 | 
					 | 
				
			||||||
    db_id,
 | 
					 | 
				
			||||||
    Some(Arc::new(registry))
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Now save() uses the registered type!
 | 
					 | 
				
			||||||
ctx.save("residents", "id123", resident_json)?;
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Benefits:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
✅ **Type-safe** - Each collection uses its proper Rust type
 | 
					 | 
				
			||||||
✅ **Flexible** - Applications register their own types
 | 
					 | 
				
			||||||
✅ **No trait object issues** - Uses closures instead of `Box<dyn Object>`
 | 
					 | 
				
			||||||
✅ **Proper indexing** - Each type's `index_keys()` method is called
 | 
					 | 
				
			||||||
✅ **Clean API** - Simple registration interface
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Implementation Plan:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. ✅ Create `TypeRegistry` with callback-based savers
 | 
					 | 
				
			||||||
2. ✅ Add `set_registry()` to `GenericStore`
 | 
					 | 
				
			||||||
3. ✅ Update `OsirisContext::save()` to use registry if available
 | 
					 | 
				
			||||||
4. ✅ Fall back to `Note` if no registry or collection not registered
 | 
					 | 
				
			||||||
5. Document usage for ZDFZ API
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Next Steps:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The type registry infrastructure is in place. Now ZDFZ API can:
 | 
					 | 
				
			||||||
1. Create a `TypeRegistry`
 | 
					 | 
				
			||||||
2. Register all SDK model types
 | 
					 | 
				
			||||||
3. Pass registry when creating OSIRIS contexts
 | 
					 | 
				
			||||||
4. Use generic `save()` method with proper types!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
---
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Status:** Design complete, ready for implementation with callback approach.
 | 
					 | 
				
			||||||
@@ -13,7 +13,7 @@
 | 
				
			|||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use clap::Parser;
 | 
					use clap::Parser;
 | 
				
			||||||
use osiris::rhai::{OsirisEngineConfig, create_osiris_engine_with_config};
 | 
					use osiris::{create_osiris_engine, OsirisContext};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Parser, Debug)]
 | 
					#[derive(Parser, Debug)]
 | 
				
			||||||
#[command(author, version, about = "OSIRIS Rhai Script Runner", long_about = None)]
 | 
					#[command(author, version, about = "OSIRIS Rhai Script Runner", long_about = None)]
 | 
				
			||||||
@@ -36,11 +36,6 @@ struct Args {
 | 
				
			|||||||
    /// Script file to execute
 | 
					    /// Script file to execute
 | 
				
			||||||
    #[arg(short = 'f', long)]
 | 
					    #[arg(short = 'f', long)]
 | 
				
			||||||
    script_file: Option<String>,
 | 
					    script_file: Option<String>,
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Predefined instances in format: name:url:db_id (can be repeated)
 | 
					 | 
				
			||||||
    /// Example: --instance freezone:redis://localhost:6379:1
 | 
					 | 
				
			||||||
    #[arg(short = 'i', long = "instance")]
 | 
					 | 
				
			||||||
    instances: Vec<String>,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
					fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
				
			||||||
@@ -52,39 +47,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
				
			|||||||
    println!("🚀 OSIRIS Runner");
 | 
					    println!("🚀 OSIRIS Runner");
 | 
				
			||||||
    println!("Runner ID: {}", args.runner_id);
 | 
					    println!("Runner ID: {}", args.runner_id);
 | 
				
			||||||
    println!("HeroDB: {} (DB {})", args.redis_url, args.db_id);
 | 
					    println!("HeroDB: {} (DB {})", args.redis_url, args.db_id);
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Parse predefined instances
 | 
					 | 
				
			||||||
    let mut config = OsirisEngineConfig::new();
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if args.instances.is_empty() {
 | 
					 | 
				
			||||||
        // No predefined instances, use default with runner_id as owner
 | 
					 | 
				
			||||||
        config.add_context("default", &args.runner_id, &args.redis_url, args.db_id);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        // Parse instance definitions (format: name:url:db_id)
 | 
					 | 
				
			||||||
        // We need to split carefully since URL contains colons
 | 
					 | 
				
			||||||
        for instance_def in &args.instances {
 | 
					 | 
				
			||||||
            // Find the first colon (name separator)
 | 
					 | 
				
			||||||
            let first_colon = instance_def.find(':')
 | 
					 | 
				
			||||||
                .ok_or_else(|| format!("Invalid instance format: '{}'. Expected: name:url:db_id", instance_def))?;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let name = &instance_def[..first_colon];
 | 
					 | 
				
			||||||
            let rest = &instance_def[first_colon + 1..];
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            // Find the last colon (db_id separator)
 | 
					 | 
				
			||||||
            let last_colon = rest.rfind(':')
 | 
					 | 
				
			||||||
                .ok_or_else(|| format!("Invalid instance format: '{}'. Expected: name:url:db_id", instance_def))?;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let url = &rest[..last_colon];
 | 
					 | 
				
			||||||
            let db_id_str = &rest[last_colon + 1..];
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let db_id: u16 = db_id_str.parse()
 | 
					 | 
				
			||||||
                .map_err(|_| format!("Invalid db_id in instance '{}': {}", instance_def, db_id_str))?;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            config.add_context(name, &args.runner_id, url, db_id);
 | 
					 | 
				
			||||||
            println!("  Context: {} → {} (DB {})", name, url, db_id);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    println!();
 | 
					    println!();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Determine script source
 | 
					    // Determine script source
 | 
				
			||||||
@@ -100,8 +62,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
				
			|||||||
    println!("📝 Executing script...\n");
 | 
					    println!("📝 Executing script...\n");
 | 
				
			||||||
    println!("─────────────────────────────────────");
 | 
					    println!("─────────────────────────────────────");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Create engine with predefined contexts
 | 
					    // Create engine
 | 
				
			||||||
    let (engine, mut scope) = create_osiris_engine_with_config(config)?;
 | 
					    let mut engine = create_osiris_engine()?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Set up context tags with SIGNATORIES
 | 
				
			||||||
 | 
					    let mut tag_map = rhai::Map::new();
 | 
				
			||||||
 | 
					    let signatories: rhai::Array = vec![rhai::Dynamic::from(args.runner_id.clone())];
 | 
				
			||||||
 | 
					    tag_map.insert("SIGNATORIES".into(), rhai::Dynamic::from(signatories));
 | 
				
			||||||
 | 
					    tag_map.insert("HERODB_URL".into(), rhai::Dynamic::from(args.redis_url.clone()));
 | 
				
			||||||
 | 
					    tag_map.insert("DB_ID".into(), rhai::Dynamic::from(args.db_id as i64));
 | 
				
			||||||
 | 
					    engine.set_default_tag(rhai::Dynamic::from(tag_map));
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let mut scope = rhai::Scope::new();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    match engine.eval_with_scope::<rhai::Dynamic>(&mut scope, &script_content) {
 | 
					    match engine.eval_with_scope::<rhai::Dynamic>(&mut scope, &script_content) {
 | 
				
			||||||
        Ok(result) => {
 | 
					        Ok(result) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
/// OSIRIS Context for Rhai
 | 
					/// OSIRIS Context
 | 
				
			||||||
/// 
 | 
					/// 
 | 
				
			||||||
/// A complete context with HeroDB storage and participant-based access.
 | 
					/// A complete context with HeroDB storage and participant-based access.
 | 
				
			||||||
/// Each context is isolated with its own HeroDB connection.
 | 
					/// Each context is isolated with its own HeroDB connection.
 | 
				
			||||||
@@ -8,9 +8,8 @@
 | 
				
			|||||||
/// - Participant list (public keys)
 | 
					/// - Participant list (public keys)
 | 
				
			||||||
/// - Generic CRUD operations for any data
 | 
					/// - Generic CRUD operations for any data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use super::builder::OsirisContextBuilder;
 | 
					 | 
				
			||||||
use crate::objects::Note;
 | 
					use crate::objects::Note;
 | 
				
			||||||
use crate::store::GenericStore;
 | 
					use crate::store::{GenericStore, HeroDbClient};
 | 
				
			||||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
 | 
					use rhai::{CustomType, EvalAltResult, TypeBuilder};
 | 
				
			||||||
use std::sync::Arc;
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -46,6 +45,10 @@ fn json_to_rhai(value: serde_json::Value) -> Result<rhai::Dynamic, String> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					// OsirisContext - Main Context Type
 | 
				
			||||||
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// OSIRIS Context - combines storage with participant-based access
 | 
					/// OSIRIS Context - combines storage with participant-based access
 | 
				
			||||||
/// 
 | 
					/// 
 | 
				
			||||||
/// This is the main context object that provides:
 | 
					/// This is the main context object that provides:
 | 
				
			||||||
@@ -110,17 +113,7 @@ impl OsirisContext {
 | 
				
			|||||||
        // Serialize Rhai object to JSON
 | 
					        // Serialize Rhai object to JSON
 | 
				
			||||||
        let json_content = format!("{:?}", data); // Simple serialization for now
 | 
					        let json_content = format!("{:?}", data); // Simple serialization for now
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Check if we have a type registry for this collection
 | 
					        // Save as Note
 | 
				
			||||||
        if let Some(registry) = store.type_registry() {
 | 
					 | 
				
			||||||
            if registry.has_type(&collection) {
 | 
					 | 
				
			||||||
                // Use the registry's generic save (which will call store.put with the correct type)
 | 
					 | 
				
			||||||
                registry.save(&store, &collection, &id_clone, &json_content)
 | 
					 | 
				
			||||||
                    .map_err(|e| format!("Failed to save using registry: {}", e))?;
 | 
					 | 
				
			||||||
                return Ok(id_clone);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Fall back to Note if no registry or no saver registered
 | 
					 | 
				
			||||||
        tokio::task::block_in_place(|| {
 | 
					        tokio::task::block_in_place(|| {
 | 
				
			||||||
            tokio::runtime::Handle::current().block_on(async move {
 | 
					            tokio::runtime::Handle::current().block_on(async move {
 | 
				
			||||||
                let mut note = Note::new(collection_clone);
 | 
					                let mut note = Note::new(collection_clone);
 | 
				
			||||||
@@ -282,60 +275,86 @@ impl CustomType for OsirisContext {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ============================================================================
 | 
					// ============================================================================
 | 
				
			||||||
// Context Creation - Standalone function
 | 
					// OsirisContextBuilder
 | 
				
			||||||
// ============================================================================
 | 
					// ============================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Register get_context function in a Rhai engine with signatory-based access control
 | 
					/// Builder for OsirisContext
 | 
				
			||||||
/// 
 | 
					pub struct OsirisContextBuilder {
 | 
				
			||||||
/// Simple logic:
 | 
					    participants: Option<Vec<String>>,
 | 
				
			||||||
/// - Context is a list of public keys (participants)
 | 
					    herodb_url: Option<String>,
 | 
				
			||||||
/// - To get_context, at least one participant must be a signatory
 | 
					    db_id: Option<u16>,
 | 
				
			||||||
/// - No state tracking, no caching - creates fresh context each time
 | 
					}
 | 
				
			||||||
pub fn register_context_api(engine: &mut rhai::Engine, herodb_url: String, base_db_id: u16) {
 | 
					 | 
				
			||||||
    // Register get_context function with signatory-based access control
 | 
					 | 
				
			||||||
    // Usage: get_context(['pk1', 'pk2', 'pk3'])
 | 
					 | 
				
			||||||
    engine.register_fn("get_context", move |context: rhai::NativeCallContext, participants: rhai::Array| -> Result<OsirisContext, Box<rhai::EvalAltResult>> {
 | 
					 | 
				
			||||||
        // Extract SIGNATORIES from context tag
 | 
					 | 
				
			||||||
        let tag_map = context
 | 
					 | 
				
			||||||
            .tag()
 | 
					 | 
				
			||||||
            .and_then(|tag| tag.read_lock::<rhai::Map>())
 | 
					 | 
				
			||||||
            .ok_or_else(|| Box::new(rhai::EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let signatories_dynamic = tag_map.get("SIGNATORIES")
 | 
					impl OsirisContextBuilder {
 | 
				
			||||||
            .ok_or_else(|| Box::new(rhai::EvalAltResult::ErrorRuntime("'SIGNATORIES' not found in context tag Map.".into(), context.position())))?;
 | 
					    /// Create a new builder
 | 
				
			||||||
        
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
        // Convert SIGNATORIES array to Vec<String>
 | 
					        Self {
 | 
				
			||||||
        let signatories_array = signatories_dynamic.clone().into_array()
 | 
					            participants: None,
 | 
				
			||||||
            .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SIGNATORIES must be an array: {}", e).into(), context.position())))?;
 | 
					            herodb_url: None,
 | 
				
			||||||
        
 | 
					            db_id: None,
 | 
				
			||||||
        let signatories: Vec<String> = signatories_array.into_iter()
 | 
					        }
 | 
				
			||||||
            .map(|s| s.into_string())
 | 
					 | 
				
			||||||
            .collect::<Result<Vec<_>, _>>()
 | 
					 | 
				
			||||||
            .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SIGNATORIES must contain strings: {}", e).into(), context.position())))?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Convert participants array to Vec<String>
 | 
					 | 
				
			||||||
        let participant_keys: Vec<String> = participants.into_iter()
 | 
					 | 
				
			||||||
            .map(|p| p.into_string())
 | 
					 | 
				
			||||||
            .collect::<Result<Vec<_>, _>>()
 | 
					 | 
				
			||||||
            .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("Participants must be strings: {}", e).into(), context.position())))?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Verify at least one participant is a signatory
 | 
					 | 
				
			||||||
        let has_signatory = participant_keys.iter().any(|p| signatories.contains(p));
 | 
					 | 
				
			||||||
        if !has_signatory {
 | 
					 | 
				
			||||||
            return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
 | 
					 | 
				
			||||||
                format!("Access denied: none of the participants are signatories").into(),
 | 
					 | 
				
			||||||
                context.position()
 | 
					 | 
				
			||||||
            )));
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
        // Create context directly with participants
 | 
					    /// Set the context participants (public keys)
 | 
				
			||||||
        OsirisContext::builder()
 | 
					    pub fn participants(mut self, participants: Vec<String>) -> Self {
 | 
				
			||||||
            .participants(participant_keys)
 | 
					        self.participants = Some(participants);
 | 
				
			||||||
            .herodb_url(&herodb_url)
 | 
					        self
 | 
				
			||||||
            .db_id(base_db_id)
 | 
					    }
 | 
				
			||||||
            .build()
 | 
					    
 | 
				
			||||||
            .map_err(|e| format!("Failed to create context: {}", e).into())
 | 
					    /// 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)]
 | 
					#[cfg(test)]
 | 
				
			||||||
@@ -353,40 +372,42 @@ mod tests {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    #[test]
 | 
					    #[test]
 | 
				
			||||||
    fn test_context_save_and_get() {
 | 
					    fn test_builder_basic() {
 | 
				
			||||||
        let ctx = OsirisContext::new("test_ctx", "redis://localhost:6379", 1).unwrap();
 | 
					        let ctx = OsirisContextBuilder::new()
 | 
				
			||||||
 | 
					            .participants(vec!["pk1".to_string()])
 | 
				
			||||||
 | 
					            .herodb_url("redis://localhost:6379")
 | 
				
			||||||
 | 
					            .db_id(1)
 | 
				
			||||||
 | 
					            .build();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Create a simple Rhai map
 | 
					        assert!(ctx.is_ok());
 | 
				
			||||||
        let mut map = rhai::Map::new();
 | 
					        let ctx = ctx.unwrap();
 | 
				
			||||||
        map.insert("title".into(), rhai::Dynamic::from("Test Note"));
 | 
					        assert_eq!(ctx.participants(), vec!["pk1".to_string()]);
 | 
				
			||||||
        map.insert("content".into(), rhai::Dynamic::from("Test content"));
 | 
					        assert_eq!(ctx.context_id(), "pk1");
 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Save the data
 | 
					 | 
				
			||||||
        let result = ctx.save("notes".to_string(), "note1".to_string(), rhai::Dynamic::from(map));
 | 
					 | 
				
			||||||
        assert!(result.is_ok());
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Get the data back
 | 
					 | 
				
			||||||
        let retrieved = ctx.get("notes".to_string(), "note1".to_string());
 | 
					 | 
				
			||||||
        assert!(retrieved.is_ok());
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    #[test]
 | 
					    #[test]
 | 
				
			||||||
    fn test_context_delete() {
 | 
					    fn test_builder_with_multiple_participants() {
 | 
				
			||||||
        let ctx = OsirisContext::new("test_ctx", "redis://localhost:6379", 1).unwrap();
 | 
					        let ctx = OsirisContextBuilder::new()
 | 
				
			||||||
 | 
					            .participants(vec!["pk1".to_string(), "pk2".to_string(), "pk3".to_string()])
 | 
				
			||||||
 | 
					            .herodb_url("redis://localhost:6379")
 | 
				
			||||||
 | 
					            .db_id(1)
 | 
				
			||||||
 | 
					            .build();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Save data
 | 
					        assert!(ctx.is_ok());
 | 
				
			||||||
        let mut map = rhai::Map::new();
 | 
					        let ctx = ctx.unwrap();
 | 
				
			||||||
        map.insert("title".into(), rhai::Dynamic::from("Test"));
 | 
					        assert_eq!(ctx.participants().len(), 3);
 | 
				
			||||||
        ctx.save("notes".to_string(), "note1".to_string(), rhai::Dynamic::from(map)).unwrap();
 | 
					        // Context ID should be sorted
 | 
				
			||||||
        
 | 
					        assert_eq!(ctx.context_id(), "pk1,pk2,pk3");
 | 
				
			||||||
        // Delete it
 | 
					 | 
				
			||||||
        let deleted = ctx.delete("notes".to_string(), "note1".to_string());
 | 
					 | 
				
			||||||
        assert!(deleted.is_ok());
 | 
					 | 
				
			||||||
        assert!(deleted.unwrap());
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Should not be able to get it anymore
 | 
					 | 
				
			||||||
        let result = ctx.get("notes".to_string(), "note1".to_string());
 | 
					 | 
				
			||||||
        assert!(result.is_err());
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    #[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"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										262
									
								
								src/engine.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								src/engine.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,262 @@
 | 
				
			|||||||
 | 
					/// OSIRIS Rhai Engine
 | 
				
			||||||
 | 
					/// 
 | 
				
			||||||
 | 
					/// Creates a Rhai engine configured with OSIRIS contexts and methods.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::context::OsirisContext;
 | 
				
			||||||
 | 
					use crate::objects::{Note, Event};
 | 
				
			||||||
 | 
					use rhai::{Engine, Module, def_package, FuncRegistration};
 | 
				
			||||||
 | 
					use rhai::packages::{Package, StandardPackage};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Register Note functions into a module
 | 
				
			||||||
 | 
					fn register_note_functions(module: &mut Module) {
 | 
				
			||||||
 | 
					    // Register Note type
 | 
				
			||||||
 | 
					    module.set_custom_type::<Note>("Note");
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Register builder-style constructor
 | 
				
			||||||
 | 
					    FuncRegistration::new("note")
 | 
				
			||||||
 | 
					        .set_into_module(module, |ns: String| Note::new(ns));
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Register chainable methods that return Self
 | 
				
			||||||
 | 
					    FuncRegistration::new("title")
 | 
				
			||||||
 | 
					        .set_into_module(module, |mut note: Note, title: String| {
 | 
				
			||||||
 | 
					            note.title = Some(title);
 | 
				
			||||||
 | 
					            note.base_data.update_modified();
 | 
				
			||||||
 | 
					            note
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    FuncRegistration::new("content")
 | 
				
			||||||
 | 
					        .set_into_module(module, |mut note: Note, content: String| {
 | 
				
			||||||
 | 
					            let size = content.len() as u64;
 | 
				
			||||||
 | 
					            note.content = Some(content);
 | 
				
			||||||
 | 
					            note.base_data.set_size(Some(size));
 | 
				
			||||||
 | 
					            note.base_data.update_modified();
 | 
				
			||||||
 | 
					            note
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    FuncRegistration::new("tag")
 | 
				
			||||||
 | 
					        .set_into_module(module, |mut note: Note, key: String, value: String| {
 | 
				
			||||||
 | 
					            note.tags.insert(key, value);
 | 
				
			||||||
 | 
					            note.base_data.update_modified();
 | 
				
			||||||
 | 
					            note
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    FuncRegistration::new("mime")
 | 
				
			||||||
 | 
					        .set_into_module(module, |mut note: Note, mime: String| {
 | 
				
			||||||
 | 
					            note.base_data.set_mime(Some(mime));
 | 
				
			||||||
 | 
					            note
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Register Event functions into a module
 | 
				
			||||||
 | 
					fn register_event_functions(module: &mut Module) {
 | 
				
			||||||
 | 
					    // Register Event type
 | 
				
			||||||
 | 
					    module.set_custom_type::<Event>("Event");
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Register builder-style constructor
 | 
				
			||||||
 | 
					    FuncRegistration::new("event")
 | 
				
			||||||
 | 
					        .set_into_module(module, |ns: String, title: String| Event::new(ns, title));
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Register chainable methods
 | 
				
			||||||
 | 
					    FuncRegistration::new("description")
 | 
				
			||||||
 | 
					        .set_into_module(module, |mut event: Event, desc: String| {
 | 
				
			||||||
 | 
					            event.description = Some(desc);
 | 
				
			||||||
 | 
					            event.base_data.update_modified();
 | 
				
			||||||
 | 
					            event
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Register get_context function in a Rhai engine with signatory-based access control
 | 
				
			||||||
 | 
					/// 
 | 
				
			||||||
 | 
					/// Simple logic:
 | 
				
			||||||
 | 
					/// - Context is a list of public keys (participants)
 | 
				
			||||||
 | 
					/// - To get_context, at least one participant must be a signatory
 | 
				
			||||||
 | 
					/// - No state tracking, no caching - creates fresh context each time
 | 
				
			||||||
 | 
					pub fn register_context_api(engine: &mut rhai::Engine) {
 | 
				
			||||||
 | 
					    // Register get_context function with signatory-based access control
 | 
				
			||||||
 | 
					    // Usage: get_context(['pk1', 'pk2', 'pk3'])
 | 
				
			||||||
 | 
					    engine.register_fn("get_context", move |context: rhai::NativeCallContext, participants: rhai::Array| -> Result<OsirisContext, Box<rhai::EvalAltResult>> {
 | 
				
			||||||
 | 
					        // Extract SIGNATORIES from context tag
 | 
				
			||||||
 | 
					        let tag_map = context
 | 
				
			||||||
 | 
					            .tag()
 | 
				
			||||||
 | 
					            .and_then(|tag| tag.read_lock::<rhai::Map>())
 | 
				
			||||||
 | 
					            .ok_or_else(|| Box::new(rhai::EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let signatories_dynamic = tag_map.get("SIGNATORIES")
 | 
				
			||||||
 | 
					            .ok_or_else(|| Box::new(rhai::EvalAltResult::ErrorRuntime("'SIGNATORIES' not found in context tag Map.".into(), context.position())))?;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Convert SIGNATORIES array to Vec<String>
 | 
				
			||||||
 | 
					        let signatories_array = signatories_dynamic.clone().into_array()
 | 
				
			||||||
 | 
					            .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SIGNATORIES must be an array: {}", e).into(), context.position())))?;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let signatories: Vec<String> = signatories_array.into_iter()
 | 
				
			||||||
 | 
					            .map(|s| s.into_string())
 | 
				
			||||||
 | 
					            .collect::<Result<Vec<_>, _>>()
 | 
				
			||||||
 | 
					            .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SIGNATORIES must contain strings: {}", e).into(), context.position())))?;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Convert participants array to Vec<String>
 | 
				
			||||||
 | 
					        let participant_keys: Vec<String> = participants.into_iter()
 | 
				
			||||||
 | 
					            .map(|p| p.into_string())
 | 
				
			||||||
 | 
					            .collect::<Result<Vec<_>, _>>()
 | 
				
			||||||
 | 
					            .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("Participants must be strings: {}", e).into(), context.position())))?;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Verify at least one participant is a signatory
 | 
				
			||||||
 | 
					        let has_signatory = participant_keys.iter().any(|p| signatories.contains(p));
 | 
				
			||||||
 | 
					        if !has_signatory {
 | 
				
			||||||
 | 
					            return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
 | 
				
			||||||
 | 
					                format!("Access denied: none of the participants are signatories").into(),
 | 
				
			||||||
 | 
					                context.position()
 | 
				
			||||||
 | 
					            )));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Create context directly with participants
 | 
				
			||||||
 | 
					        OsirisContext::builder()
 | 
				
			||||||
 | 
					            .participants(participant_keys)
 | 
				
			||||||
 | 
					            .build()
 | 
				
			||||||
 | 
					            .map_err(|e| format!("Failed to create context: {}", e).into())
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Define the OSIRIS package
 | 
				
			||||||
 | 
					def_package! {
 | 
				
			||||||
 | 
					    /// OSIRIS package with all OSIRIS types and functions
 | 
				
			||||||
 | 
					    pub OsirisPackage(module) : StandardPackage {
 | 
				
			||||||
 | 
					        // Register OsirisContext type
 | 
				
			||||||
 | 
					        module.set_custom_type::<OsirisContext>("OsirisContext");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Register Note functions
 | 
				
			||||||
 | 
					        register_note_functions(module);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Register Event functions
 | 
				
			||||||
 | 
					        register_event_functions(module);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Register get_context function with signatory-based access control
 | 
				
			||||||
 | 
					        FuncRegistration::new("get_context")
 | 
				
			||||||
 | 
					            .set_into_module(module, |context: rhai::NativeCallContext, participants: rhai::Array| -> Result<OsirisContext, Box<rhai::EvalAltResult>> {
 | 
				
			||||||
 | 
					                // Extract SIGNATORIES from context tag
 | 
				
			||||||
 | 
					                let tag_map = context
 | 
				
			||||||
 | 
					                    .tag()
 | 
				
			||||||
 | 
					                    .and_then(|tag| tag.read_lock::<rhai::Map>())
 | 
				
			||||||
 | 
					                    .ok_or_else(|| Box::new(rhai::EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                let signatories_dynamic = tag_map.get("SIGNATORIES")
 | 
				
			||||||
 | 
					                    .ok_or_else(|| Box::new(rhai::EvalAltResult::ErrorRuntime("'SIGNATORIES' not found in context tag Map.".into(), context.position())))?;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Convert SIGNATORIES array to Vec<String>
 | 
				
			||||||
 | 
					                let signatories_array = signatories_dynamic.clone().into_array()
 | 
				
			||||||
 | 
					                    .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SIGNATORIES must be an array: {}", e).into(), context.position())))?;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                let signatories: Vec<String> = signatories_array.into_iter()
 | 
				
			||||||
 | 
					                    .map(|s| s.into_string())
 | 
				
			||||||
 | 
					                    .collect::<Result<Vec<_>, _>>()
 | 
				
			||||||
 | 
					                    .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("SIGNATORIES must contain strings: {}", e).into(), context.position())))?;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Convert participants array to Vec<String>
 | 
				
			||||||
 | 
					                let participant_keys: Vec<String> = participants.into_iter()
 | 
				
			||||||
 | 
					                    .map(|p| p.into_string())
 | 
				
			||||||
 | 
					                    .collect::<Result<Vec<_>, _>>()
 | 
				
			||||||
 | 
					                    .map_err(|e| Box::new(rhai::EvalAltResult::ErrorRuntime(format!("Participants must be strings: {}", e).into(), context.position())))?;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Verify at least one participant is a signatory
 | 
				
			||||||
 | 
					                let has_signatory = participant_keys.iter().any(|p| signatories.contains(p));
 | 
				
			||||||
 | 
					                if !has_signatory {
 | 
				
			||||||
 | 
					                    return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
 | 
				
			||||||
 | 
					                        format!("Access denied: none of the participants are signatories").into(),
 | 
				
			||||||
 | 
					                        context.position()
 | 
				
			||||||
 | 
					                    )));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Create context directly with participants
 | 
				
			||||||
 | 
					                OsirisContext::builder()
 | 
				
			||||||
 | 
					                    .participants(participant_keys)
 | 
				
			||||||
 | 
					                    .build()
 | 
				
			||||||
 | 
					                    .map_err(|e| format!("Failed to create context: {}", e).into())
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a single OSIRIS engine (for backward compatibility)
 | 
				
			||||||
 | 
					pub fn create_osiris_engine() -> Result<Engine, Box<dyn std::error::Error>> {
 | 
				
			||||||
 | 
					    let mut engine = Engine::new_raw();
 | 
				
			||||||
 | 
					    let package = OsirisPackage::new();
 | 
				
			||||||
 | 
					    package.register_into_engine(&mut engine);
 | 
				
			||||||
 | 
					    Ok(engine)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					    use super::*;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn test_create_osiris_engine() {
 | 
				
			||||||
 | 
					        let result = create_osiris_engine();
 | 
				
			||||||
 | 
					        assert!(result.is_ok());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let mut engine = result.unwrap();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set up context tags with SIGNATORIES (like in runner_rust example)
 | 
				
			||||||
 | 
					        let mut tag_map = rhai::Map::new();
 | 
				
			||||||
 | 
					        // Create a proper Rhai array
 | 
				
			||||||
 | 
					        let signatories: rhai::Array = vec![
 | 
				
			||||||
 | 
					            rhai::Dynamic::from("pk1".to_string()),
 | 
				
			||||||
 | 
					            rhai::Dynamic::from("pk2".to_string()),
 | 
				
			||||||
 | 
					            rhai::Dynamic::from("pk3".to_string()),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        tag_map.insert("SIGNATORIES".into(), rhai::Dynamic::from(signatories));
 | 
				
			||||||
 | 
					        tag_map.insert("DB_PATH".into(), "/tmp/test_db".to_string().into());
 | 
				
			||||||
 | 
					        tag_map.insert("CONTEXT_ID".into(), "test_context".to_string().into());
 | 
				
			||||||
 | 
					        engine.set_default_tag(rhai::Dynamic::from(tag_map));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Test get_context with valid signatories
 | 
				
			||||||
 | 
					        let mut scope = rhai::Scope::new();
 | 
				
			||||||
 | 
					        let test_result = engine.eval_with_scope::<rhai::Dynamic>(
 | 
				
			||||||
 | 
					            &mut scope,
 | 
				
			||||||
 | 
					            r#"
 | 
				
			||||||
 | 
					                // All participants must be signatories
 | 
				
			||||||
 | 
					                let ctx = get_context(["pk1", "pk2"]);
 | 
				
			||||||
 | 
					                ctx.context_id()
 | 
				
			||||||
 | 
					            "#
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if let Err(ref e) = test_result {
 | 
				
			||||||
 | 
					            eprintln!("Test error: {}", e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        assert!(test_result.is_ok(), "Failed to get context: {:?}", test_result.err());
 | 
				
			||||||
 | 
					        assert_eq!(test_result.unwrap().to_string(), "pk1,pk2");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn test_engine_with_manager_access_denied() {
 | 
				
			||||||
 | 
					        let result = create_osiris_engine();
 | 
				
			||||||
 | 
					        assert!(result.is_ok());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let mut engine = result.unwrap();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set up context tags with SIGNATORIES
 | 
				
			||||||
 | 
					        let mut tag_map = rhai::Map::new();
 | 
				
			||||||
 | 
					        // Create a proper Rhai array
 | 
				
			||||||
 | 
					        let signatories: rhai::Array = vec![
 | 
				
			||||||
 | 
					            rhai::Dynamic::from("pk1".to_string()),
 | 
				
			||||||
 | 
					            rhai::Dynamic::from("pk2".to_string()),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        tag_map.insert("SIGNATORIES".into(), rhai::Dynamic::from(signatories));
 | 
				
			||||||
 | 
					        tag_map.insert("DB_PATH".into(), "/tmp/test_db".to_string().into());
 | 
				
			||||||
 | 
					        tag_map.insert("CONTEXT_ID".into(), "test_context".to_string().into());
 | 
				
			||||||
 | 
					        engine.set_default_tag(rhai::Dynamic::from(tag_map));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Test get_context with invalid participant (not a signatory)
 | 
				
			||||||
 | 
					        let mut scope = rhai::Scope::new();
 | 
				
			||||||
 | 
					        let test_result = engine.eval_with_scope::<rhai::Dynamic>(
 | 
				
			||||||
 | 
					            &mut scope,
 | 
				
			||||||
 | 
					            r#"
 | 
				
			||||||
 | 
					                // pk3 is not a signatory, should fail
 | 
				
			||||||
 | 
					                let ctx = get_context(["pk1", "pk3"]);
 | 
				
			||||||
 | 
					                ctx.context_id()
 | 
				
			||||||
 | 
					            "#
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Should fail because pk3 is not in SIGNATORIES
 | 
				
			||||||
 | 
					        assert!(test_result.is_err());
 | 
				
			||||||
 | 
					        let err_msg = test_result.unwrap_err().to_string();
 | 
				
			||||||
 | 
					        assert!(err_msg.contains("Access denied") || err_msg.contains("not a signatory"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2,7 +2,7 @@ use crate::error::Result;
 | 
				
			|||||||
use crate::store::{HeroDbClient, OsirisObject};
 | 
					use crate::store::{HeroDbClient, OsirisObject};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Field indexing for fast filtering by tags and metadata
 | 
					/// Field indexing for fast filtering by tags and metadata
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
pub struct FieldIndex {
 | 
					pub struct FieldIndex {
 | 
				
			||||||
    client: HeroDbClient,
 | 
					    client: HeroDbClient,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/lib.rs
									
									
									
									
									
								
							@@ -8,14 +8,21 @@ pub mod interfaces;
 | 
				
			|||||||
pub mod objects;
 | 
					pub mod objects;
 | 
				
			||||||
pub mod retrieve;
 | 
					pub mod retrieve;
 | 
				
			||||||
pub mod store;
 | 
					pub mod store;
 | 
				
			||||||
pub mod rhai;
 | 
					
 | 
				
			||||||
 | 
					// Rhai integration modules (top-level)
 | 
				
			||||||
 | 
					pub mod context;
 | 
				
			||||||
 | 
					pub mod engine;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use error::{Error, Result};
 | 
					pub use error::{Error, Result};
 | 
				
			||||||
pub use store::{BaseData, IndexKey, Object, Storable};
 | 
					pub use store::{BaseData, IndexKey, Object, Storable};
 | 
				
			||||||
pub use objects::{Event, Note};
 | 
					pub use objects::{Event, Note};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// OsirisContext is the main type for Rhai integration
 | 
					// OsirisContext is the main type for Rhai integration
 | 
				
			||||||
pub use rhai::{OsirisContext, OsirisInstance};
 | 
					pub use context::{OsirisContext, OsirisInstance, OsirisContextBuilder};
 | 
				
			||||||
 | 
					pub use engine::{
 | 
				
			||||||
 | 
					    create_osiris_engine,
 | 
				
			||||||
 | 
					    OsirisPackage,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Re-export the derive macro
 | 
					// Re-export the derive macro
 | 
				
			||||||
pub use osiris_derive::Object as DeriveObject;
 | 
					pub use osiris_derive::Object as DeriveObject;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,188 +0,0 @@
 | 
				
			|||||||
/// Builder for OsirisContext
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use super::OsirisContext;
 | 
					 | 
				
			||||||
use crate::store::{GenericStore, HeroDbClient, TypeRegistry};
 | 
					 | 
				
			||||||
use std::sync::Arc;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Builder for OsirisContext
 | 
					 | 
				
			||||||
pub struct OsirisContextBuilder {
 | 
					 | 
				
			||||||
    participants: Option<Vec<String>>,
 | 
					 | 
				
			||||||
    herodb_url: Option<String>,
 | 
					 | 
				
			||||||
    db_id: Option<u16>,
 | 
					 | 
				
			||||||
    registry: Option<Arc<TypeRegistry>>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl OsirisContextBuilder {
 | 
					 | 
				
			||||||
    /// Create a new builder
 | 
					 | 
				
			||||||
    pub fn new() -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            participants: None,
 | 
					 | 
				
			||||||
            herodb_url: None,
 | 
					 | 
				
			||||||
            db_id: None,
 | 
					 | 
				
			||||||
            registry: 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
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Set the type registry
 | 
					 | 
				
			||||||
    pub fn registry(mut self, registry: Arc<TypeRegistry>) -> Self {
 | 
					 | 
				
			||||||
        self.registry = Some(registry);
 | 
					 | 
				
			||||||
        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")?;
 | 
					 | 
				
			||||||
        let herodb_url = self.herodb_url.ok_or("HeroDB URL is required")?;
 | 
					 | 
				
			||||||
        let db_id = self.db_id.ok_or("Database ID is required")?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        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 with optional registry
 | 
					 | 
				
			||||||
        let store = if let Some(reg) = self.registry {
 | 
					 | 
				
			||||||
            GenericStore::with_registry(client, reg)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            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_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_with_registry() {
 | 
					 | 
				
			||||||
        let registry = Arc::new(TypeRegistry::new());
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let ctx = OsirisContextBuilder::new()
 | 
					 | 
				
			||||||
            .name("test_context")
 | 
					 | 
				
			||||||
            .herodb_url("redis://localhost:6379")
 | 
					 | 
				
			||||||
            .db_id(1)
 | 
					 | 
				
			||||||
            .registry(registry)
 | 
					 | 
				
			||||||
            .build();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        assert!(ctx.is_ok());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[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"));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_builder_missing_url() {
 | 
					 | 
				
			||||||
        let ctx = OsirisContextBuilder::new()
 | 
					 | 
				
			||||||
            .name("test_context")
 | 
					 | 
				
			||||||
            .db_id(1)
 | 
					 | 
				
			||||||
            .build();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        assert!(ctx.is_err());
 | 
					 | 
				
			||||||
        assert!(ctx.unwrap_err().to_string().contains("HeroDB URL is required"));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_builder_missing_db_id() {
 | 
					 | 
				
			||||||
        let ctx = OsirisContextBuilder::new()
 | 
					 | 
				
			||||||
            .name("test_context")
 | 
					 | 
				
			||||||
            .herodb_url("redis://localhost:6379")
 | 
					 | 
				
			||||||
            .build();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        assert!(ctx.is_err());
 | 
					 | 
				
			||||||
        assert!(ctx.unwrap_err().to_string().contains("Database ID is required"));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_builder_fluent_api() {
 | 
					 | 
				
			||||||
        // Test that builder methods can be chained
 | 
					 | 
				
			||||||
        let result = OsirisContextBuilder::new()
 | 
					 | 
				
			||||||
            .name("ctx1")
 | 
					 | 
				
			||||||
            .owner("owner1")
 | 
					 | 
				
			||||||
            .herodb_url("redis://localhost:6379")
 | 
					 | 
				
			||||||
            .db_id(1)
 | 
					 | 
				
			||||||
            .build();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        assert!(result.is_ok());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,117 +0,0 @@
 | 
				
			|||||||
/// OSIRIS Rhai Engine
 | 
					 | 
				
			||||||
/// 
 | 
					 | 
				
			||||||
/// Creates a Rhai engine configured with OSIRIS contexts and methods.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use super::{OsirisContext, register_context_api};
 | 
					 | 
				
			||||||
use rhai::Engine;
 | 
					 | 
				
			||||||
use std::collections::HashMap;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Create a Rhai engine with get_context function
 | 
					 | 
				
			||||||
/// This allows dynamic context creation via get_context() in Rhai scripts
 | 
					 | 
				
			||||||
pub fn create_osiris_engine(
 | 
					 | 
				
			||||||
    herodb_url: &str,
 | 
					 | 
				
			||||||
    base_db_id: u16,
 | 
					 | 
				
			||||||
) -> Result<Engine, Box<dyn std::error::Error>> {
 | 
					 | 
				
			||||||
    let mut engine = Engine::new();
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Register OsirisContext type
 | 
					 | 
				
			||||||
    engine.build_type::<OsirisContext>();
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Register all OSIRIS functions (Note, Event, etc.)
 | 
					 | 
				
			||||||
    super::register_osiris_functions(&mut engine);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Register get_context function
 | 
					 | 
				
			||||||
    register_context_api(&mut engine, herodb_url.to_string(), base_db_id);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    Ok(engine)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[cfg(test)]
 | 
					 | 
				
			||||||
mod tests {
 | 
					 | 
				
			||||||
    use super::*;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_create_osiris_engine() {
 | 
					 | 
				
			||||||
        let result = create_osiris_engine("redis://localhost:6379", 1);
 | 
					 | 
				
			||||||
        assert!(result.is_ok());
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let mut engine = result.unwrap();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Set up context tags with SIGNATORIES (like in runner_rust example)
 | 
					 | 
				
			||||||
        let mut tag_map = rhai::Map::new();
 | 
					 | 
				
			||||||
        // Create a proper Rhai array
 | 
					 | 
				
			||||||
        let signatories: rhai::Array = vec![
 | 
					 | 
				
			||||||
            rhai::Dynamic::from("pk1".to_string()),
 | 
					 | 
				
			||||||
            rhai::Dynamic::from("pk2".to_string()),
 | 
					 | 
				
			||||||
            rhai::Dynamic::from("pk3".to_string()),
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
        tag_map.insert("SIGNATORIES".into(), rhai::Dynamic::from(signatories));
 | 
					 | 
				
			||||||
        tag_map.insert("DB_PATH".into(), "/tmp/test_db".to_string().into());
 | 
					 | 
				
			||||||
        tag_map.insert("CONTEXT_ID".into(), "test_context".to_string().into());
 | 
					 | 
				
			||||||
        engine.set_default_tag(rhai::Dynamic::from(tag_map));
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Test get_context with valid signatories
 | 
					 | 
				
			||||||
        let mut scope = rhai::Scope::new();
 | 
					 | 
				
			||||||
        let test_result = engine.eval_with_scope::<rhai::Dynamic>(
 | 
					 | 
				
			||||||
            &mut scope,
 | 
					 | 
				
			||||||
            r#"
 | 
					 | 
				
			||||||
                // All participants must be signatories
 | 
					 | 
				
			||||||
                let ctx = get_context(["pk1", "pk2"]);
 | 
					 | 
				
			||||||
                ctx.context_id()
 | 
					 | 
				
			||||||
            "#
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        if let Err(ref e) = test_result {
 | 
					 | 
				
			||||||
            eprintln!("Test error: {}", e);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        assert!(test_result.is_ok(), "Failed to get context: {:?}", test_result.err());
 | 
					 | 
				
			||||||
        assert_eq!(test_result.unwrap().to_string(), "pk1,pk2");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_engine_with_manager_access_denied() {
 | 
					 | 
				
			||||||
        let result = create_osiris_engine("redis://localhost:6379", 1);
 | 
					 | 
				
			||||||
        assert!(result.is_ok());
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let mut engine = result.unwrap();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Set up context tags with SIGNATORIES
 | 
					 | 
				
			||||||
        let mut tag_map = rhai::Map::new();
 | 
					 | 
				
			||||||
        // Create a proper Rhai array
 | 
					 | 
				
			||||||
        let signatories: rhai::Array = vec![
 | 
					 | 
				
			||||||
            rhai::Dynamic::from("pk1".to_string()),
 | 
					 | 
				
			||||||
            rhai::Dynamic::from("pk2".to_string()),
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
        tag_map.insert("SIGNATORIES".into(), rhai::Dynamic::from(signatories));
 | 
					 | 
				
			||||||
        tag_map.insert("DB_PATH".into(), "/tmp/test_db".to_string().into());
 | 
					 | 
				
			||||||
        tag_map.insert("CONTEXT_ID".into(), "test_context".to_string().into());
 | 
					 | 
				
			||||||
        engine.set_default_tag(rhai::Dynamic::from(tag_map));
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Test get_context with invalid participant (not a signatory)
 | 
					 | 
				
			||||||
        let mut scope = rhai::Scope::new();
 | 
					 | 
				
			||||||
        let test_result = engine.eval_with_scope::<rhai::Dynamic>(
 | 
					 | 
				
			||||||
            &mut scope,
 | 
					 | 
				
			||||||
            r#"
 | 
					 | 
				
			||||||
                // pk3 is not a signatory, should fail
 | 
					 | 
				
			||||||
                let ctx = get_context(["pk1", "pk3"]);
 | 
					 | 
				
			||||||
                ctx.context_id()
 | 
					 | 
				
			||||||
            "#
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Should fail because pk3 is not in SIGNATORIES
 | 
					 | 
				
			||||||
        assert!(test_result.is_err());
 | 
					 | 
				
			||||||
        let err_msg = test_result.unwrap_err().to_string();
 | 
					 | 
				
			||||||
        assert!(err_msg.contains("Access denied") || err_msg.contains("not a signatory"));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_engine_context_operations() {
 | 
					 | 
				
			||||||
        let result = create_osiris_engine("owner", "redis://localhost:6379", 1);
 | 
					 | 
				
			||||||
        assert!(result.is_ok());
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let (_engine, scope) = result.unwrap();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Just verify the scope has the default context
 | 
					 | 
				
			||||||
        assert_eq!(scope.len(), 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,29 +0,0 @@
 | 
				
			|||||||
/// Rhai integration for OSIRIS
 | 
					 | 
				
			||||||
/// 
 | 
					 | 
				
			||||||
/// Provides OsirisContext - a complete context with HeroDB storage and member management.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
mod builder;
 | 
					 | 
				
			||||||
mod instance;
 | 
					 | 
				
			||||||
pub mod engine;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::objects::note::rhai::register_note_api;
 | 
					 | 
				
			||||||
use crate::objects::event::rhai::register_event_api;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Main exports
 | 
					 | 
				
			||||||
pub use builder::OsirisContextBuilder;
 | 
					 | 
				
			||||||
pub use instance::{
 | 
					 | 
				
			||||||
    OsirisContext, 
 | 
					 | 
				
			||||||
    OsirisInstance,
 | 
					 | 
				
			||||||
    register_context_api,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub use engine::{
 | 
					 | 
				
			||||||
    create_osiris_engine,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Register all OSIRIS functions (Note, Event, etc.) in a Rhai engine
 | 
					 | 
				
			||||||
/// This does NOT include context management - use register_context_api for that
 | 
					 | 
				
			||||||
pub fn register_osiris_functions(engine: &mut rhai::Engine) {
 | 
					 | 
				
			||||||
    register_note_api(engine);
 | 
					 | 
				
			||||||
    register_event_api(engine);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,14 +1,12 @@
 | 
				
			|||||||
use crate::error::Result;
 | 
					use crate::error::Result;
 | 
				
			||||||
use crate::index::FieldIndex;
 | 
					use crate::index::FieldIndex;
 | 
				
			||||||
use crate::store::{HeroDbClient, Object, TypeRegistry};
 | 
					use crate::store::{HeroDbClient, Object};
 | 
				
			||||||
use std::sync::Arc;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Generic storage layer for OSIRIS objects
 | 
					/// Generic storage layer for OSIRIS objects
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
pub struct GenericStore {
 | 
					pub struct GenericStore {
 | 
				
			||||||
    client: HeroDbClient,
 | 
					    client: HeroDbClient,
 | 
				
			||||||
    index: FieldIndex,
 | 
					    index: FieldIndex,
 | 
				
			||||||
    type_registry: Option<Arc<TypeRegistry>>,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl GenericStore {
 | 
					impl GenericStore {
 | 
				
			||||||
@@ -18,25 +16,9 @@ impl GenericStore {
 | 
				
			|||||||
        Self { 
 | 
					        Self { 
 | 
				
			||||||
            client, 
 | 
					            client, 
 | 
				
			||||||
            index,
 | 
					            index,
 | 
				
			||||||
            type_registry: None,
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Create a new generic store with a type registry
 | 
					 | 
				
			||||||
    pub fn with_registry(client: HeroDbClient, registry: Arc<TypeRegistry>) -> Self {
 | 
					 | 
				
			||||||
        let index = FieldIndex::new(client.clone());
 | 
					 | 
				
			||||||
        Self { 
 | 
					 | 
				
			||||||
            client, 
 | 
					 | 
				
			||||||
            index,
 | 
					 | 
				
			||||||
            type_registry: Some(registry),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Set the type registry
 | 
					 | 
				
			||||||
    pub fn set_registry(&mut self, registry: Arc<TypeRegistry>) {
 | 
					 | 
				
			||||||
        self.type_registry = Some(registry);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Store an object
 | 
					    /// Store an object
 | 
				
			||||||
    pub async fn put<T: Object>(&self, obj: &T) -> Result<()> {
 | 
					    pub async fn put<T: Object>(&self, obj: &T) -> Result<()> {
 | 
				
			||||||
        // Serialize object to JSON
 | 
					        // Serialize object to JSON
 | 
				
			||||||
@@ -68,11 +50,6 @@ impl GenericStore {
 | 
				
			|||||||
            .ok_or_else(|| crate::error::Error::NotFound(format!("Object {}:{}", ns, id)))
 | 
					            .ok_or_else(|| crate::error::Error::NotFound(format!("Object {}:{}", ns, id)))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Get the type registry if configured
 | 
					 | 
				
			||||||
    pub fn type_registry(&self) -> Option<Arc<TypeRegistry>> {
 | 
					 | 
				
			||||||
        self.type_registry.clone()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Delete an object
 | 
					    /// Delete an object
 | 
				
			||||||
    pub async fn delete<T: Object>(&self, obj: &T) -> Result<bool> {
 | 
					    pub async fn delete<T: Object>(&self, obj: &T) -> Result<bool> {
 | 
				
			||||||
        let key = format!("obj:{}:{}", obj.namespace(), obj.id());
 | 
					        let key = format!("obj:{}:{}", obj.namespace(), obj.id());
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,12 +2,10 @@ pub mod base_data;
 | 
				
			|||||||
pub mod object_trait;
 | 
					pub mod object_trait;
 | 
				
			||||||
pub mod herodb_client;
 | 
					pub mod herodb_client;
 | 
				
			||||||
pub mod generic_store;
 | 
					pub mod generic_store;
 | 
				
			||||||
pub mod type_registry;
 | 
					 | 
				
			||||||
pub mod object; // Keep old implementation for backwards compat temporarily
 | 
					pub mod object; // Keep old implementation for backwards compat temporarily
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use base_data::BaseData;
 | 
					pub use base_data::BaseData;
 | 
				
			||||||
pub use object_trait::{IndexKey, Object, Storable};
 | 
					pub use object_trait::{IndexKey, Object, Storable};
 | 
				
			||||||
pub use herodb_client::HeroDbClient;
 | 
					pub use herodb_client::HeroDbClient;
 | 
				
			||||||
pub use generic_store::GenericStore;
 | 
					pub use generic_store::GenericStore;
 | 
				
			||||||
pub use type_registry::TypeRegistry;
 | 
					 | 
				
			||||||
pub use object::{Metadata, OsirisObject}; // Old implementation
 | 
					pub use object::{Metadata, OsirisObject}; // Old implementation
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,216 +0,0 @@
 | 
				
			|||||||
/// Type Registry for OSIRIS
 | 
					 | 
				
			||||||
/// 
 | 
					 | 
				
			||||||
/// Maps collection names to Rust types so that save() can use the correct struct.
 | 
					 | 
				
			||||||
/// Each type must implement Object trait for proper indexing.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::error::Result;
 | 
					 | 
				
			||||||
use crate::store::{GenericStore, Object};
 | 
					 | 
				
			||||||
use std::collections::HashMap;
 | 
					 | 
				
			||||||
use std::sync::{Arc, RwLock};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Type deserializer: takes JSON and returns a typed Object
 | 
					 | 
				
			||||||
pub type TypeDeserializer = Arc<dyn Fn(&str) -> Result<Box<dyn std::any::Any + Send>> + Send + Sync>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Type saver: takes the Any box and saves it using the correct type
 | 
					 | 
				
			||||||
pub type TypeSaver = Arc<dyn Fn(&GenericStore, Box<dyn std::any::Any + Send>) -> Result<()> + Send + Sync>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Registry of types mapped to collection names
 | 
					 | 
				
			||||||
#[derive(Clone)]
 | 
					 | 
				
			||||||
pub struct TypeRegistry {
 | 
					 | 
				
			||||||
    deserializers: Arc<RwLock<HashMap<String, TypeDeserializer>>>,
 | 
					 | 
				
			||||||
    savers: Arc<RwLock<HashMap<String, TypeSaver>>>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl std::fmt::Debug for TypeRegistry {
 | 
					 | 
				
			||||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
					 | 
				
			||||||
        f.debug_struct("TypeRegistry")
 | 
					 | 
				
			||||||
            .field("collections", &self.list_collections())
 | 
					 | 
				
			||||||
            .finish()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl TypeRegistry {
 | 
					 | 
				
			||||||
    /// Create a new empty type registry
 | 
					 | 
				
			||||||
    pub fn new() -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            deserializers: Arc::new(RwLock::new(HashMap::new())),
 | 
					 | 
				
			||||||
            savers: Arc::new(RwLock::new(HashMap::new())),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Register a type for a collection
 | 
					 | 
				
			||||||
    /// 
 | 
					 | 
				
			||||||
    /// Example:
 | 
					 | 
				
			||||||
    /// ```rust
 | 
					 | 
				
			||||||
    /// registry.register_type::<Resident>("residents");
 | 
					 | 
				
			||||||
    /// registry.register_type::<Company>("companies");
 | 
					 | 
				
			||||||
    /// ```
 | 
					 | 
				
			||||||
    pub fn register_type<T>(&self, collection: impl ToString) -> Result<()>
 | 
					 | 
				
			||||||
    where
 | 
					 | 
				
			||||||
        T: Object + serde::de::DeserializeOwned + 'static,
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let collection_str = collection.to_string();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Deserializer: JSON -> Box<Any>
 | 
					 | 
				
			||||||
        let deserializer: TypeDeserializer = Arc::new(move |json: &str| {
 | 
					 | 
				
			||||||
            let obj: T = serde_json::from_str(json)
 | 
					 | 
				
			||||||
                .map_err(|e| crate::error::Error::from(e))?;
 | 
					 | 
				
			||||||
            Ok(Box::new(obj) as Box<dyn std::any::Any + Send>)
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Saver: Box<Any> -> store.put()
 | 
					 | 
				
			||||||
        let saver: TypeSaver = Arc::new(move |store: &GenericStore, any: Box<dyn std::any::Any + Send>| {
 | 
					 | 
				
			||||||
            let obj = any.downcast::<T>()
 | 
					 | 
				
			||||||
                .map_err(|_| crate::error::Error::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to downcast object")))?;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            tokio::task::block_in_place(|| {
 | 
					 | 
				
			||||||
                tokio::runtime::Handle::current().block_on(async move {
 | 
					 | 
				
			||||||
                    store.put(&*obj).await
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Register both
 | 
					 | 
				
			||||||
        self.deserializers.write()
 | 
					 | 
				
			||||||
            .map_err(|e| crate::error::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to acquire write lock: {}", e))))?
 | 
					 | 
				
			||||||
            .insert(collection_str.clone(), deserializer);
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        self.savers.write()
 | 
					 | 
				
			||||||
            .map_err(|e| crate::error::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to acquire write lock: {}", e))))?
 | 
					 | 
				
			||||||
            .insert(collection_str, saver);
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Generic save function - uses registry to determine type
 | 
					 | 
				
			||||||
    pub fn save(&self, store: &GenericStore, collection: &str, _id: &str, json: &str) -> Result<()> {
 | 
					 | 
				
			||||||
        // Get deserializer for this collection
 | 
					 | 
				
			||||||
        let deserializers = self.deserializers.read()
 | 
					 | 
				
			||||||
            .map_err(|e| crate::error::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to acquire read lock: {}", e))))?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let deserializer = deserializers.get(collection)
 | 
					 | 
				
			||||||
            .ok_or_else(|| crate::error::Error::NotFound(format!("No type registered for collection: {}", collection)))?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Deserialize JSON to typed object (as Any)
 | 
					 | 
				
			||||||
        let any_obj = deserializer(json)?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Get saver for this collection
 | 
					 | 
				
			||||||
        let savers = self.savers.read()
 | 
					 | 
				
			||||||
            .map_err(|e| crate::error::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to acquire read lock: {}", e))))?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let saver = savers.get(collection)
 | 
					 | 
				
			||||||
            .ok_or_else(|| crate::error::Error::NotFound(format!("No saver registered for collection: {}", collection)))?;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Save using the correct type
 | 
					 | 
				
			||||||
        saver(store, any_obj)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Check if a collection has a registered type
 | 
					 | 
				
			||||||
    pub fn has_type(&self, collection: &str) -> bool {
 | 
					 | 
				
			||||||
        self.deserializers.read()
 | 
					 | 
				
			||||||
            .map(|d| d.contains_key(collection))
 | 
					 | 
				
			||||||
            .unwrap_or(false)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// List all registered collections
 | 
					 | 
				
			||||||
    pub fn list_collections(&self) -> Vec<String> {
 | 
					 | 
				
			||||||
        self.deserializers.read()
 | 
					 | 
				
			||||||
            .map(|d| d.keys().cloned().collect())
 | 
					 | 
				
			||||||
            .unwrap_or_default()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for TypeRegistry {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self::new()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[cfg(test)]
 | 
					 | 
				
			||||||
mod tests {
 | 
					 | 
				
			||||||
    use super::*;
 | 
					 | 
				
			||||||
    use crate::objects::Note;
 | 
					 | 
				
			||||||
    use tokio::runtime::Runtime;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_registry_creation() {
 | 
					 | 
				
			||||||
        let registry = TypeRegistry::new();
 | 
					 | 
				
			||||||
        assert!(!registry.has_type("notes"));
 | 
					 | 
				
			||||||
        assert_eq!(registry.list_collections().len(), 0);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_register_type() {
 | 
					 | 
				
			||||||
        let registry = TypeRegistry::new();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Register Note type
 | 
					 | 
				
			||||||
        let result = registry.register_type::<Note>("notes");
 | 
					 | 
				
			||||||
        assert!(result.is_ok());
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Check it's registered
 | 
					 | 
				
			||||||
        assert!(registry.has_type("notes"));
 | 
					 | 
				
			||||||
        assert_eq!(registry.list_collections().len(), 1);
 | 
					 | 
				
			||||||
        assert!(registry.list_collections().contains(&"notes".to_string()));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_register_multiple_types() {
 | 
					 | 
				
			||||||
        let registry = TypeRegistry::new();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        registry.register_type::<Note>("notes").unwrap();
 | 
					 | 
				
			||||||
        registry.register_type::<Note>("drafts").unwrap(); // Same type, different collection
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        assert!(registry.has_type("notes"));
 | 
					 | 
				
			||||||
        assert!(registry.has_type("drafts"));
 | 
					 | 
				
			||||||
        assert_eq!(registry.list_collections().len(), 2);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_save_with_registry() {
 | 
					 | 
				
			||||||
        let registry = TypeRegistry::new();
 | 
					 | 
				
			||||||
        registry.register_type::<Note>("notes").unwrap();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Verify the type is registered
 | 
					 | 
				
			||||||
        assert!(registry.has_type("notes"));
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Note: Actual save test would require a running Redis instance
 | 
					 | 
				
			||||||
        // The registration itself proves the type system works
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_save_unregistered_collection() {
 | 
					 | 
				
			||||||
        let registry = TypeRegistry::new();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Verify unregistered collection is not found
 | 
					 | 
				
			||||||
        assert!(!registry.has_type("unknown"));
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Note: Actual save test would require a running Redis instance
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_list_collections() {
 | 
					 | 
				
			||||||
        let registry = TypeRegistry::new();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        registry.register_type::<Note>("notes").unwrap();
 | 
					 | 
				
			||||||
        registry.register_type::<Note>("drafts").unwrap();
 | 
					 | 
				
			||||||
        registry.register_type::<Note>("archive").unwrap();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let collections = registry.list_collections();
 | 
					 | 
				
			||||||
        assert_eq!(collections.len(), 3);
 | 
					 | 
				
			||||||
        assert!(collections.contains(&"notes".to_string()));
 | 
					 | 
				
			||||||
        assert!(collections.contains(&"drafts".to_string()));
 | 
					 | 
				
			||||||
        assert!(collections.contains(&"archive".to_string()));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn test_has_type() {
 | 
					 | 
				
			||||||
        let registry = TypeRegistry::new();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        assert!(!registry.has_type("notes"));
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        registry.register_type::<Note>("notes").unwrap();
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        assert!(registry.has_type("notes"));
 | 
					 | 
				
			||||||
        assert!(!registry.has_type("other"));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
		Reference in New Issue
	
	Block a user