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

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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