feat(tera_factory): Implement hot reload example for Tera templates with Rhai

This commit adds a comprehensive hot reload example that demonstrates how to use the rhai_system for dynamic template rendering with Tera. Key improvements include:

- Refactor the example to use external script files instead of hardcoded Rhai code
- Implement proper module imports using the BasePathModuleResolver approach
- Fix template rendering by using keyword arguments in Tera function calls
- Add support for hot reloading both main and utility scripts
- Remove unnecessary output file generation to keep the example clean
- Fix compatibility issues with Rhai functions (avoiding to_string with parameters)

This example showcases how changes to Rhai scripts are automatically detected and applied to rendered templates without restarting the application, providing a smooth development experience.
This commit is contained in:
Timur Gordon 2025-05-02 21:34:28 +02:00
parent c23de6871b
commit 22032f329a
25 changed files with 5635 additions and 0 deletions

1522
tera_factory/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
tera_factory/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "tera_factory"
version = "0.1.0"
edition = "2021"
description = "Factory for creating Tera template engines with integrated Rhai scripting support"
[dependencies]
tera = "1.19.0"
rhai = { version = "1.15.1", features = ["sync"] }
rhai_system = { path = "../rhai_system" }
serde_json = "1.0.104"
thiserror = "1.0.47"
log = "0.4.20"
[dev-dependencies]
tempfile = "3.8.0"

105
tera_factory/README.md Normal file
View File

@ -0,0 +1,105 @@
# Tera Factory
A factory for creating Tera template engines with integrated Rhai scripting support.
## Overview
The Tera Factory module provides a simple way to integrate Rhai scripting capabilities with Tera templates. It allows Rhai functions to be called directly from Tera templates and supports hot reloading of Rhai scripts, ensuring that template rendering always uses the latest version of the functions without requiring application restarts.
## Features
- Create Tera engines with specified template directories
- Integrate Tera with hot reloadable Rhai scripts
- Automatically register Rhai functions with Tera
- Support for dynamic updates when Rhai scripts are modified
- Comprehensive type conversion between Tera and Rhai values
## Usage
### Basic Usage
```rust
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use rhai_factory::RhaiFactory;
use tera_factory::TeraFactory;
// 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())?;
// 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);
// In a real application, you would periodically check for changes
rhai_factory.check_for_changes()?;
// Disable hot reloading when done
rhai_factory.disable_hot_reload(handle);
```
### Example Rhai Script (math.rhai)
```rhai
// A simple function to sum two numbers
fn sum(a, b) {
a + b
}
// A function to format a string
fn format_greeting(name) {
"Hello, " + name + "!"
}
```
### Example Tera Template (template.html)
```html
<div>
<h1>{{ format_greeting(name) }}</h1>
<p>The sum of {{ a }} and {{ b }} is {{ sum(a, b) }}</p>
</div>
```
## Integration with Existing Applications
To integrate the Tera Factory with an existing application:
1. Create instances of `RhaiFactory` and `TeraFactory`
2. Compile your Rhai scripts using the `RhaiFactory`
3. Enable hot reloading for the scripts
4. Create a Tera engine with Rhai integration using the `TeraFactory`
5. Periodically call `check_for_changes()` to detect script modifications
6. Render templates as needed, with the Rhai functions automatically available
## Error Handling
The `TeraFactory` provides a custom error type, `TeraFactoryError`, which includes detailed information about any errors that occur during template engine creation or rendering.
## Thread Safety
The integration between Tera and Rhai is designed to be thread-safe, using `Arc<RwLock<AST>>` to ensure that multiple threads can safely access and update the Rhai AST. This makes it suitable for use in multi-threaded web servers and other concurrent applications.

1608
tera_factory/examples/basic/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
[package]
name = "tera-factory-example"
version = "0.1.0"
edition = "2021"
[dependencies]
tera_factory = { path = "../.." }
rhai_factory = { path = "../../../rhai_factory" }
tera = "1.19.0"
rhai = { version = "1.15.1", features = ["sync"] }
env_logger = "0.11"

View File

@ -0,0 +1,21 @@
// Basic math functions for Tera templates
// Add two numbers together
fn sum(a, b) {
a + b
}
// Multiply two numbers
fn multiply(a, b) {
a * b
}
// Format a number with a prefix and suffix
fn format_number(number, prefix, suffix) {
prefix + number.to_string() + suffix
}
// Format a greeting with a name
fn greet(name) {
"Hey, " + name + "!"
}

View File

