This commit represents a major refactoring of our Rhai scripting system, transforming it from a factory-based approach to a more robust system-based architecture with improved hot reloading capabilities. Key Changes: - Renamed package from rhai_factory to rhai_system to better reflect its purpose - Renamed system_factory.rs to factory.rs for consistency and clarity - Implemented support for multiple script files in hot reloading - Added cross-script function calls, allowing functions in one script to call functions in another - Improved file watching to monitor all script files for changes - Enhanced error handling for script compilation failures - Simplified the API with a cleaner create_hot_reloadable_system function - Removed unused modules (error.rs, factory.rs, hot_reload_old.rs, module_cache.rs, relative_resolver.rs) - Updated all tests to work with the new architecture The new architecture: - Uses a System struct that holds references to script paths and provides a clean API - Compiles and merges multiple Rhai script files into a single AST - Automatically detects changes to any script file and recompiles them - Maintains thread safety with proper synchronization primitives - Provides better error messages when scripts fail to compile This refactoring aligns with our BasePathModuleResolver approach for module imports, making the resolution process more predictable and consistent. The hot reload example has been updated to demonstrate the new capabilities, showing how to: 1. Load and execute multiple script files 2. Watch for changes to these files 3. Automatically reload scripts when they change 4. Call functions across different script files All tests are passing, and the example demonstrates the improved functionality.
547 lines
17 KiB
Markdown
547 lines
17 KiB
Markdown
# 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<RwLock<AST>>,
|
|
}
|
|
|
|
impl TeraFunction for RhaiFunctionAdapter {
|
|
fn call(&self, args: &HashMap<String, Value>) -> TeraResult<Value> {
|
|
// 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::<Dynamic>(&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<P: AsRef<Path>>(&self, template_dirs: &[P])
|
|
-> Result<Tera, TeraFactoryError> {
|
|
// Create a Tera engine with the specified template directories
|
|
}
|
|
|
|
/// Create a Tera engine with Rhai function integration
|
|
pub fn create_tera_with_rhai<P: AsRef<Path>>(
|
|
&self,
|
|
template_dirs: &[P],
|
|
hot_ast: Arc<RwLock<AST>>
|
|
) -> Result<Tera, TeraFactoryError> {
|
|
// Create a Tera engine with Rhai function integration
|
|
}
|
|
}
|
|
```
|
|
|
|
## Implementation Details
|
|
|
|
### 1. Creating a Tera Engine
|
|
|
|
```rust
|
|
impl TeraFactory {
|
|
pub fn create_tera_engine<P: AsRef<Path>>(&self, template_dirs: &[P])
|
|
-> Result<Tera, TeraFactoryError> {
|
|
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<P: AsRef<Path>>(
|
|
&self,
|
|
template_dirs: &[P],
|
|
hot_ast: Arc<RwLock<AST>>
|
|
) -> Result<Tera, TeraFactoryError> {
|
|
// 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<Box<dyn Error + Send + Sync>>,
|
|
}
|
|
|
|
impl TeraFactoryError {
|
|
pub fn new(message: impl Into<String>) -> 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<dyn std::error::Error>> {
|
|
// 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. |