use rhai::{Engine, AST, Scope, Dynamic, Array}; use std::collections::HashMap; use std::path::Path; use std::sync::{Arc, RwLock}; use std::fs; /// Type alias for a Rhai function that can be called from Rust pub type RhaiFn = Box) -> Result>; /// ScriptManager handles loading, compiling, and exposing Rhai scripts pub struct ScriptManager { engine: Engine, scripts: HashMap>>, functions: HashMap, filters: HashMap, } /// Creates a standard engine with all necessary string functions registered fn create_standard_engine() -> Engine { // Create a new engine with all default features enabled let mut engine = Engine::new(); // Set up standard packages engine.set_fast_operators(true); // Register essential string functions needed by the scripts engine.register_fn("substr", |s: &str, start: i64, len: i64| { let start = start as usize; let len = len as usize; if start >= s.len() { return String::new(); } let end = (start + len).min(s.len()); s[start..end].to_string() }); // Register string case conversion functions engine.register_fn("to_upper", |s: &str| s.to_uppercase()); engine.register_fn("to_lower", |s: &str| s.to_lowercase()); // Register array/string splitting and joining functions // This form matches exactly what is expected in the script: text.split(" ") engine.register_fn("split", |text: &str, delimiter: &str| -> Array { text.split(delimiter).map(|s| Dynamic::from(s.to_string())).collect() }); // Register split with one parameter (defaults to space delimiter) engine.register_fn("split", |s: &str| -> Array { s.split_whitespace() .map(|part| Dynamic::from(part.to_string())) .collect() }); // Register len property as a method for arrays engine.register_fn("len", |array: &mut Array| -> i64 { array.len() as i64 }); engine.register_fn("join", |arr: &mut Array, separator: &str| -> String { arr.iter() .map(|v| v.to_string()) .collect::>() .join(separator) }); // Register string replace function engine.register_fn("replace", |s: &str, from: &str, to: &str| -> String { s.replace(from, to) }); // Register push function for arrays engine.register_fn("push", |arr: &mut Array, value: Dynamic| { arr.push(value); Dynamic::from(()) }); // Register len property for strings engine.register_fn("len", |s: &str| -> i64 { s.len() as i64 }); // Register range operator for the repeat function engine.register_fn("..", |start: i64, end: i64| { let mut arr = Array::new(); for i in start..end { arr.push(Dynamic::from(i)); } arr }); engine } impl ScriptManager { /// Create a new ScriptManager pub fn new() -> Self { Self { engine: create_standard_engine(), scripts: HashMap::new(), functions: HashMap::new(), filters: HashMap::new(), } } /// Load a Rhai script from a file pub fn load_script(&mut self, name: &str, path: impl AsRef) -> Result<(), String> { let path = path.as_ref(); // Read the script file let script_content = fs::read_to_string(path) .map_err(|e| format!("Failed to read script file {}: {}", path.display(), e))?; // Compile the script with the current engine let ast = self.engine.compile(&script_content) .map_err(|e| format!("Failed to compile script {}: {}", name, e))?; // Store the compiled AST self.scripts.insert(name.to_string(), Arc::new(RwLock::new(ast))); // Extract and map functions self.map_script_functions(name)?; Ok(()) } /// Extract functions from a script and map them to callable wrappers fn map_script_functions(&mut self, script_name: &str) -> Result<(), String> { let script_arc = self.scripts.get(script_name) .ok_or_else(|| format!("Script not found: {}", script_name))? .clone(); // Get the AST to extract function names dynamically let ast_lock = script_arc.read().unwrap(); // Use iter_functions to get all defined functions in the script let function_defs: Vec<_> = ast_lock.iter_functions().collect(); // Log found functions for debugging println!("Registering functions for script '{}':", script_name); // Register each function we found for fn_def in function_defs { let fn_name = fn_def.name.to_string(); println!(" - {} (params: {})", fn_name, fn_def.params.len()); let full_name = format!("{}:{}", script_name, fn_name); let script_arc_clone = script_arc.clone(); let fn_name_owned = fn_name.clone(); // Create a closure that will call this function using a shared engine configuration let function_wrapper: RhaiFn = Box::new(move |args: Vec| -> Result { // Create a configured engine for each invocation let engine = create_standard_engine(); let mut scope = Scope::new(); // Call the function engine.call_fn::( &mut scope, &script_arc_clone.read().unwrap(), &fn_name_owned, args ).map_err(|e| format!("Error calling function {}: {}", fn_name_owned, e)) }); // Store the function wrapper in the maps self.functions.insert(full_name, function_wrapper); // Create a new wrapper for the filter let script_arc_clone = script_arc.clone(); let fn_name_owned = fn_name.clone(); let filter_wrapper: RhaiFn = Box::new(move |args: Vec| -> Result { // Use the same engine creation function let engine = create_standard_engine(); let mut scope = Scope::new(); engine.call_fn::( &mut scope, &script_arc_clone.read().unwrap(), &fn_name_owned, args ).map_err(|e| format!("Error calling filter {}: {}", fn_name_owned, e)) }); // Store the filter wrapper self.filters.insert(fn_name, filter_wrapper); } Ok(()) } /// Get a function by its full name (script:function) pub fn get_function(&self, name: &str) -> Option<&RhaiFn> { self.functions.get(name) } /// Get a filter by its name pub fn get_filter(&self, name: &str) -> Option<&RhaiFn> { self.filters.get(name) } /// Get all filters pub fn get_all_filters(&self) -> &HashMap { &self.filters } /// Get all functions pub fn get_all_functions(&self) -> &HashMap { &self.functions } /// Call a function by its full name (script:function) pub fn call_function(&self, name: &str, args: Vec) -> Result { if let Some(func) = self.functions.get(name) { func(args) } else { Err(format!("Function not found: {}", name)) } } /// Call a filter by its name pub fn call_filter(&self, name: &str, args: Vec) -> Result { if let Some(filter) = self.filters.get(name) { filter(args) } else { Err(format!("Filter not found: {}", name)) } } /// Reload a script from a file pub fn reload_script(&mut self, name: &str, path: impl AsRef) -> Result<(), String> { let path = path.as_ref(); // Read the script file let script_content = fs::read_to_string(path) .map_err(|e| format!("Failed to read script file {}: {}", path.display(), e))?; // Compile the script let ast = self.engine.compile(&script_content) .map_err(|e| format!("Failed to compile script {}: {}", name, e))?; // Update the stored AST if let Some(script_arc) = self.scripts.get(name) { let mut script = script_arc.write().unwrap(); *script = ast; } else { self.scripts.insert(name.to_string(), Arc::new(RwLock::new(ast))); } // Re-map functions self.map_script_functions(name)?; Ok(()) } /// Load all .rhai scripts from a directory pub fn load_scripts_from_directory(&mut self, dir_path: impl AsRef) -> Result, String> { let dir_path = dir_path.as_ref(); // Check if directory exists if !dir_path.exists() || !dir_path.is_dir() { return Err(format!("Directory does not exist or is not a directory: {}", dir_path.display())); } // Get all entries in the directory let entries = fs::read_dir(dir_path) .map_err(|e| format!("Failed to read directory {}: {}", dir_path.display(), e))?; let mut loaded_scripts = Vec::new(); // Process each entry for entry_result in entries { let entry = entry_result.map_err(|e| format!("Failed to read directory entry: {}", e))?; let path = entry.path(); // Only process .rhai files if path.is_file() && path.extension().map_or(false, |ext| ext == "rhai") { if let Some(stem) = path.file_stem() { if let Some(script_name) = stem.to_str() { // Load the script self.load_script(script_name, &path)?; loaded_scripts.push(script_name.to_string()); } } } } Ok(loaded_scripts) } /// Get a list of all registered function names pub fn get_function_names(&self) -> Vec { self.functions.keys().cloned().collect() } }