# Tera Engine Factory with Hot Reloadable Rhai Integration ## Overview We'll create a `TeraFactory` module that provides a factory for creating Tera template engines with integrated Rhai scripting support. The factory will: 1. Create Tera engines with specified template directories 2. Integrate with the hot reloadable Rhai AST from the `RhaiFactory` 3. Allow Rhai functions to be called from Tera templates 4. Automatically update available functions when Rhai scripts are hot reloaded ## Architecture ```mermaid graph TD A[TeraFactory] --> B[create_tera_engine] A --> C[create_tera_with_rhai] C --> D[Tera Engine with Rhai Functions] E[RhaiFactory] --> F[HotReloadableAST] F --> C G[RhaiFunctionAdapter] --> D H[Template Directories] --> B ``` ## Component Details ### 1. TeraFactory Module Structure ``` tera_factory/ ├── Cargo.toml # Dependencies including tera and rhai_factory └── src/ ├── lib.rs # Main module exports and unit tests ├── factory.rs # Factory implementation and unit tests ├── error.rs # Custom error types and unit tests └── function_adapter.rs # Rhai function adapter for Tera ``` ### 2. TeraFactory Implementation The core factory will provide these main functions: 1. **create_tera_engine(template_dirs)** - Creates a basic Tera engine with the specified template directories 2. **create_tera_with_rhai(template_dirs, hot_ast)** - Creates a Tera engine with Rhai function integration using a hot reloadable AST ### 3. RhaiFunctionAdapter We'll enhance the existing `RhaiFunctionAdapter` to work with the hot reloadable AST: ```rust /// Thread-safe adapter to use Rhai functions in Tera templates with hot reload support pub struct RhaiFunctionAdapter { fn_name: String, hot_ast: Arc>, } impl TeraFunction for RhaiFunctionAdapter { fn call(&self, args: &HashMap) -> TeraResult { // Convert args from Tera into Rhai's Dynamic let mut scope = Scope::new(); for (key, value) in args { // Convert Tera value to Rhai Dynamic let dynamic = convert_tera_to_rhai(value); scope.push_dynamic(key.clone(), dynamic); } // Create a new engine for each call let engine = Engine::new(); // Get a read lock on the AST let ast = self.hot_ast.read().unwrap(); // Call the function using the latest AST let result = engine .call_fn::(&mut scope, &ast, &self.fn_name, ()) .map_err(|e| tera::Error::msg(format!("Rhai error: {}", e)))?; // Convert Rhai result to Tera value let tera_value = convert_rhai_to_tera(&result); Ok(tera_value) } } ``` ### 4. TeraFactory API ```rust pub struct TeraFactory { // Configuration options } impl TeraFactory { /// Create a new TeraFactory with default settings pub fn new() -> Self { Self {} } /// Create a Tera engine with the specified template directories pub fn create_tera_engine>(&self, template_dirs: &[P]) -> Result { // Create a Tera engine with the specified template directories } /// Create a Tera engine with Rhai function integration pub fn create_tera_with_rhai>( &self, template_dirs: &[P], hot_ast: Arc> ) -> Result { // Create a Tera engine with Rhai function integration } } ``` ## Implementation Details ### 1. Creating a Tera Engine ```rust impl TeraFactory { pub fn create_tera_engine>(&self, template_dirs: &[P]) -> Result { let mut tera = Tera::default(); // Add templates from each directory for template_dir in template_dirs { let pattern = format!("{}/**/*.html", template_dir.as_ref().display()); match Tera::parse(&pattern) { Ok(parsed_tera) => { tera.extend(&parsed_tera).map_err(|e| { TeraFactoryError::new(format!("Failed to extend Tera with templates: {}", e)) })?; } Err(e) => { // If glob pattern fails, try to find individual HTML files let dir_path = template_dir.as_ref(); if let Ok(entries) = std::fs::read_dir(dir_path) { for entry in entries.filter_map(Result::ok) { let path = entry.path(); if path.extension().map_or(false, |ext| ext == "html") { let name = path.file_name().unwrap().to_string_lossy().to_string(); if let Ok(content) = std::fs::read_to_string(&path) { tera.add_raw_template(&name, &content).map_err(|e| { TeraFactoryError::new(format!("Failed to add template {}: {}", name, e)) })?; } } } } else { return Err(TeraFactoryError::new(format!( "Failed to parse templates from {} and could not read directory: {}", dir_path.display(), e ))); } } } } Ok(tera) } } ``` ### 2. Integrating with Rhai ```rust impl TeraFactory { pub fn create_tera_with_rhai>( &self, template_dirs: &[P], hot_ast: Arc> ) -> Result { // Create a basic Tera engine let mut tera = self.create_tera_engine(template_dirs)?; // Get a read lock on the AST to register functions let ast = hot_ast.read().unwrap(); // Register all functions from the AST for fn_def in ast.iter_functions() { let fn_name = fn_def.name.to_string(); // Create an adapter for this function let adapter = RhaiFunctionAdapter { fn_name: fn_name.clone(), hot_ast: Arc::clone(&hot_ast), }; // Register the function with Tera tera.register_function(&fn_name, adapter); } Ok(tera) } } ``` ### 3. Error Handling ```rust #[derive(Debug)] pub struct TeraFactoryError { message: String, source: Option>, } impl TeraFactoryError { pub fn new(message: impl Into) -> Self { Self { message: message.into(), source: None, } } pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self { self.source = Some(Box::new(source)); self } } impl fmt::Display for TeraFactoryError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.message) } } impl Error for TeraFactoryError { fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_ref().map(|s| s.as_ref() as &(dyn Error + 'static)) } } ``` ### 4. Value Conversion ```rust /// Convert a Tera value to a Rhai Dynamic value fn convert_tera_to_rhai(value: &Value) -> Dynamic { match value { Value::Null => Dynamic::UNIT, Value::Bool(b) => Dynamic::from(*b), Value::Number(n) => { if n.is_i64() { Dynamic::from(n.as_i64().unwrap()) } else if n.is_u64() { Dynamic::from(n.as_u64().unwrap()) } else { Dynamic::from(n.as_f64().unwrap()) } }, Value::String(s) => Dynamic::from(s.clone()), Value::Array(arr) => { let mut rhai_array = Vec::new(); for item in arr { rhai_array.push(convert_tera_to_rhai(item)); } Dynamic::from(rhai_array) }, Value::Object(obj) => { let mut rhai_map = rhai::Map::new(); for (key, value) in obj { rhai_map.insert(key.clone().into(), convert_tera_to_rhai(value)); } Dynamic::from(rhai_map) }, } } /// Convert a Rhai Dynamic value to a Tera value fn convert_rhai_to_tera(value: &Dynamic) -> Value { if value.is_unit() { Value::Null } else if value.is_bool() { Value::Bool(value.as_bool().unwrap()) } else if value.is_i64() { Value::Number(serde_json::Number::from(value.as_i64().unwrap())) } else if value.is_f64() { // This is a bit tricky as serde_json::Number doesn't have a direct from_f64 let f = value.as_f64().unwrap(); serde_json::to_value(f).unwrap() } else if value.is_string() { Value::String(value.to_string()) } else if value.is_array() { let arr = value.clone().into_array().unwrap(); let mut tera_array = Vec::new(); for item in arr { tera_array.push(convert_rhai_to_tera(&item)); } Value::Array(tera_array) } else if value.is_map() { let map = value.clone().into_map().unwrap(); let mut tera_object = serde_json::Map::new(); for (key, value) in map { tera_object.insert(key.to_string(), convert_rhai_to_tera(&value)); } Value::Object(tera_object) } else { // For any other type, convert to string Value::String(value.to_string()) } } ``` ## Testing Strategy ### 1. Unit Tests ```rust #[cfg(test)] mod tests { use super::*; #[test] fn create_tera_engine_with_valid_directories() { let factory = TeraFactory::new(); let template_dirs = vec!["tests/templates"]; let result = factory.create_tera_engine(&template_dirs); assert!(result.is_ok()); let tera = result.unwrap(); assert!(tera.get_template_names().count() > 0); } #[test] fn create_tera_with_rhai_registers_functions() { let rhai_factory = Arc::new(RhaiFactory::with_caching()); let tera_factory = TeraFactory::new(); // Compile a script with a simple function let script = "fn sum(a, b) { a + b }"; let engine = rhai_factory.create_engine(); let ast = engine.compile(script).unwrap(); let hot_ast = Arc::new(RwLock::new(ast)); // Create a Tera engine with Rhai integration let template_dirs = vec!["tests/templates"]; let result = tera_factory.create_tera_with_rhai(&template_dirs, hot_ast); assert!(result.is_ok()); // Verify the function is registered let tera = result.unwrap(); let mut context = tera::Context::new(); context.insert("a", &10); context.insert("b", &32); let rendered = tera.render("function_test.html", &context).unwrap(); assert_eq!(rendered.trim(), "42"); } #[test] fn hot_reload_updates_functions() { let rhai_factory = Arc::new(RhaiFactory::with_caching()); let tera_factory = TeraFactory::new(); // Create a temporary script file let temp_dir = tempfile::tempdir().unwrap(); let script_path = temp_dir.path().join("test.rhai"); std::fs::write(&script_path, "fn sum(a, b) { a + b }").unwrap(); // Compile the script let ast = rhai_factory.compile_modules(&[&script_path], None).unwrap(); let hot_ast = Arc::new(RwLock::new(ast)); // Enable hot reloading let handle = rhai_factory.enable_hot_reload( hot_ast.clone(), &[&script_path], None, None ).unwrap(); // Create a Tera engine with Rhai integration let template_dirs = vec!["tests/templates"]; let tera = tera_factory.create_tera_with_rhai(&template_dirs, hot_ast.clone()).unwrap(); // Render the template with the initial function let mut context = tera::Context::new(); context.insert("a", &10); context.insert("b", &32); let rendered = tera.render("function_test.html", &context).unwrap(); assert_eq!(rendered.trim(), "42"); // Modify the script to change the function std::fs::write(&script_path, "fn sum(a, b) { (a + b) * 2 }").unwrap(); // Wait for the file system to register the change std::thread::sleep(std::time::Duration::from_millis(100)); // Check for changes rhai_factory.check_for_changes().unwrap(); // Render the template again with the updated function let rendered = tera.render("function_test.html", &context).unwrap(); assert_eq!(rendered.trim(), "84"); // Disable hot reloading rhai_factory.disable_hot_reload(handle); } } ``` ### 2. Integration Tests ```rust #[test] fn integration_test_tera_with_hot_reloadable_rhai() { // Create the factories let rhai_factory = Arc::new(RhaiFactory::with_caching()); let tera_factory = TeraFactory::new(); // Set up test directories let scripts_dir = PathBuf::from("tests/rhai_scripts"); let templates_dir = PathBuf::from("tests/templates"); // Compile the initial script let script_path = scripts_dir.join("math.rhai"); let ast = rhai_factory.compile_modules(&[&script_path], Some(&scripts_dir)).unwrap(); let hot_ast = Arc::new(RwLock::new(ast)); // Enable hot reloading let handle = rhai_factory.enable_hot_reload( hot_ast.clone(), &[&script_path], Some(&scripts_dir), None ).unwrap(); // Create a Tera engine with Rhai integration let tera = tera_factory.create_tera_with_rhai(&[&templates_dir], hot_ast.clone()).unwrap(); // Render the template with the initial function let mut context = tera::Context::new(); context.insert("a", &20); context.insert("b", &22); let rendered = tera.render("math.html", &context).unwrap(); assert_eq!(rendered.trim(), "42"); // Modify the script to change the function let modified_script = r#" fn sum(a, b) { // Return twice the sum (a + b) * 2 } "#; std::fs::write(&script_path, modified_script).unwrap(); // Wait for the file system to register the change std::thread::sleep(std::time::Duration::from_millis(100)); // Check for changes rhai_factory.check_for_changes().unwrap(); // Render the template again with the updated function let rendered = tera.render("math.html", &context).unwrap(); assert_eq!(rendered.trim(), "84"); // Disable hot reloading rhai_factory.disable_hot_reload(handle); } ``` ## Example Usage Here's a complete example of how to use the TeraFactory with hot reloadable Rhai integration: ```rust use std::path::PathBuf; use std::sync::{Arc, RwLock}; use rhai_factory::{RhaiFactory, HotReloadableAST}; use tera_factory::TeraFactory; fn main() -> Result<(), Box> { // Create the factories let rhai_factory = Arc::new(RhaiFactory::with_caching()); let tera_factory = TeraFactory::new(); // Set up directories let scripts_dir = PathBuf::from("scripts"); let templates_dir = PathBuf::from("templates"); // Compile the initial script let script_path = scripts_dir.join("math.rhai"); let ast = rhai_factory.compile_modules(&[&script_path], Some(&scripts_dir))?; let hot_ast = Arc::new(RwLock::new(ast)); // Enable hot reloading let handle = rhai_factory.enable_hot_reload( hot_ast.clone(), &[&script_path], Some(&scripts_dir), Some(Box::new(|| println!("Script reloaded!"))) )?; // Create a Tera engine with Rhai integration let tera = tera_factory.create_tera_with_rhai(&[&templates_dir], hot_ast.clone())?; // Application loop loop { // Check for script changes rhai_factory.check_for_changes()?; // Render a template let mut context = tera::Context::new(); context.insert("a", &20); context.insert("b", &22); let rendered = tera.render("math.html", &context)?; println!("Rendered template: {}", rendered); // Wait a bit before checking again std::thread::sleep(std::time::Duration::from_secs(1)); // In a real application, you would break out of this loop when done } // Disable hot reloading when done rhai_factory.disable_hot_reload(handle); Ok(()) } ``` ## Test Files ### 1. Rhai Script (tests/rhai_scripts/math.rhai) ```rhai // Initial version of the sum function fn sum(a, b) { a + b } ``` ### 2. Tera Template (tests/templates/math.html) ```html {{ sum(a, b) }} ``` ## Conclusion The TeraFactory with hot reloadable Rhai integration provides a powerful way to create dynamic templates that can call Rhai functions. The hot reload feature allows these functions to be updated without restarting the application, making it ideal for development environments and systems where behavior needs to be modified dynamically. By leveraging the hot reload feature of the RhaiFactory, the TeraFactory can automatically update the available functions when Rhai scripts change, ensuring that templates always use the latest version of the functions.