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:
parent
c23de6871b
commit
22032f329a
1522
tera_factory/Cargo.lock
generated
Normal file
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
16
tera_factory/Cargo.toml
Normal 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
105
tera_factory/README.md
Normal 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
1608
tera_factory/examples/basic/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
tera_factory/examples/basic/Cargo.toml
Normal file
11
tera_factory/examples/basic/Cargo.toml
Normal 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"
|
21
tera_factory/examples/basic/scripts/math.rhai
Normal file
21
tera_factory/examples/basic/scripts/math.rhai
Normal 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 + "!"
|
||||||
|
}
|
98
tera_factory/examples/basic/src/main.rs
Normal file
98
tera_factory/examples/basic/src/main.rs
Normal 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(())
|
||||||
|
}
|
43
tera_factory/examples/basic/templates/index.html
Normal file
43
tera_factory/examples/basic/templates/index.html
Normal 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>
|
96
tera_factory/examples/hot_reload/README.md
Normal file
96
tera_factory/examples/hot_reload/README.md
Normal 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.
|
31
tera_factory/examples/hot_reload/initial_main.rhai
Normal file
31
tera_factory/examples/hot_reload/initial_main.rhai
Normal 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);
|
||||||
|
}
|
22
tera_factory/examples/hot_reload/initial_utils.rhai
Normal file
22
tera_factory/examples/hot_reload/initial_utils.rhai
Normal 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;
|
||||||
|
}
|
194
tera_factory/examples/hot_reload/main.rs
Normal file
194
tera_factory/examples/hot_reload/main.rs
Normal 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(())
|
||||||
|
}
|
31
tera_factory/examples/hot_reload/scripts/initial_main.rhai
Normal file
31
tera_factory/examples/hot_reload/scripts/initial_main.rhai
Normal 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);
|
||||||
|
}
|
22
tera_factory/examples/hot_reload/scripts/initial_utils.rhai
Normal file
22
tera_factory/examples/hot_reload/scripts/initial_utils.rhai
Normal 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;
|
||||||
|
}
|
32
tera_factory/examples/hot_reload/scripts/main.rhai
Normal file
32
tera_factory/examples/hot_reload/scripts/main.rhai
Normal 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);
|
||||||
|
}
|
32
tera_factory/examples/hot_reload/scripts/modified_main.rhai
Normal file
32
tera_factory/examples/hot_reload/scripts/modified_main.rhai
Normal 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);
|
||||||
|
}
|
27
tera_factory/examples/hot_reload/scripts/modified_utils.rhai
Normal file
27
tera_factory/examples/hot_reload/scripts/modified_utils.rhai
Normal 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;
|
||||||
|
}
|
27
tera_factory/examples/hot_reload/scripts/utils.rhai
Normal file
27
tera_factory/examples/hot_reload/scripts/utils.rhai
Normal 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;
|
||||||
|
}
|
57
tera_factory/examples/hot_reload/templates/index.html.tera
Normal file
57
tera_factory/examples/hot_reload/templates/index.html.tera
Normal 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
37
tera_factory/src/error.rs
Normal 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
242
tera_factory/src/factory.rs
Normal 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>");
|
||||||
|
}
|
||||||
|
}
|
579
tera_factory/src/function_adapter.rs
Normal file
579
tera_factory/src/function_adapter.rs
Normal 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
61
tera_factory/src/lib.rs
Normal 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;
|
338
tera_factory/tests/function_adapter_tests.rs
Normal file
338
tera_factory/tests/function_adapter_tests.rs
Normal 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");
|
||||||
|
}
|
383
tera_factory/tests/tera_factory_tests.rs
Normal file
383
tera_factory/tests/tera_factory_tests.rs
Normal 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);
|
||||||
|
}
|
Reference in New Issue
Block a user