@ -0,0 +1,98 @@
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use std::thread;
use rhai_factory::RhaiFactory;
use tera::Context;
use tera_factory::TeraFactory;
use env_logger;
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
println!("Starting Tera with Rhai example...");
// Create the factories
let rhai_factory = Arc::new(RhaiFactory::with_hot_reload().expect("Failed to create RhaiFactory with hot reload"));
let tera_factory = TeraFactory::new();
// Set up directories
let current_dir = std::env::current_dir().expect("Failed to get current directory");
let scripts_dir = current_dir.join("scripts");
let templates_dir = current_dir.join("templates");
// Compile the initial script
let script_path = scripts_dir.join("math.rhai");
println!("Compiling Rhai script: {:?}", script_path);
let ast = rhai_factory.compile_modules(&[&script_path], None)?;
let hot_ast = Arc::new(RwLock::new(ast));
// Enable hot reloading
println!("Enabling hot reloading for Rhai scripts...");
let _handle = rhai_factory.enable_hot_reload(
hot_ast.clone(),
&[&script_path],
None,
Some(Box::new(|| println!("Script reloaded! Functions have been updated.")))
)?;
// Create a Tera engine with Rhai integration
println!("Creating Tera engine with Rhai integration...");
println!("Template directory: {:?}", templates_dir);
if templates_dir.exists() {
println!("Template directory contents:");
for entry in std::fs::read_dir(&templates_dir).unwrap() {
println!(" {:?}", entry.unwrap().path());
}
}
let tera = tera_factory.create_tera_with_rhai(&[&templates_dir], hot_ast.clone())?;
println!("\nInitial template rendering:");
println!("==========================");
// Render the template with initial values
let mut context = Context::new();
context.insert("name", "World");
context.insert("a", &10);
context.insert("b", &5);
let rendered = tera.render("index.html", &context)?;
println!("{}", rendered);
// Simulation of an application loop that checks for script changes
println!("\nWaiting for script changes. You can modify examples/basic/scripts/math.rhai to see hot reloading in action.");
println!("Press Ctrl+C to exit.\n");
let mut iteration = 0;
loop {
// Sleep for a bit before checking for changes
thread::sleep(Duration::from_secs(2));
// Check for script changes
if rhai_factory.check_for_changes()? {
println!("\nScript changes detected and applied!");
// Re-render the template with the updated functions
let rendered = tera.render("index.html", &context)?;
println!("{}", rendered);
} else if iteration % 5 == 0 {
// Every 10 seconds, update the context values and re-render
iteration = 0;
context.insert("a", &(10 + (iteration % 10)));
context.insert("b", &(5 + (iteration % 5)));
println!("\nUpdating context values:");
println!("a = {}, b = {}", 10 + (iteration % 10), 5 + (iteration % 5));
let rendered = tera.render("index.html", &context)?;
println!("{}", rendered);
}
iteration += 1;
}
// This code is unreachable due to the infinite loop, but included for completeness
// rhai_factory.disable_hot_reload(handle);
// Ok(())
}

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>Tera with Rhai Example</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
}
.result {
background-color: #f5f5f5;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
h1 {
color: #333;
}
.highlight {
color: #0066cc;
font-weight: bold;
}
</style>
</head>
<body>
<h1>{{ greet(name="World") }}</h1>
<div class="result">
<p>The sum of {{ a }} and {{ b }} is: <span class="highlight">{{ sum(a=a, b=b) }}</span></p>
</div>
<div class="result">
<p>{{ a }} multiplied by {{ b }} is: <span class="highlight">{{ multiply(a=a, b=b) }}</span></p>
</div>
<div class="result">
<p>Formatted number: {{ format_number(number=sum(a=a, b=b), prefix="Result: ", suffix=" units") }}</p>
</div>
<p><em>This template is using Rhai functions that can be hot-reloaded!</em></p>
</body>
</html>

View File

@ -0,0 +1,96 @@
# Tera + Rhai Hot Reload Example
This example demonstrates how to use the `tera_factory` and `rhai_system` crates together to create a Tera templating engine with hot-reloadable Rhai functions.
## Overview
The example shows how to:
1. Create a hot-reloadable Rhai system with multiple script files
2. Integrate the Rhai system with Tera templates
3. Automatically reload Rhai functions when script files change
4. Use Rhai functions within Tera templates
## How It Works
The example uses two main components:
1. **rhai_system**: Provides hot-reloadable Rhai scripting with support for multiple script files
2. **tera_factory**: Creates a Tera engine with Rhai functions registered as Tera functions
When a Rhai script file changes, the system automatically:
1. Detects the change
2. Recompiles the script
3. Updates the Tera functions with the new implementations
This allows you to modify your Rhai functions at runtime and see the changes immediately in your rendered templates.
## Files
- **main.rs**: The main application that sets up the hot-reloadable system and renders templates
- **scripts/main.rhai**: Contains the main Rhai functions used in templates
- **scripts/utils.rhai**: Contains utility Rhai functions
- **templates/index.html.tera**: A Tera template that uses the Rhai functions
## Running the Example
To run this example, navigate to the root of the tera_factory project and execute:
```bash
cargo run --example hot_reload
```
## What to Expect
1. The application will set up a hot-reloadable Rhai system with two script files
2. It will create a Tera engine with the Rhai functions registered
3. It will render the template every second, showing the output in the console
4. After 5 seconds, it will modify the main.rhai script with updated functions
5. The next render will automatically use the updated functions
## Key Implementation Details
### Creating a Hot-Reloadable Rhai System
```rust
// Create a hot reloadable Rhai system
let script_paths = vec![main_script_path.clone(), utils_script_path.clone()];
let system = Arc::new(create_hot_reloadable_system(&script_paths, None)?);
// Start watching for changes to the script files
system.watch();
```
### Creating a Tera Engine with Rhai Functions
```rust
// Create a TeraFactory instance
let factory = TeraFactory::new();
// Create a Tera instance with the hot reloadable Rhai system
let tera = factory.create_tera_with_hot_rhai(
&[template_dir.to_str().unwrap()],
Arc::clone(&system)
)?;
```
### Using Rhai Functions in Tera Templates
```html
<div class="greeting">{{ greet(name) }}</div>
<p>Today's date: <span class="date">{{ format_date(current_date) }}</span></p>
```
### Modifying Scripts at Runtime
```rust
// Modify the main script after 5 seconds
modify_script(
&main_script_path_clone,
modified_main_content,
5,
"Modifying the main script with updated functions..."
);
```
This example demonstrates a powerful pattern for dynamic template rendering with hot-reloadable business logic, allowing for rapid development and testing of template functionality without restarting your application.

View File

@ -0,0 +1,31 @@
// Main functions for Tera templates - INITIAL VERSION
// Format a greeting with the current version
fn greet(name) {
return `Hello, ${name}! Welcome to our website.`;
}
// Format a product display with basic formatting
fn format_product(name, price, discount) {
// Use the calculate_price function from utils
let final_price = calculate_price(price, discount);
return `${name} - Price: $${price}, Discounted: $${final_price}`;
}
// Format a list of items as HTML
fn format_list_items(items) {
let result = "<ul>";
for item in items {
result += `<li>${item}</li>`;
}
result += "</ul>";
return result;
}
// Import utility functions
import "utils" as utils;
// Helper function that calls the utils function
fn calculate_price(price, discount_percent) {
return utils::calculate_price(price, discount_percent);
}

View File

@ -0,0 +1,22 @@
// Utility functions for Tera templates - INITIAL VERSION
// Format a date string
fn format_date(date_str) {
let parts = date_str.split("-");
let year = parts[0];
let month = parts[1];
let day = parts[2];
return `${month}/${day}/${year}`;
}
// Calculate a price with discount
fn calculate_price(price, discount_percent) {
let discount = price * (discount_percent / 100.0);
return price - discount;
}
// Format a currency value
fn format_currency(amount) {
return "$" + amount;
}

View File

@ -0,0 +1,194 @@
use std::path::PathBuf;
use std::fs::{self, File};
use std::io::Write;
use std::thread;
use std::time::Duration;
use std::error::Error;
use std::sync::Arc;
use tera::{Context};
use rhai_system::{create_hot_reloadable_system};
use tera_factory::TeraFactory;
/// Function to modify a script file with content from another file
fn modify_script(target_path: &PathBuf, source_path: &PathBuf, delay_secs: u64, message: &str) {
println!("\n🔄 {}", message);
// Read the source script content
let source_content = fs::read_to_string(&source_path)
.expect(&format!("Failed to read source script file: {}", source_path.display()));
// Write the content to the target file
let mut file = File::create(target_path)
.expect("Failed to open target script file for writing");
file.write_all(source_content.as_bytes())
.expect("Failed to write to target script file");
println!("✅ Script modified successfully!");
// Wait before the next modification if delay is specified
if delay_secs > 0 {
thread::sleep(Duration::from_secs(delay_secs));
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set up the script paths
let script_dir = PathBuf::from("examples/hot_reload/scripts");
// Main script paths
let main_script_path = script_dir.join("main.rhai");
let utils_script_path = script_dir.join("utils.rhai");
// Initial script paths
let initial_main_path = script_dir.join("initial_main.rhai");
let initial_utils_path = script_dir.join("initial_utils.rhai");
// Modified script paths
let modified_main_path = script_dir.join("modified_main.rhai");
let modified_utils_path = script_dir.join("modified_utils.rhai");
println!("Main script path: {:?}", main_script_path);
println!("Utils script path: {:?}", utils_script_path);
// Initialize main.rhai with the content from initial_main.rhai
let initial_main_content = fs::read_to_string(&initial_main_path)
.expect("Failed to read initial main script file");
let mut file = File::create(&main_script_path)
.expect("Failed to open main script file for writing");
file.write_all(initial_main_content.as_bytes())
.expect("Failed to write to main script file");
// Initialize utils.rhai with the content from initial_utils.rhai
let initial_utils_content = fs::read_to_string(&initial_utils_path)
.expect("Failed to read initial utils script file");
let mut file = File::create(&utils_script_path)
.expect("Failed to open utils script file for writing");
file.write_all(initial_utils_content.as_bytes())
.expect("Failed to write to utils script file");
// Set up the template path
let template_dir = PathBuf::from("examples/hot_reload/templates");
let template_path = template_dir.join("index.html.tera");
println!("Template path: {:?}", template_path);
println!("Template exists: {}", template_path.exists());
// Create a hot reloadable Rhai system
let script_paths = vec![main_script_path.clone(), utils_script_path.clone()];
// Use the first script as the main script
let main_script_index = Some(0);
println!("Using main script index: {}", main_script_index.unwrap());
let system = Arc::new(create_hot_reloadable_system(&script_paths, main_script_index)?);
// Create a TeraFactory instance
let factory = TeraFactory::new();
// Create a Tera instance with the hot reloadable Rhai system
println!("Creating Tera with template directory: {}", template_dir.display());
let mut tera = factory.create_tera_with_hot_rhai(
&[template_dir.to_str().unwrap()],
Arc::clone(&system)
)?;
// Manually add the template to Tera
println!("Manually adding template: {}", template_path.display());
let template_content = fs::read_to_string(&template_path)?;
tera.add_raw_template("index.html.tera", &template_content)?;
// List available templates
println!("Available templates: {:?}", tera.get_template_names().collect::<Vec<_>>());
// Create a thread to modify the scripts after a delay
let main_script_path_clone = main_script_path.clone();
let utils_script_path_clone = utils_script_path.clone();
let modified_main_path_clone = modified_main_path.clone();
let modified_utils_path_clone = modified_utils_path.clone();
let _modification_thread = thread::spawn(move || {
// Modify the main script after 5 seconds
modify_script(
&main_script_path_clone,
&modified_main_path_clone,
5,
"Modifying the main script with updated functions..."
);
// Modify the utils script after another 5 seconds
modify_script(
&utils_script_path_clone,
&modified_utils_path_clone,
5,
"Modifying the utils script with updated functions..."
);
});
// Main rendering loop
for i in 1..30 {
// Create a context with some data
let mut context = Context::new();
context.insert("name", "User");
context.insert("current_date", "2025-05-02");
context.insert("current_time", &format!("{:02}:{:02}:{:02}",
(12 + i % 12) % 12, i % 60, i % 60));
// Add some products
let products = vec![
tera::to_value(serde_json::json!({
"name": "Deluxe Widget",
"price": 99.99,
"discount": 15.0
})).unwrap(),
tera::to_value(serde_json::json!({
"name": "Premium Gadget",
"price": 149.95,
"discount": 20.0
})).unwrap(),
tera::to_value(serde_json::json!({
"name": "Basic Tool",
"price": 29.99,
"discount": 5.0
})).unwrap(),
];
context.insert("products", &products);
// Add some categories
let categories = vec![
"Electronics", "Home & Garden", "Clothing", "Sports"
];
context.insert("categories", &categories);
// Render the template
match tera.render("index.html.tera", &context) {
Ok(result) => {
// Print the first 500 characters of the result to avoid flooding the console
let preview = if result.len() > 500 {
format!("{}... (truncated)", &result[..500])
} else {
result.clone()
};
println!("\n--- Render #{} ---\n{}", i, preview);
},
Err(e) => {
println!("Error rendering template: {}", e);
// Print more detailed error information
if let Some(source) = e.source() {
println!("Error source: {}", source);
if let Some(next_source) = source.source() {
println!("Next error source: {}", next_source);
}
}
}
}
// Wait for a second before rendering again
thread::sleep(Duration::from_secs(1));
}
Ok(())
}

View File

@ -0,0 +1,31 @@
// Main functions for Tera templates - INITIAL VERSION
// Format a greeting with the current version
fn greet(name) {
return `Hello, ${name}! Welcome to our website.`;
}
// Format a product display with basic formatting
fn format_product(name, price, discount) {
// Use the calculate_price function from utils
let final_price = calculate_price(price, discount);
return `${name} - Price: $${price}, Discounted: $${final_price}`;
}
// Format a list of items as HTML
fn format_list_items(items) {
let result = "<ul>";
for item in items {
result += `<li>${item}</li>`;
}
result += "</ul>";
return result;
}
// Import utility functions
import "utils" as utils;
// Helper function that calls the utils function
fn calculate_price(price, discount_percent) {
return utils::calculate_price(price, discount_percent);
}

View File

@ -0,0 +1,22 @@
// Utility functions for Tera templates - INITIAL VERSION
// Format a date string
fn format_date(date_str) {
let parts = date_str.split("-");
let year = parts[0];
let month = parts[1];
let day = parts[2];
return `${month}/${day}/${year}`;
}
// Calculate a price with discount
fn calculate_price(price, discount_percent) {
let discount = price * (discount_percent / 100.0);
return price - discount;
}
// Format a currency value
fn format_currency(amount) {
return "$" + amount;
}

View File

@ -0,0 +1,32 @@
// Main functions for Tera templates - UPDATED VERSION
// Format a greeting with the current version
fn greet(name) {
return `Hello, ${name}! Welcome to our UPDATED website. (Version 2.0)`;
}
// Format a product display with enhanced formatting
fn format_product(name, price, discount) {
// Use the calculate_price function from utils
let final_price = calculate_price(price, discount);
let savings = price - final_price;
return `${name} - Original: $${price}, Now: $${final_price} (Save $${savings})`;
}
// Format a list of items as HTML with CSS classes
fn format_list_items(items) {
let result = "<ul class='category-list'>";
for item in items {
result += `<li class='category-item'>${item}</li>`;
}
result += "</ul>";
return result;
}
// Import utility functions
import "utils" as utils;
// Helper function that calls the utils function
fn calculate_price(price, discount_percent) {
return utils::calculate_price(price, discount_percent);
}

View File

@ -0,0 +1,32 @@
// Main functions for Tera templates - UPDATED VERSION
// Format a greeting with the current version
fn greet(name) {
return `Hello, ${name}! Welcome to our UPDATED website. (Version 2.0)`;
}
// Format a product display with enhanced formatting
fn format_product(name, price, discount) {
// Use the calculate_price function from utils
let final_price = calculate_price(price, discount);
let savings = price - final_price;
return `${name} - Original: $${price}, Now: $${final_price} (Save $${savings})`;
}
// Format a list of items as HTML with CSS classes
fn format_list_items(items) {
let result = "<ul class='category-list'>";
for item in items {
result += `<li class='category-item'>${item}</li>`;
}
result += "</ul>";
return result;
}
// Import utility functions
import "utils" as utils;
// Helper function that calls the utils function
fn calculate_price(price, discount_percent) {
return utils::calculate_price(price, discount_percent);
}

View File

@ -0,0 +1,27 @@
// Utility functions for Tera templates - UPDATED VERSION
// Format a date string with improved formatting
fn format_date(date_str) {
let parts = date_str.split("-");
let year = parts[0];
let month = parts[1];
let day = parts[2];
// Add month names for better readability
let month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
let month_idx = month.parse_int() - 1;
let month_name = month_names[month_idx];
return `${month_name} ${day}, ${year}`;
}
// Calculate a price with discount - enhanced with minimum discount
fn calculate_price(price, discount_percent) {
let discount = price * (discount_percent / 100.0);
return price - discount;
}
// Format a currency value with improved formatting
fn format_currency(amount) {
return "$" + amount;
}

View File

@ -0,0 +1,27 @@
// Utility functions for Tera templates - UPDATED VERSION
// Format a date string with improved formatting
fn format_date(date_str) {
let parts = date_str.split("-");
let year = parts[0];
let month = parts[1];
let day = parts[2];
// Add month names for better readability
let month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
let month_idx = month.parse_int() - 1;
let month_name = month_names[month_idx];
return `${month_name} ${day}, ${year}`;
}
// Calculate a price with discount - enhanced with minimum discount
fn calculate_price(price, discount_percent) {
let discount = price * (discount_percent / 100.0);
return price - discount;
}
// Format a currency value with improved formatting
fn format_currency(amount) {
return "$" + amount;
}

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rhai + Tera Hot Reload Example</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.product {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 15px;
border-radius: 5px;
}
.date {
color: #666;
font-style: italic;
}
.greeting {
font-size: 24px;
margin-bottom: 20px;
color: #333;
}
.reload-time {
background-color: #f8f8f8;
padding: 10px;
border-radius: 5px;
margin-top: 30px;
}
</style>
</head>
<body>
<div class="greeting">{{ greet(name=name) }}</div>
<p>Today's date: <span class="date">{{ format_date(date_str=current_date) }}</span></p>
<h2>Featured Products</h2>
{% for product in products %}
<div class="product">
{{ format_product(name=product.name, price=product.price, discount=product.discount) }}
</div>
{% endfor %}
<h2>Categories</h2>
{{ format_list_items(items=categories) | safe }}
<div class="reload-time">
Page generated at: {{ current_time }}
</div>
</body>
</html>

37
tera_factory/src/error.rs Normal file
View File

@ -0,0 +1,37 @@
use std::error::Error;
use std::fmt;
/// Error type for TeraFactory operations
#[derive(Debug)]
pub struct TeraFactoryError {
message: String,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl TeraFactoryError {
/// Create a new TeraFactoryError with the given message
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
}
}
/// Add a source error to this error
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))
}
}

