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:
Timur Gordon
2025-10-28 12:20:17 +01:00
parent e04012c8c0
commit e760a184b1
12 changed files with 399 additions and 805 deletions

262
src/engine.rs Normal file
View 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"));
}
}