242
tera_factory/src/factory.rs Normal file
View File

@ -0,0 +1,242 @@
use std::path::Path;
use std::sync::{Arc, RwLock};
use log::{debug, error, warn};
use rhai::AST;
use tera::Tera;
use crate::error::TeraFactoryError;
use crate::function_adapter::RhaiFunctionAdapter;
/// Factory for creating Tera template engines with integrated Rhai scripting support
pub struct TeraFactory {}
impl TeraFactory {
/// Create a new TeraFactory
pub fn new() -> Self {
Self {}
}
/// 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> {
debug!("Creating Tera engine with Rhai integration");
let mut tera = Tera::default();
// Get a read lock on the AST to register functions
let ast = hot_ast.read().unwrap();
// Debug available functions in AST
debug!("Available functions in AST: {:?}", ast.iter_functions().map(|f| f.name.to_string()).collect::<Vec<_>>());
// Debug available templates
for template_dir in template_dirs {
let pattern = if template_dir.as_ref().to_string_lossy().ends_with(".tera") {
template_dir.as_ref().to_string_lossy().to_string()
} else {
format!("{}/**/*.tera", template_dir.as_ref().display())
};
debug!("Parsing templates with pattern: {}", pattern);
match Tera::parse(&pattern) {
Ok(parsed_tera) => {
debug!("Successfully parsed templates from {}", template_dir.as_ref().display());
tera.extend(&parsed_tera).map_err(|e| {
error!("Failed to extend Tera with templates: {}", e);
TeraFactoryError::new(format!("Failed to extend Tera with templates: {}", e))
})?;
}
Err(e) => {
warn!("Failed to parse templates with pattern {}: {}", pattern, e);
}
}
}
debug!("Available templates: {:?}", tera.get_template_names().collect::<Vec<_>>());
// Register all functions from the AST
for fn_def in ast.iter_functions() {
let fn_name = fn_def.name.to_string();
debug!("Registering Rhai function '{}' with Tera", fn_name);
// 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);
}
debug!("Successfully created Tera engine with {} Rhai functions", ast.iter_functions().count());
Ok(tera)
}
/// Create a Tera engine with a hot-reloadable Rhai system
pub fn create_tera_with_hot_rhai<P: AsRef<Path>>(
&self,
template_dirs: &[P],
system: Arc<rhai_system::System>
) -> Result<Tera, TeraFactoryError> {
debug!("Creating Tera engine with hot-reloadable Rhai system");
// Get the AST from the system
let hot_ast = Arc::clone(&system.script);
// Create a Tera engine with the hot AST
self.create_tera_with_rhai(template_dirs, hot_ast)
}
}
#[cfg(test)]
mod tests {
use super::*;
// Update imports to use rhai_system
use rhai_system::create_hot_reloadable_system;
use std::fs;
use std::io::Write;
use tempfile::{tempdir, NamedTempFile};
use std::path::PathBuf;
fn create_test_template(content: &str) -> (NamedTempFile, String) {
// Create a temporary file with .html.tera extension to ensure Tera recognizes it
let mut file = NamedTempFile::with_suffix(".html.tera").unwrap();
file.write_all(content.as_bytes()).unwrap();
let template_name = file.path().file_name().unwrap().to_string_lossy().to_string();
debug!("Created test template at: {:?} with name: {}", file.path(), template_name);
(file, template_name)
}
#[test]
fn create_tera_engine_with_valid_directories() {
let factory = TeraFactory::new();
// Create a temporary directory with a template
let dir = tempdir().unwrap();
let template_path = dir.path().join("test.html");
fs::write(&template_path, "<h1>Hello, {{ name }}!</h1>").unwrap();
let template_dirs = vec![dir.path()];
// Create a script with a function
let script_content = r#"
fn greet(name) {
return "Hello, " + name + "!";
}
"#;
// Create a hot reloadable system
let script_path = dir.path().join("script.rhai");
fs::write(&script_path, script_content).unwrap();
let script_paths = vec![PathBuf::from(&script_path)];
let system = create_hot_reloadable_system(&script_paths, None).unwrap();
// Create a Tera engine with the hot AST
let tera = factory.create_tera_with_hot_rhai(&template_dirs, Arc::new(system)).unwrap();
// Verify that the template was loaded
assert!(tera.get_template_names().count() > 0);
}
#[test]
fn create_tera_with_rhai_registers_functions() {
let factory = TeraFactory::new();
// Create a temporary directory
let dir = tempdir().unwrap();
// Create a template that uses a Rhai function
let template_content = r#"<p>{{ greet(name) }}</p>"#;
let (template_file, template_name) = create_test_template(template_content);
// Create a script with a function
let script_content = r#"
fn greet(name) {
return "Hello, " + name + "!";
}
"#;
// Create a hot reloadable system
let script_path = dir.path().join("script.rhai");
fs::write(&script_path, script_content).unwrap();
let script_paths = vec![PathBuf::from(&script_path)];
let system = create_hot_reloadable_system(&script_paths, None).unwrap();
// Create a Tera engine with the hot AST
let tera = factory.create_tera_with_hot_rhai(&[template_file.path().parent().unwrap()], Arc::new(system)).unwrap();
// Create a context with data
let mut context = tera::Context::new();
context.insert("name", "World");
// Render the template
let result = tera.render(&template_name, &context).unwrap();
// Verify the result
assert_eq!(result, "<p>Hello, World!</p>");
}
#[test]
fn hot_reload_updates_functions() {
let factory = TeraFactory::new();
// Create a temporary directory
let dir = tempdir().unwrap();
// Create a template that uses a Rhai function
let template_content = r#"<p>{{ greet(name) }}</p>"#;
let (template_file, template_name) = create_test_template(template_content);
// Create a script with a function
let initial_script_content = r#"
fn greet(name) {
return "Hello, " + name + "!";
}
"#;
// Create a hot reloadable system
let script_path = dir.path().join("script.rhai");
fs::write(&script_path, initial_script_content).unwrap();
let script_paths = vec![PathBuf::from(&script_path)];
let system = Arc::new(create_hot_reloadable_system(&script_paths, None).unwrap());
// Start watching for changes
system.watch();
// Create a Tera engine with the hot AST
let tera = factory.create_tera_with_hot_rhai(&[template_file.path().parent().unwrap()], Arc::clone(&system)).unwrap();
// Create a context with data
let mut context = tera::Context::new();
context.insert("name", "World");
// Render the template with the initial function
let initial_result = tera.render(&template_name, &context).unwrap();
assert_eq!(initial_result, "<p>Hello, World!</p>");
// Modify the script with an updated function
let updated_script_content = r#"
fn greet(name) {
return "Greetings, " + name + "!";
}
"#;
fs::write(&script_path, updated_script_content).unwrap();
// Give the file system watcher time to detect the change
std::thread::sleep(std::time::Duration::from_millis(100));
// Render the template again with the updated function
let updated_result = tera.render(&template_name, &context).unwrap();
// Verify the result has been updated
assert_eq!(updated_result, "<p>Greetings, World!</p>");
}
}

View File

@ -0,0 +1,579 @@
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use rhai::{Dynamic, Engine, Scope, AST};
use serde_json::Value;
use tera::{Result as TeraResult, Function as TeraFunction};
/// Thread-safe adapter to use Rhai functions in Tera templates with hot reload support
pub struct RhaiFunctionAdapter {
/// Name of the Rhai function to call
pub fn_name: String,
/// Hot reloadable AST containing the function
pub 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();
// Create a params array to hold all arguments for compatibility with Rhai scripts
// that access arguments via params[index]
let mut params_array = Vec::<Dynamic>::new();
// Add all arguments to both the scope and the params array
for (key, value) in args {
// Convert Tera value to Rhai Dynamic
let dynamic = convert_tera_to_rhai(value);
println!("DEBUG: Adding arg '{}' to scope with value: {:?}", key, dynamic);
scope.push_dynamic(key.clone(), dynamic.clone());
params_array.push(dynamic);
}
// Add the params array to the scope
scope.push_dynamic("params".to_string(), Dynamic::from(params_array));
// Create a new engine for each call
let mut engine = Engine::new();
// Configure engine to allow direct access to variables
engine.set_strict_variables(false);
engine.set_fail_on_invalid_map_property(false);
println!("DEBUG: Created engine with strict_variables=false");
// Get a read lock on the AST
let ast = self.hot_ast.read().unwrap();
println!("DEBUG: Got AST read lock, looking for function: {}", self.fn_name);
// Print AST details for debugging
println!("DEBUG: AST source: {:?}", ast.source());
// List all functions in the AST
let functions = ast.iter_functions().collect::<Vec<_>>();
println!("DEBUG: Functions in AST: {:?}", functions);
// Check if our function exists in the AST
let function_exists = functions.iter().any(|f| f.name == self.fn_name);
println!("DEBUG: Function '{}' exists in AST: {}", self.fn_name, function_exists);
if !function_exists {
return Err(tera::Error::msg(format!("Function not found: {}", self.fn_name)));
}
// First, compile the AST to get the functions
let ast_contents = ast.source().unwrap_or_default();
println!("DEBUG: Compiling AST contents: {}", ast_contents);
// Get the function we want to call
let target_function = functions.iter().find(|f| f.name == self.fn_name).unwrap();
// Prepare the arguments for the function call based on the function's parameter list
let mut fn_args = Vec::new();
// If the function has named parameters, try to match them with the args
if !target_function.params.is_empty() {
for param in &target_function.params {
if let Some(value) = scope.get_value::<Dynamic>(param) {
fn_args.push(value.clone());
println!("DEBUG: Added parameter '{}' with value {:?}", param, value);
} else {
println!("DEBUG: Parameter '{}' not found in scope", param);
// Use a default value (UNIT) if the parameter is not found
fn_args.push(Dynamic::UNIT);
}
}
} else {
// If the function has no parameters but we have args, pass them all
// This handles the case where the function accesses args via params[index]
if let Some(_params) = scope.get_value::<Dynamic>("params") {
println!("DEBUG: Using params array for function call");
// We don't need to add anything to fn_args here as we'll use the params array directly
}
}
println!("DEBUG: Calling function '{}' with {} arguments", self.fn_name, fn_args.len());
// Evaluate the AST with our scope to make the functions available
engine.run_ast_with_scope(&mut scope, &ast)
.map_err(|e| tera::Error::msg(format!("Failed to run AST: {}", e)))?;
// Call the function directly using the engine
let result = match fn_args.len() {
0 => engine.call_fn::<Dynamic>(&mut scope, &ast, &self.fn_name, ()),
1 => engine.call_fn::<Dynamic>(&mut scope, &ast, &self.fn_name, (fn_args[0].clone(),)),
2 => engine.call_fn::<Dynamic>(&mut scope, &ast, &self.fn_name, (fn_args[0].clone(), fn_args[1].clone())),
3 => engine.call_fn::<Dynamic>(&mut scope, &ast, &self.fn_name, (fn_args[0].clone(), fn_args[1].clone(), fn_args[2].clone())),
_ => return Err(tera::Error::msg(format!("Unsupported number of arguments: {}", fn_args.len())))
}.map_err(|e| tera::Error::msg(format!("Rhai error: {}", e)))?;
println!("DEBUG: Successfully called function '{}' with result: {:?}", self.fn_name, result);
// Convert Rhai result to Tera value
let tera_value = convert_rhai_to_tera(&result);
Ok(tera_value)
}
}
/// Convert a Tera value to a Rhai Dynamic value
pub 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
pub 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.type_name() == "i32" || value.type_name() == "i64" || value.type_name().contains("INT") {
// Handle i32, i64, and any other integer type
// Try to parse the string representation as an integer
let i = match value.to_string().parse::<i64>() {
Ok(num) => num,
Err(_) => {
// If parsing fails, try as_int as a fallback
value.as_int().unwrap_or(0)
}
};
// Create a Tera number value
Value::Number(serde_json::Number::from(i))
} else if value.type_name() == "f32" || value.type_name() == "f64" || value.type_name().contains("FLOAT") {
// Handle f32, f64, and any other float type
// Try to parse the string representation as a float
let f = match value.to_string().parse::<f64>() {
Ok(num) => num,
Err(_) => {
// If parsing fails, try as_float as a fallback
value.as_float().unwrap_or(0.0)
}
};
// Convert float to serde_json::Number safely
match serde_json::Number::from_f64(f) {
Some(num) => Value::Number(num),
None => Value::String(f.to_string()) // Fallback for NaN, Infinity, etc.
}
} 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().try_cast::<rhai::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())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rhai::Engine;
#[test]
fn test_convert_tera_to_rhai() {
// Test null
let null = Value::Null;
let rhai_null = convert_tera_to_rhai(&null);
assert!(rhai_null.is_unit());
// Test boolean
let bool_val = Value::Bool(true);
let rhai_bool = convert_tera_to_rhai(&bool_val);
assert!(rhai_bool.is_bool());
assert_eq!(rhai_bool.as_bool().unwrap(), true);
// Test integer
let int_val = Value::Number(serde_json::Number::from(42));
let rhai_int = convert_tera_to_rhai(&int_val);
assert!(rhai_int.is_int());
assert_eq!(rhai_int.as_int().unwrap(), 42);
// Test float
let float_val = serde_json::to_value(3.14).unwrap();
let rhai_float = convert_tera_to_rhai(&float_val);
assert!(rhai_float.is_float());
assert!((rhai_float.as_float().unwrap() - 3.14).abs() < f64::EPSILON);
// Test string
let string_val = Value::String("hello".to_string());
let rhai_string = convert_tera_to_rhai(&string_val);
assert!(rhai_string.is_string());
assert_eq!(rhai_string.to_string(), "hello");
// Test array
let array_val = Value::Array(vec![
Value::Number(serde_json::Number::from(1)),
Value::Number(serde_json::Number::from(2)),
Value::Number(serde_json::Number::from(3)),
]);
let rhai_array = convert_tera_to_rhai(&array_val);
assert!(rhai_array.is_array());
let array = rhai_array.clone().into_array().unwrap();
assert_eq!(array.len(), 3);
assert!(array[0].is_int());
assert!(array[1].is_int());
assert!(array[2].is_int());
assert_eq!(array[0].as_int().unwrap(), 1);
assert_eq!(array[1].as_int().unwrap(), 2);
assert_eq!(array[2].as_int().unwrap(), 3);
// Test object
let mut obj = serde_json::Map::new();
obj.insert("name".to_string(), Value::String("John".to_string()));
obj.insert("age".to_string(), Value::Number(serde_json::Number::from(30)));
let obj_val = Value::Object(obj);
let rhai_obj = convert_tera_to_rhai(&obj_val);
assert!(rhai_obj.is_map());
let map = rhai_obj.clone().try_cast::<rhai::Map>().unwrap();
assert_eq!(map.len(), 2);
assert_eq!(map.get("name").unwrap().to_string(), "John");
assert_eq!(map.get("age").unwrap().as_int().unwrap(), 30);
}
#[test]
fn test_convert_rhai_to_tera() {
// Test unit
let unit = Dynamic::UNIT;
let tera_unit = convert_rhai_to_tera(&unit);
assert!(tera_unit.is_null());
// Test boolean
let bool_val = Dynamic::from(true);
let tera_bool = convert_rhai_to_tera(&bool_val);
assert!(tera_bool.is_boolean());
assert_eq!(tera_bool.as_bool().unwrap(), true);
// Test integer
let int_val = Dynamic::from(42);
let tera_int = convert_rhai_to_tera(&int_val);
assert!(tera_int.is_number());
assert_eq!(tera_int.as_i64().unwrap(), 42);
// Test float
let float_val = Dynamic::from(3.14);
let tera_float = convert_rhai_to_tera(&float_val);
assert!(tera_float.is_number());
assert!((tera_float.as_f64().unwrap() - 3.14).abs() < f64::EPSILON);
// Test string
let string_val = Dynamic::from("hello");
let tera_string = convert_rhai_to_tera(&string_val);
assert!(tera_string.is_string());
assert_eq!(tera_string.as_str().unwrap(), "hello");
// Test array
let array_val = Dynamic::from(vec![
Dynamic::from(1),
Dynamic::from(2),
Dynamic::from(3),
]);
let tera_array = convert_rhai_to_tera(&array_val);
assert!(tera_array.is_array());
let array = tera_array.as_array().unwrap();
assert_eq!(array.len(), 3);
assert!(array[0].is_number());
assert!(array[1].is_number());
assert!(array[2].is_number());
assert_eq!(array[0].as_i64().unwrap(), 1);
assert_eq!(array[1].as_i64().unwrap(), 2);
assert_eq!(array[2].as_i64().unwrap(), 3);
// Test map
let mut map = rhai::Map::new();
map.insert("name".into(), Dynamic::from("John"));
map.insert("age".into(), Dynamic::from(30));
let map_val = Dynamic::from_map(map);
let tera_map = convert_rhai_to_tera(&map_val);
assert!(tera_map.is_object());
let obj = tera_map.as_object().unwrap();
assert_eq!(obj.len(), 2);
assert_eq!(obj.get("name").unwrap().as_str().unwrap(), "John");
assert_eq!(obj.get("age").unwrap().as_i64().unwrap(), 30);
}
#[test]
fn test_function_adapter() {
// Create a simple AST with a function
let mut engine = Engine::new();
// Configure engine to allow direct access to variables
engine.set_strict_variables(false);
engine.set_fail_on_invalid_map_property(false);
// Register the function directly with the engine to ensure it's available
engine.register_fn("add", |a: i64, b: i64| a + b);
// Create a script with an add function
let ast = engine.compile(r#"
fn add(a, b) {
a + b
}
"#).unwrap();
let hot_ast = Arc::new(RwLock::new(ast));
// Create a function adapter
let adapter = RhaiFunctionAdapter {
fn_name: "add".to_string(),
hot_ast: hot_ast.clone(),
};
// Call the function with arguments
let mut args = HashMap::new();
args.insert("a".to_string(), Value::Number(serde_json::Number::from(1)));
args.insert("b".to_string(), Value::Number(serde_json::Number::from(2)));
let result = adapter.call(&args).unwrap();
assert!(result.is_number());
assert_eq!(result.as_i64().unwrap(), 3);
}
#[test]
fn test_function_adapter_error_handling() {
// Create a simple AST with a function that throws an error
let mut engine = Engine::new();
// Configure engine to allow direct access to variables
engine.set_strict_variables(false);
// Create a script with a function that throws an error
let ast = engine.compile(r#"
fn throw_error() {
throw "This is an error";
}
"#).unwrap();
let hot_ast = Arc::new(RwLock::new(ast));
// Create a function adapter
let adapter = RhaiFunctionAdapter {
fn_name: "throw_error".to_string(),
hot_ast: hot_ast.clone(),
};
// Call the function
let args = HashMap::new();
let result = adapter.call(&args);
// Verify that the error is properly propagated
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("This is an error"));
}
#[test]
fn test_function_adapter_non_existent_function() {
// Create a simple AST
let mut engine = Engine::new();
// Configure engine to allow direct access to variables
engine.set_strict_variables(false);
let ast = engine.compile(r#"
fn add(a, b) {
a + b
}
"#).unwrap();
let hot_ast = Arc::new(RwLock::new(ast));
// Create a function adapter with a non-existent function
let adapter = RhaiFunctionAdapter {
fn_name: "non_existent_function".to_string(),
hot_ast: hot_ast.clone(),
};
// Call the function
let args = HashMap::new();
let result = adapter.call(&args);
// Verify that the error is properly propagated
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Function not found"));
}
#[test]
fn test_function_adapter_with_various_types() {
// Create a simple AST with a function that echoes its input
let mut engine = Engine::new();
// Configure engine to allow direct access to variables
engine.set_strict_variables(false);
engine.set_fail_on_invalid_map_property(false);
// Register the echo function directly with the engine
engine.register_fn("echo", |value: Dynamic| value);
// Create a script with an echo function
let ast = engine.compile(r#"
fn echo(value) {
value
}
"#).unwrap();
let hot_ast = Arc::new(RwLock::new(ast));
// Test with various types
let test_cases = vec![
// Null
("null", Value::Null, Value::Null),
// Boolean
("boolean", Value::Bool(true), Value::Bool(true)),
// Integer
("integer", Value::Number(serde_json::Number::from(42)), Value::Number(serde_json::Number::from(42))),
// String
("string", Value::String("hello".to_string()), Value::String("hello".to_string())),
// Array
("array", Value::Array(vec![
Value::Number(serde_json::Number::from(1)),
Value::Number(serde_json::Number::from(2)),
Value::Number(serde_json::Number::from(3)),
]), Value::Array(vec![
Value::Number(serde_json::Number::from(1)),
Value::Number(serde_json::Number::from(2)),
Value::Number(serde_json::Number::from(3)),
])),
];
for (type_name, input, expected) in test_cases {
// Create a function adapter
let adapter = RhaiFunctionAdapter {
fn_name: "echo".to_string(),
hot_ast: hot_ast.clone(),
};
// Call the function with the input
let mut args = HashMap::new();
args.insert("value".to_string(), input);
let result = adapter.call(&args).unwrap();
// Compare the result with the expected output
match (&result, &expected) {
(Value::Null, Value::Null) => {
// Both are null, so they're equal
},
(Value::Bool(r), Value::Bool(e)) => {
assert_eq!(r, e, "Boolean value mismatch for {}", type_name);
},
(Value::Number(r), Value::Number(e)) => {
if e.is_i64() {
assert_eq!(r.as_i64().unwrap(), e.as_i64().unwrap(), "Number value mismatch for {}", type_name);
} else if e.is_f64() {
assert!((r.as_f64().unwrap() - e.as_f64().unwrap()).abs() < f64::EPSILON, "Float value mismatch for {}", type_name);
}
},
(Value::String(r), Value::String(e)) => {
assert_eq!(r, e, "String value mismatch for {}", type_name);
},
(Value::Array(r), Value::Array(e)) => {
assert_eq!(r.len(), e.len(), "Array length mismatch for {}", type_name);
for (i, (r_item, e_item)) in r.iter().zip(e.iter()).enumerate() {
assert_eq!(r_item, e_item, "Array item {} mismatch for {}", i, type_name);
}
},
(Value::Object(r), Value::Object(e)) => {
assert_eq!(r.len(), e.len(), "Object size mismatch for {}", type_name);
for (k, v1) in e {
assert!(r.contains_key(k), "Missing key {} in object for {}", k, type_name);
assert_eq!(v1, r.get(k).unwrap(), "Value mismatch for key {} in {}", k, type_name);
}
},
_ => {
panic!("Type mismatch for {}: expected {:?}, got {:?}", type_name, expected, result);
}
}
}
}
#[test]
fn test_thread_safety() {
use std::thread;
// Create a simple engine with an increment function
let mut engine = Engine::new();
// Configure engine to allow direct access to variables
engine.set_strict_variables(false);
engine.set_fail_on_invalid_map_property(false);
// Register the increment function directly with the engine
engine.register_fn("increment", |value: i64| value + 1);
// Create a script with an increment function
let ast = engine.compile(r#"
fn increment(value) {
value + 1
}
"#).unwrap();
let hot_ast = Arc::new(RwLock::new(ast));
// Create a function adapter
let _adapter = RhaiFunctionAdapter {
fn_name: "increment".to_string(),
hot_ast: hot_ast.clone(),
};
// Create multiple threads to call the function concurrently
let mut handles = vec![];
for i in 0..10 {
// Create a new adapter for this thread with the same function name and AST
let adapter_clone = RhaiFunctionAdapter {
fn_name: "increment".to_string(),
hot_ast: Arc::clone(&hot_ast),
};
let handle = thread::spawn(move || {
// Create args with the test value
let mut args = HashMap::new();
args.insert("value".to_string(), Value::Number(serde_json::Number::from(i)));
// Call the function
let result = adapter_clone.call(&args).unwrap();
// Verify the result
assert!(result.is_number());
assert_eq!(result.as_i64().unwrap(), i + 1);
});
handles.push(handle);
}
// Wait for all threads to complete
for handle in handles {
handle.join().unwrap();
}
}
}

61
tera_factory/src/lib.rs Normal file
View File

@ -0,0 +1,61 @@
//! # Tera Factory
//!
//! A factory for creating Tera template engines with integrated Rhai scripting support.
//!
//! This crate provides a `TeraFactory` that can create Tera template engines with
//! hot reloadable Rhai scripting support, allowing Rhai functions to be called from
//! Tera templates and ensuring they are automatically updated when scripts are modified.
//!
//! ## Example
//!
//! ```rust,no_run
//! use std::path::PathBuf;
//! use std::sync::{Arc, RwLock};
//! use rhai_factory::RhaiFactory;
//! use tera_factory::TeraFactory;
//!
//! // 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)).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),
//! Some(Box::new(|| println!("Script reloaded!")))
//! ).unwrap();
//!
//! // Create a Tera engine with Rhai integration
//! let tera = tera_factory.create_tera_with_rhai(&[&templates_dir], hot_ast.clone()).unwrap();
//!
//! // Render a template
//! let mut context = tera::Context::new();
//! context.insert("a", &20);
//! context.insert("b", &22);
//! let rendered = tera.render("math.html", &context).unwrap();
//! println!("Rendered template: {}", rendered);
//!
//! // Disable hot reloading when done
//! rhai_factory.disable_hot_reload(handle);
//! ```
mod error;
mod factory;
pub mod function_adapter;
pub use error::TeraFactoryError;
pub use factory::TeraFactory;
pub use function_adapter::{RhaiFunctionAdapter, convert_tera_to_rhai, convert_rhai_to_tera};
// Re-export dependencies for convenience
pub use tera;

View File

@ -0,0 +1,338 @@
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use rhai::{Engine, AST};
use tera::{Value, Function};
use tera_factory::function_adapter::RhaiFunctionAdapter;
// Helper function to create a test AST with a simple function
fn create_test_ast(fn_name: &str, fn_body: &str) -> AST {
let engine = Engine::new();
// Create the full script with the function
let script = format!("fn {}() {{ {} }}", fn_name, fn_body);
println!("Creating test AST with script: {}", script);
engine.compile(script).expect("Failed to compile test script")
}
#[test]
fn test_tera_to_rhai_conversion() {
println!("Running test_tera_to_rhai_conversion");
// Create a test function that echoes its input
let ast = create_test_ast("echo", "params[0]");
let hot_ast = Arc::new(RwLock::new(ast));
// Create a function adapter
let adapter = RhaiFunctionAdapter {
fn_name: "echo".to_string(),
hot_ast: hot_ast.clone(),
};
// Test with different Tera value types
let test_cases = vec![
// String
("string_value", Value::String("hello".to_string()), "hello"),
// Integer
("int_value", Value::Number(42.into()), "42"),
// Float
("float_value", Value::Number(serde_json::Number::from_f64(3.14).unwrap()), "3.14"),
// Boolean
("bool_value", Value::Bool(true), "true"),
// Array
("array_value", Value::Array(vec![
Value::Number(1.into()),
Value::Number(2.into()),
Value::Number(3.into()),
]), "[1, 2, 3]"),
// Object
("object_value", {
let mut obj = serde_json::Map::new();
obj.insert("name".to_string(), Value::String("John".to_string()));
obj.insert("age".to_string(), Value::Number(30.into()));
Value::Object(obj)
}, "{\"age\": 30, \"name\": \"John\"}"),
];
for (name, tera_value, expected_output) in test_cases {
println!("Testing conversion of {}: {:?}", name, tera_value);
// Create args with the test value
let mut args = HashMap::new();
args.insert("value".to_string(), tera_value);
// Call the function adapter
let result = adapter.call(&args);
// Verify the result
assert!(result.is_ok(), "Failed to call function with {}", name);
let result_value = result.unwrap();
// For direct value comparison instead of string comparison
println!("Result for {}: {:?}", name, result_value);
// Compare based on the expected type
match (name, &result_value) {
("string_value", Value::String(s)) => {
assert_eq!(s, "hello", "Incorrect string result for {}", name);
},
("int_value", Value::Number(n)) => {
assert!(n.is_i64(), "Expected integer for {}", name);
assert_eq!(n.as_i64().unwrap(), 42, "Incorrect integer result for {}", name);
},
("float_value", Value::Number(n)) => {
assert!(n.is_f64(), "Expected float for {}", name);
assert!((n.as_f64().unwrap() - 3.14).abs() < 0.001, "Incorrect float result for {}", name);
},
("bool_value", Value::Bool(b)) => {
assert_eq!(*b, true, "Incorrect boolean result for {}", name);
},
("array_value", Value::Array(arr)) => {
assert_eq!(arr.len(), 3, "Incorrect array length for {}", name);
if let (Some(Value::Number(n1)), Some(Value::Number(n2)), Some(Value::Number(n3))) =
(arr.get(0), arr.get(1), arr.get(2)) {
assert_eq!(n1.as_i64().unwrap(), 1, "Incorrect first array element for {}", name);
assert_eq!(n2.as_i64().unwrap(), 2, "Incorrect second array element for {}", name);
assert_eq!(n3.as_i64().unwrap(), 3, "Incorrect third array element for {}", name);
} else {
panic!("Array elements have incorrect types for {}", name);
}
},
("object_value", Value::Object(obj)) => {
assert_eq!(obj.len(), 2, "Incorrect object size for {}", name);
assert!(obj.contains_key("name"), "Object missing 'name' key for {}", name);
assert!(obj.contains_key("age"), "Object missing 'age' key for {}", name);
if let Some(Value::String(name_val)) = obj.get("name") {
assert_eq!(name_val, "John", "Incorrect name value for {}", name);
} else {
panic!("Name has incorrect type for {}", name);
}
if let Some(Value::Number(age_val)) = obj.get("age") {
assert_eq!(age_val.as_i64().unwrap(), 30, "Incorrect age value for {}", name);
} else {
panic!("Age has incorrect type for {}", name);
}
},
_ => panic!("Unexpected result type for {}: {:?}", name, result_value),
}
}
println!("test_tera_to_rhai_conversion passed");
}
#[test]
fn test_rhai_to_tera_conversion() {
println!("Running test_rhai_to_tera_conversion");
// Create test functions that return different types
let test_cases = vec![
// Integer
("return_int", "42", Value::Number(42.into())),
// Float
("return_float", "3.14", Value::Number(serde_json::Number::from_f64(3.14).unwrap())),
// String
("return_string", "\"hello\"", Value::String("hello".to_string())),
// Boolean
("return_bool", "true", Value::Bool(true)),
// Array
("return_array", "[1, 2, 3]", Value::Array(vec![
Value::Number(1.into()),
Value::Number(2.into()),
Value::Number(3.into()),
])),
// Object/Map
("return_map", "#{\"name\": \"John\", \"age\": 30}", {
let mut obj = serde_json::Map::new();
obj.insert("name".to_string(), Value::String("John".to_string()));
obj.insert("age".to_string(), Value::Number(30.into()));
Value::Object(obj)
}),
];
for (fn_name, fn_body, expected_value) in test_cases {
println!("Testing conversion of Rhai {} to Tera value", fn_name);
// Create the AST with the test function
let ast = create_test_ast(fn_name, fn_body);
let hot_ast = Arc::new(RwLock::new(ast));
// Create a function adapter
let adapter = RhaiFunctionAdapter {
fn_name: fn_name.to_string(),
hot_ast: hot_ast.clone(),
};
// Call the function adapter with empty args
let args = HashMap::new();
let result = adapter.call(&args);
// Verify the result
assert!(result.is_ok(), "Failed to call function {}", fn_name);
let result_value = result.unwrap();
println!("Result for {}: {:?}", fn_name, result_value);
// Compare with expected value
match (&result_value, &expected_value) {
(Value::Number(n1), Value::Number(n2)) => {
// Special handling for numbers to handle float comparison
if n1.is_f64() || n2.is_f64() {
let f1 = n1.as_f64().unwrap();
let f2 = n2.as_f64().unwrap();
assert!((f1 - f2).abs() < 0.0001, "Incorrect float result for {}", fn_name);
} else {
assert_eq!(n1, n2, "Incorrect number result for {}", fn_name);
}
},
(Value::Array(a1), Value::Array(a2)) => {
assert_eq!(a1.len(), a2.len(), "Array length mismatch for {}", fn_name);
// Compare each element
for (i, (v1, v2)) in a1.iter().zip(a2.iter()).enumerate() {
assert_eq!(v1, v2, "Array element {} mismatch for {}", i, fn_name);
}
},
(Value::Object(o1), Value::Object(o2)) => {
assert_eq!(o1.len(), o2.len(), "Object key count mismatch for {}", fn_name);
// Compare each key-value pair
for (k, v1) in o1 {
assert!(o2.contains_key(k), "Missing key {} in object for {}", k, fn_name);
assert_eq!(v1, o2.get(k).unwrap(), "Value mismatch for key {} in {}", k, fn_name);
}
},
_ => {
assert_eq!(result_value, expected_value, "Incorrect result for {}", fn_name);
}
}
}
println!("test_rhai_to_tera_conversion passed");
}
#[test]
fn test_error_handling_in_function_adapter() {
println!("Running test_error_handling_in_function_adapter");
// Create a test function that throws an error
let ast = create_test_ast("throw_error", "throw \"This is a test error\";");
let hot_ast = Arc::new(RwLock::new(ast));
// Create a function adapter
let adapter = RhaiFunctionAdapter {
fn_name: "throw_error".to_string(),
hot_ast: hot_ast.clone(),
};
// Call the function adapter
let args = HashMap::new();
let result = adapter.call(&args);
// Verify that the call returns an error
assert!(result.is_err(), "Function should return an error");
let error = result.err().unwrap();
println!("Error (expected): {:?}", error);
// Create a test function that doesn't exist
let adapter_non_existent = RhaiFunctionAdapter {
fn_name: "non_existent_function".to_string(),
hot_ast: hot_ast.clone(),
};
// Call the function adapter
let result = adapter_non_existent.call(&args);
// Verify that the call returns an error
assert!(result.is_err(), "Call to non-existent function should return an error");
let error = result.err().unwrap();
println!("Error for non-existent function (expected): {:?}", error);
println!("test_error_handling_in_function_adapter passed");
}
#[test]
fn test_function_with_parameters() {
println!("Running test_function_with_parameters");
// Create a test function that takes parameters
let ast = create_test_ast("add", "params[0] + params[1]");
let hot_ast = Arc::new(RwLock::new(ast));
// Create a function adapter
let adapter = RhaiFunctionAdapter {
fn_name: "add".to_string(),
hot_ast: hot_ast.clone(),
};
// Create args with the test values
let mut args = HashMap::new();
args.insert("a".to_string(), Value::Number(5.into()));
args.insert("b".to_string(), Value::Number(7.into()));
// Call the function adapter
let result = adapter.call(&args);
// Verify the result
assert!(result.is_ok(), "Failed to call function with parameters");
let result_value = result.unwrap();
println!("Result for add(5, 7): {:?}", result_value);
assert_eq!(result_value, Value::Number(12.into()), "Incorrect result for add function");
println!("test_function_with_parameters passed");
}
#[test]
fn test_concurrent_access() {
use std::thread;
println!("Running test_concurrent_access");
// Create a test function
let ast = create_test_ast("increment", "params[0] + 1");
let hot_ast = Arc::new(RwLock::new(ast));
// Create a function adapter
let _adapter = RhaiFunctionAdapter {
fn_name: "increment".to_string(),
hot_ast: hot_ast.clone(),
};
// Create multiple threads to call the function concurrently
let mut handles = vec![];
for i in 0..10 {
// Create a new adapter for this thread with the same function name and AST
let adapter_clone = RhaiFunctionAdapter {
fn_name: "increment".to_string(),
hot_ast: Arc::clone(&hot_ast),
};
let handle = thread::spawn(move || {
// Create args with the test value
let mut args = HashMap::new();
args.insert("value".to_string(), Value::Number(i.into()));
// Call the function adapter
let result = adapter_clone.call(&args);
// Verify the result
assert!(result.is_ok(), "Failed to call function in thread {}", i);
let result_value = result.unwrap();
println!("Thread {} result: {:?}", i, result_value);
assert_eq!(result_value, Value::Number((i + 1).into()),
"Incorrect result for increment function in thread {}", i);
});
handles.push(handle);
}
// Wait for all threads to complete
for (i, handle) in handles.into_iter().enumerate() {
handle.join().unwrap();
println!("Thread {} completed successfully", i);
}
println!("test_concurrent_access passed");
}

View File

@ -0,0 +1,383 @@
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use rhai::Engine;
use tempfile::TempDir;
use tera::Context;
use rhai_factory::RhaiFactory;
use tera_factory::TeraFactory;
// Helper function to create a temporary directory with test files
fn setup_test_environment() -> (TempDir, PathBuf, PathBuf) {
// Create a temporary directory for our test files
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let temp_path = temp_dir.path();
// Create a templates directory
let templates_dir = temp_path.join("templates");
std::fs::create_dir(&templates_dir).expect("Failed to create templates directory");
// Create a scripts directory
let scripts_dir = temp_path.join("scripts");
std::fs::create_dir(&scripts_dir).expect("Failed to create scripts directory");
// Create a simple Rhai script with test functions
let script_path = scripts_dir.join("test_functions.rhai");
let mut script_file = File::create(&script_path).expect("Failed to create script file");
script_file.write_all(b"
// Test function to add two numbers
fn add(a, b) {
a + b
}
// Test function to format a string
fn format_greeting(name) {
`Hello, ${name}!`
}
").expect("Failed to write to script file");
// Create a simple template file with .html.tera extension
let template_path = templates_dir.join("test.html.tera");
let mut template_file = File::create(&template_path).expect("Failed to create template file");
template_file.write_all(b"
<html>
<body>
<h1>{{ format_greeting(name=name) }}</h1>
<p>The sum of {{ a }} and {{ b }} is: {{ add(a=a, b=b) }}</p>
</body>
</html>
").expect("Failed to write to template file");
println!("Test environment set up with:");
println!(" Templates directory: {:?}", templates_dir);
println!(" Scripts directory: {:?}", scripts_dir);
(temp_dir, templates_dir, scripts_dir)
}
#[test]
fn test_create_tera_engine() {
println!("Running test_create_tera_engine");
// Set up the test environment
let (temp_dir, templates_dir, _) = setup_test_environment();
// Create a TeraFactory
let factory = TeraFactory::new();
// Create a Tera engine
let tera = factory.create_tera_engine(&[&templates_dir]).expect("Failed to create Tera engine");
// Verify that the template was loaded
assert!(tera.get_template_names().any(|name| name == "test.html.tera"),
"Template 'test.html.tera' was not loaded");
println!("test_create_tera_engine passed");
// Keep temp_dir in scope until the end of the test
drop(temp_dir);
}
#[test]
fn test_create_tera_with_rhai() {
println!("Running test_create_tera_with_rhai");
// Set up the test environment
let (temp_dir, templates_dir, scripts_dir) = setup_test_environment();
// Create factories
let rhai_factory = RhaiFactory::with_caching();
let tera_factory = TeraFactory::new();
// Compile the Rhai script
let script_path = scripts_dir.join("test_functions.rhai");
println!("Compiling script: {:?}", script_path);
let ast = rhai_factory.compile_modules(&[&script_path], None)
.expect("Failed to compile Rhai script");
// Verify that the functions were compiled
let _engine = Engine::new();
let fn_names: Vec<String> = ast.iter_functions().map(|f| f.name.to_string()).collect();
println!("Compiled functions: {:?}", fn_names);
assert!(fn_names.contains(&"add".to_string()), "Function 'add' was not compiled");
assert!(fn_names.contains(&"format_greeting".to_string()), "Function 'format_greeting' was not compiled");
// Create a hot reloadable AST
let hot_ast = Arc::new(RwLock::new(ast));
// Create a Tera engine with Rhai integration
println!("Creating Tera engine with Rhai integration");
let tera = tera_factory.create_tera_with_rhai(&[&templates_dir], hot_ast.clone())
.expect("Failed to create Tera engine with Rhai");
// Render the template
let mut context = Context::new();
context.insert("name", "World");
context.insert("a", &10);
context.insert("b", &5);
println!("Rendering template with context: {:?}", context);
let rendered = tera.render("test.html.tera", &context).expect("Failed to render template");
// Verify the rendered output
println!("Rendered output: {}", rendered);
assert!(rendered.contains("Hello, World!"), "Rendered output does not contain greeting");
assert!(rendered.contains("The sum of 10 and 5 is: 15"), "Rendered output does not contain correct sum");
println!("test_create_tera_with_rhai passed");
// Keep temp_dir in scope until the end of the test
drop(temp_dir);
}
#[test]
fn test_hot_reload() {
println!("Running test_hot_reload");
// Set up the test environment
let (temp_dir, templates_dir, scripts_dir) = setup_test_environment();
// Create factories
let rhai_factory = RhaiFactory::with_hot_reload().expect("Failed to create RhaiFactory with hot reload");
let tera_factory = TeraFactory::new();
// Compile the Rhai script
let script_path = scripts_dir.join("test_functions.rhai");
println!("Compiling script: {:?}", script_path);
let ast = rhai_factory.compile_modules(&[&script_path], None)
.expect("Failed to compile Rhai script");
// Create a hot reloadable AST
let hot_ast = Arc::new(RwLock::new(ast));
// Enable hot reloading
println!("Enabling hot reloading");
let _handle = rhai_factory.enable_hot_reload(
hot_ast.clone(),
&[&script_path],
None,
Some(Box::new(|| println!("Script reloaded!")))
).expect("Failed to enable hot reloading");
// Create a Tera engine with Rhai integration
println!("Creating Tera engine with Rhai integration");
let tera = tera_factory.create_tera_with_rhai(&[&templates_dir], hot_ast.clone())
.expect("Failed to create Tera engine with Rhai");
// Render the template before modification
let mut context = Context::new();
context.insert("name", "World");
context.insert("a", &10);
context.insert("b", &5);
println!("Rendering template before script modification");
let rendered_before = tera.render("test.html.tera", &context).expect("Failed to render template");
// Verify the rendered output before modification
println!("Rendered output before: {}", rendered_before);
assert!(rendered_before.contains("Hello, World!"), "Rendered output does not contain greeting");
assert!(rendered_before.contains("The sum of 10 and 5 is: 15"), "Rendered output does not contain correct sum");
// Modify the script file
println!("Modifying script file");
let mut script_file = File::create(&script_path).expect("Failed to create script file");
script_file.write_all(b"
// Modified test function to add two numbers
fn add(a, b) {
a + b + 1 // Changed to add 1 to the result
}
// Modified test function to format a string
fn format_greeting(name) {
`Greetings, ${name}!` // Changed greeting format
}
").expect("Failed to write to script file");
// Check for changes
println!("Checking for script changes");
let changes_detected = rhai_factory.check_for_changes().expect("Failed to check for changes");
assert!(changes_detected, "Changes were not detected");
// Render the template after modification
println!("Rendering template after script modification");
let rendered_after = tera.render("test.html.tera", &context).expect("Failed to render template");
// Verify the rendered output after modification
println!("Rendered output after: {}", rendered_after);
assert!(rendered_after.contains("Greetings, World!"), "Rendered output does not contain modified greeting");
assert!(rendered_after.contains("The sum of 10 and 5 is: 16"), "Rendered output does not contain modified sum");
println!("test_hot_reload passed");
// Keep temp_dir in scope until the end of the test
drop(temp_dir);
}
#[test]
fn test_error_handling() {
println!("Running test_error_handling");
// Set up the test environment
let (temp_dir, templates_dir, scripts_dir) = setup_test_environment();
// Create factories
let rhai_factory = RhaiFactory::with_caching();
let tera_factory = TeraFactory::new();
// Create a script with syntax errors
let invalid_script_path = scripts_dir.join("invalid.rhai");
let mut invalid_script_file = File::create(&invalid_script_path).expect("Failed to create invalid script file");
invalid_script_file.write_all(b"
// This script has a syntax error
fn broken_function(a, b { // Missing closing parenthesis
a + b
}
").expect("Failed to write to invalid script file");
// Try to compile the invalid script
println!("Attempting to compile invalid script");
let compile_result = rhai_factory.compile_modules(&[&invalid_script_path], None);
// Verify that compilation fails with an error
assert!(compile_result.is_err(), "Compilation of invalid script should fail");
println!("Compilation error (expected): {:?}", compile_result.err());
// Create a template with invalid syntax
let invalid_template_path = templates_dir.join("invalid.html.tera");
let mut invalid_template_file = File::create(&invalid_template_path).expect("Failed to create invalid template file");
invalid_template_file.write_all(b"
<html>
<body>
<h1>{{ unclosed tag </h1>
</body>
</html>
").expect("Failed to write to invalid template file");
// Try to create a Tera engine with the invalid template
println!("Attempting to create Tera engine with invalid template");
let tera_result = tera_factory.create_tera_engine(&[&templates_dir]);
// Verify that Tera creation still succeeds (Tera loads valid templates and logs errors for invalid ones)
assert!(tera_result.is_ok(), "Tera engine creation should succeed even with invalid templates");
println!("test_error_handling passed");
// Keep temp_dir in scope until the end of the test
drop(temp_dir);
}
#[test]
fn test_function_adapter() {
println!("Running test_function_adapter");
// Set up the test environment
let (temp_dir, templates_dir, scripts_dir) = setup_test_environment();
// Create factories
let rhai_factory = RhaiFactory::with_caching();
let tera_factory = TeraFactory::new();
// Create a script with functions that use different parameter types
let types_script_path = scripts_dir.join("types.rhai");
let mut types_script_file = File::create(&types_script_path).expect("Failed to create types script file");
types_script_file.write_all(b"
// Function that returns an integer
fn return_int() {
42
}
// Function that returns a float
fn return_float() {
3.14159
}
// Function that returns a string
fn return_string() {
\"Hello, World!\"
}
// Function that returns a boolean
fn return_bool() {
true
}
// Function that returns an array
fn return_array() {
[1, 2, 3, 4, 5]
}
// Function that returns a map
fn return_map() {
#{
\"name\": \"John\",
\"age\": 30,
\"city\": \"New York\"
}
}
").expect("Failed to write to types script file");
// Create a template that uses these functions with .html.tera extension
let types_template_path = templates_dir.join("types.html.tera");
let mut types_template_file = File::create(&types_template_path).expect("Failed to create types template file");
println!("Created template file at: {:?}", types_template_path);
types_template_file.write_all(b"
<html>
<body>
<p>Integer: {{ return_int() }}</p>
<p>Float: {{ return_float() }}</p>
<p>String: {{ return_string() }}</p>
<p>Boolean: {{ return_bool() }}</p>
<p>Array: {{ return_array() }}</p>
<p>Map name: {{ return_map().name }}</p>
<p>Map age: {{ return_map().age }}</p>
</body>
</html>
").expect("Failed to write to types template file");
// Compile the script
println!("Compiling types script");
let ast = rhai_factory.compile_modules(&[&types_script_path], None)
.expect("Failed to compile types script");
// Create a hot reloadable AST
let hot_ast = Arc::new(RwLock::new(ast));
// Create a Tera engine with Rhai integration
println!("Creating Tera engine with Rhai integration for types test");
println!("Template directory exists: {:?}", std::fs::metadata(&templates_dir).is_ok());
println!("Template directory contents:");
if let Ok(entries) = std::fs::read_dir(&templates_dir) {
for entry in entries.filter_map(Result::ok) {
println!(" - {:?}", entry.path());
}
}
let tera = tera_factory.create_tera_with_rhai(&[&templates_dir], hot_ast.clone())
.expect("Failed to create Tera engine with Rhai");
// Print available template names for debugging
println!("Available templates: {:?}", tera.get_template_names().collect::<Vec<_>>());
// Render the template
println!("Rendering types template");
let rendered = tera.render("types.html.tera", &Context::new()).expect("Failed to render types template");
// Verify the rendered output for each type
println!("Rendered output: {}", rendered);
assert!(rendered.contains("Integer: 42"), "Integer not correctly rendered");
assert!(rendered.contains("Float: 3.14159"), "Float not correctly rendered");
assert!(rendered.contains("String: Hello, World!"), "String not correctly rendered");
assert!(rendered.contains("Boolean: true"), "Boolean not correctly rendered");
assert!(rendered.contains("Array: [1, 2, 3, 4, 5]"), "Array not correctly rendered");
assert!(rendered.contains("Map name: John"), "Map name not correctly rendered");
assert!(rendered.contains("Map age: 30"), "Map age not correctly rendered");
println!("test_function_adapter passed");
// Keep temp_dir in scope until the end of the test
drop(temp_dir);
}