refactor: Overhaul Rhai scripting with multi-file hot reloading

This commit represents a major refactoring of our Rhai scripting system,
transforming it from a factory-based approach to a more robust system-based
architecture with improved hot reloading capabilities.

Key Changes:
- Renamed package from rhai_factory to rhai_system to better reflect its purpose
- Renamed system_factory.rs to factory.rs for consistency and clarity
- Implemented support for multiple script files in hot reloading
- Added cross-script function calls, allowing functions in one script to call functions in another
- Improved file watching to monitor all script files for changes
- Enhanced error handling for script compilation failures
- Simplified the API with a cleaner create_hot_reloadable_system function
- Removed unused modules (error.rs, factory.rs, hot_reload_old.rs, module_cache.rs, relative_resolver.rs)
- Updated all tests to work with the new architecture

The new architecture:
- Uses a System struct that holds references to script paths and provides a clean API
- Compiles and merges multiple Rhai script files into a single AST
- Automatically detects changes to any script file and recompiles them
- Maintains thread safety with proper synchronization primitives
- Provides better error messages when scripts fail to compile

This refactoring aligns with our BasePathModuleResolver approach for module imports,
making the resolution process more predictable and consistent. The hot reload example
has been updated to demonstrate the new capabilities, showing how to:
1. Load and execute multiple script files
2. Watch for changes to these files
3. Automatically reload scripts when they change
4. Call functions across different script files

All tests are passing, and the example demonstrates the improved functionality.
This commit is contained in:
Timur Gordon 2025-05-02 21:04:33 +02:00
parent 939b6b4e57
commit 372b7a2772
54 changed files with 5692 additions and 0 deletions

1023
rhai_system/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

21
rhai_system/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "rhai_system"
version = "0.1.0"
edition = "2021"
description = "A thread-safe system for creating and managing Rhai script engines with hot reloading support"
[dependencies]
rhai = { version = "1.15.0", features = ["sync"] }
thiserror = "1.0"
log = "0.4"
notify = "5.1.0"
uuid = { version = "1.3.0", features = ["v4", "serde"] }
[dev-dependencies]
tokio = { version = "1.28", features = ["full"] }
tempfile = "3.5"
rand = "0.8"
[[example]]
name = "hot_reload"
path = "examples/hot_reload/main.rs"

152
rhai_system/README.md Normal file
View File

@ -0,0 +1,152 @@
# Rhai System
A thread-safe system for creating and managing Rhai script engines with hot reloading support for multiple script files.
## Overview
Rhai System is a Rust module that simplifies working with the [Rhai scripting language](https://rhai.rs/) by providing a system for creating thread-safe Rhai engines with pre-compiled scripts. It supports hot reloading of multiple script files, ensuring your application always uses the latest version of your scripts without requiring a restart.
## Features
- **Thread Safety**: Uses Rhai's `sync` feature to ensure engines are `Send + Sync`
- **Multiple Script Support**: Compiles and merges multiple Rhai script files into a single AST
- **Hot Reload**: Automatically detects changes to script files and recompiles them without requiring application restart
- **Cross-Script Function Calls**: Functions defined in one script can call functions defined in another script
- **Detailed Error Handling**: Provides clear error messages when scripts fail to compile
- **Performance Optimized**: Efficiently recompiles only the scripts that have changed
## Usage
### Basic Usage
```rust
use rhai_system::create_hot_reloadable_system;
use std::path::PathBuf;
// Create a hot reloadable system with multiple script files
let script_paths = vec![
PathBuf::from("scripts/main.rhai"),
PathBuf::from("scripts/utils.rhai"),
];
let system = create_hot_reloadable_system(&script_paths, None).unwrap();
// Call a function from the script
let result: i64 = system.call_fn("add", (40, 2)).unwrap();
assert_eq!(result, 42);
// The system will automatically reload scripts when they change
```
### Watching for Changes
The system automatically sets up file watchers for all script files:
```rust
use rhai_system::create_hot_reloadable_system;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
// Create a hot reloadable system
let script_paths = vec![PathBuf::from("scripts/main.rhai")];
let system = create_hot_reloadable_system(&script_paths, None).unwrap();
// Start watching for changes to the script files
system.watch();
// The system will now automatically reload scripts when they change
// Your application can continue running and using the latest version of the scripts
```
### Thread-Safe Usage
The system is designed to be thread-safe, allowing you to use it from multiple threads:
```rust
use rhai_system::create_hot_reloadable_system;
use std::path::PathBuf;
use std::thread;
use std::sync::Arc;
// Create a hot reloadable system
let script_paths = vec![PathBuf::from("scripts/main.rhai")];
let system = Arc::new(create_hot_reloadable_system(&script_paths, None).unwrap());
// Clone the system for use in another thread
let system_clone = Arc::clone(&system);
// Start watching for changes in the main thread
system.watch();
// Use the system in another thread
let handle = thread::spawn(move || {
// Create a thread-local clone of the system
let thread_system = system_clone.clone_for_thread();
// Call functions from the script
let result: i64 = thread_system.call_fn("add", (40, 2)).unwrap();
assert_eq!(result, 42);
});
handle.join().unwrap();
```
## Module Resolution
Rhai System supports the BasePathModuleResolver approach for resolving module imports:
- Uses a single base path for resolving all module imports
- Makes the resolution process more predictable and consistent
- Simplifies the Rhai module import system by removing the complexity of relative path resolution
See the `examples/base_path_imports` directory for a comprehensive example of this approach.
## Error Handling
The system provides detailed error information when scripts fail to compile:
```rust
use rhai_system::create_hot_reloadable_system;
use std::path::PathBuf;
let script_paths = vec![PathBuf::from("non_existent.rhai")];
let result = create_hot_reloadable_system(&script_paths, None);
match result {
Err(err) => {
// Error will contain:
// - Which script failed to compile
// - The reason for the failure
println!("Error: {}", err);
}
Ok(_) => panic!("Expected an error"),
}
```
## Testing
The project follows Rust's standard testing approach:
```bash
# Run all tests
cargo test
# Run a specific test
cargo test test_hot_reload_callback
```
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
rhai_system = { path = "path/to/rhai_system" }
```
## Examples
See the `examples` directory for complete examples:
- `hot_reload`: Demonstrates hot reloading of multiple script files
- `base_path_imports`: Demonstrates the BasePathModuleResolver approach for module imports

969
rhai_system/architecture.md Normal file
View File

@ -0,0 +1,969 @@
# Rhai Engine Factory Implementation Plan
Based on our exploration of the Rhai documentation and your requirements, I'll now outline a detailed plan for implementing the Rhai engine factory module.
## Overview
We'll create a Rust module called `rhai_factory` that provides a factory for creating thread-safe Rhai engines with pre-compiled scripts. The factory will:
1. Use the `sync` feature to ensure the engine is `Send + Sync`
2. Compile Rhai modules into a self-contained AST for better performance
3. Provide detailed error handling that shows which module failed to import and why
## Architecture
```mermaid
graph TD
A[RhaiFactory] --> B[create_engine]
A --> C[compile_modules]
A --> D[create_engine_with_modules]
B --> E[Engine with sync feature]
C --> F[Self-contained AST]
D --> G[Engine with compiled modules]
H[FileModuleResolver] --> C
I[Error Handling] --> C
J[Module Cache] --> C
```
## Component Details
### 1. RhaiFactory Module Structure
```
rhai_factory/
├── Cargo.toml # Dependencies including rhai with sync feature
├── src/
│ ├── lib.rs # Main module exports and unit tests
│ ├── factory.rs # Factory implementation and unit tests
│ ├── error.rs # Custom error types and unit tests
│ └── module_cache.rs # Optional module caching and unit tests
└── tests/
├── common/ # Common test utilities
│ └── mod.rs # Test fixtures and helpers
├── integration_tests.rs # Integration tests
└── rhai_scripts/ # Test Rhai scripts
├── main.rhai
├── module1.rhai
└── module2.rhai
```
### 2. Factory Implementation
The core factory will provide these main functions:
1. **create_engine()** - Creates a basic thread-safe Rhai engine with default configuration
2. **compile_modules(modules_paths, base_path)** - Compiles a list of Rhai modules into a self-contained AST
3. **create_engine_with_modules(modules_paths, base_path)** - Creates an engine with pre-compiled modules
### 3. Error Handling
We'll create a custom error type `RhaiFactoryError` that provides detailed information about:
- Which module failed to import
- The reason for the failure
- The path that was attempted
- Any nested module import failures
### 4. Module Caching (Optional Enhancement)
To improve performance when repeatedly using the same modules:
- Implement a module cache that stores compiled ASTs
- Use a hash of the module content as the cache key
- Provide options to invalidate the cache when modules change
### 5. Thread Safety
We'll ensure thread safety by:
- Using the `sync` feature of Rhai
- Ensuring all our factory methods return thread-safe types
- Using appropriate synchronization primitives for any shared state
## Implementation Steps
1. **Setup Project Structure**
- Create the directory structure
- Set up Cargo.toml with appropriate dependencies
- Create initial module files
2. **Implement Basic Factory**
- Create the factory struct with configuration options
- Implement `create_engine()` method
- Add engine configuration options
3. **Implement Module Compilation**
- Create the `compile_modules()` method
- Implement module resolution logic
- Handle recursive module imports
4. **Implement Error Handling**
- Create custom error types
- Implement detailed error reporting
- Add context to error messages
5. **Implement Combined Factory Method**
- Create `create_engine_with_modules()` method
- Ensure proper error propagation
- Add configuration options
6. **Write Tests**
- Create test fixtures and helpers
- Implement unit tests within each module
- Create integration tests
- Test thread safety
7. **Documentation**
- Add comprehensive documentation
- Include examples
- Document thread safety guarantees
## Code Outline
Here's a sketch of the main components:
### lib.rs
```rust
mod factory;
mod error;
mod module_cache;
pub use factory::RhaiFactory;
pub use error::RhaiFactoryError;
// Re-export commonly used Rhai types
pub use rhai::{Engine, AST, Scope, Module};
#[cfg(test)]
mod tests {
// Unit tests for the library as a whole
}
```
### factory.rs
```rust
use rhai::{Engine, AST, Scope, Module};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::error::RhaiFactoryError;
pub struct RhaiFactory {
// Configuration options
}
impl RhaiFactory {
pub fn new() -> Self {
// Initialize with default options
}
pub fn create_engine(&self) -> Engine {
// Create a thread-safe engine
}
pub fn compile_modules<P: AsRef<Path>>(&self, module_paths: &[P], base_path: Option<P>)
-> Result<AST, RhaiFactoryError> {
// Compile modules into a self-contained AST
}
pub fn create_engine_with_modules<P: AsRef<Path>>(&self, module_paths: &[P], base_path: Option<P>)
-> Result<(Engine, AST), RhaiFactoryError> {
// Create engine and compile modules
}
}
#[cfg(test)]
mod tests {
// Unit tests for the factory implementation
}
```
### error.rs
```rust
use std::error::Error;
use std::fmt;
use std::path::PathBuf;
#[derive(Debug)]
pub struct RhaiFactoryError {
module_path: Option<PathBuf>,
message: String,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl RhaiFactoryError {
pub fn new(message: impl Into<String>) -> Self {
// Create a new error
}
pub fn with_module(mut self, module_path: impl Into<PathBuf>) -> Self {
// Add module path context
}
pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
// Add source error
}
}
impl fmt::Display for RhaiFactoryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Format error message with context
}
}
impl Error for RhaiFactoryError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
// Return source error if any
}
}
```
## Testing Strategy
### 1. Unit Tests
We'll follow Rust's standard approach of including unit tests in each module:
```rust
#[cfg(test)]
mod tests {
use super::*;
// Test fixtures for common setup
struct TestFixture {
// Common test setup
}
impl TestFixture {
fn new() -> Self {
// Initialize test fixture
}
}
#[test]
fn test_specific_functionality() {
// Test a specific function or behavior
}
}
```
Key unit test areas:
- Factory creation and configuration
- Engine creation
- Module compilation
- Error handling
- Module caching
- Thread safety
### 2. Integration Tests
Integration tests will be placed in the `tests/` directory:
```
tests/
├── common/ # Common test utilities
│ └── mod.rs # Test fixtures and helpers
├── integration_tests.rs # Integration tests
└── rhai_scripts/ # Test Rhai scripts
```
The integration tests will focus on:
- End-to-end functionality
- Module imports and resolution
- Thread safety in a real-world context
- Error handling with real scripts
### 3. Test Fixtures and Helpers
We'll create test fixtures to simplify test setup and reduce code duplication:
```rust
// In tests/common/mod.rs
pub struct TestFixture {
pub factory: RhaiFactory,
pub scripts_dir: PathBuf,
}
impl TestFixture {
pub fn new() -> Self {
// Initialize test fixture
}
pub fn script_path(&self, name: &str) -> PathBuf {
// Get path to a test script
}
}
```
### 4. Example Usage
We'll provide example usage in the README.md and documentation:
```rust
// Create a factory
let factory = RhaiFactory::new();
// Create a thread-safe engine
let engine = factory.create_engine();
// Compile modules
let ast = factory.compile_modules(&["main.rhai"], Some(Path::new("./scripts")))?;
// Use the engine and AST
let result: i64 = engine.eval_ast(&ast)?;
```
## Thread Safety Considerations
1. The Rhai engine with the `sync` feature is `Send + Sync`
2. All factory methods will be thread-safe
3. Any shared state will use appropriate synchronization primitives
4. The compiled AST will be shareable between threads
## Hot Reload Feature
### Overview
The hot reload feature will allow the Rhai Factory to automatically detect changes to script files and recompile them without requiring application restart. This is particularly useful for development environments and systems where scripts control behavior that needs to be modified dynamically.
As described in the Rhai documentation (_archive/rhai_engine/rhaibook/patterns/hot-reload.md), hot reloading allows scripts to be modified dynamically without re-initializing the host system. The Rhai `Engine` is re-entrant, meaning it's decoupled from scripts, and a new script only needs to be recompiled and the new `AST` replaces the old for new behaviors to be active.
### Architecture Extension
```mermaid
graph TD
A[RhaiFactory] --> B[HotReloadManager]
B --> C[FileWatcher]
B --> D[ScriptRegistry]
C --> E[File System Events]
D --> F[Script Metadata]
B --> G[Reload Callbacks]
G --> H[Script Consumers]
I[ModuleCache] --> B
```
### Component Details
#### 1. Hot Reload Manager
The Hot Reload Manager will be responsible for:
1. Tracking which scripts are being watched
2. Detecting changes to script files
3. Recompiling scripts when changes are detected
4. Notifying consumers when scripts have been reloaded
This follows the pattern described in the Rhai hot reload documentation, where the system watches for script file changes and recompiles the scripts when changes are detected.
#### 2. File Watcher
The File Watcher will:
1. Monitor the file system for changes to script files
2. Notify the Hot Reload Manager when changes are detected
3. Support watching individual files or directories
This component will implement the "watch for script file change" functionality mentioned in the hot reload documentation.
#### 3. Script Registry
The Script Registry will:
1. Maintain metadata about watched scripts
2. Track dependencies between scripts
3. Determine which scripts need to be recompiled when a file changes
This is important for handling recursive imports and ensuring that all dependent scripts are recompiled when a dependency changes.
#### 4. Reload Callbacks
The system will provide:
1. A callback mechanism for consumers to be notified when scripts are reloaded
2. Options for synchronous or asynchronous notification
This follows the pattern in the hot reload documentation where a callback is provided to handle the reloaded script.
### Implementation Details
#### 1. Enhanced RhaiFactory API
We'll extend the RhaiFactory with new methods:
```rust
impl RhaiFactory {
// Existing methods...
/// Enable hot reloading for a compiled AST
pub fn enable_hot_reload<P: AsRef<Path>>(&self,
ast: Arc<RwLock<AST>>,
module_paths: &[P],
base_path: Option<P>,
callback: Option<Box<dyn Fn() + Send + Sync>>
) -> Result<HotReloadHandle, RhaiFactoryError>;
/// Disable hot reloading for a previously enabled AST
pub fn disable_hot_reload(&self, handle: HotReloadHandle);
/// Check if any scripts have changed and trigger reloads if necessary
pub fn check_for_changes(&self) -> Result<bool, RhaiFactoryError>;
}
```
These methods provide the core functionality needed for hot reloading, following the pattern described in the Rhai documentation.
#### 2. Hot Reload Handle
We'll create a handle type to manage hot reload sessions:
```rust
/// Handle for a hot reload session
pub struct HotReloadHandle {
id: uuid::Uuid,
// Other fields as needed
}
```
This handle will be used to identify and manage hot reload sessions.
#### 3. Thread-Safe AST Container
We'll create a thread-safe container for ASTs that can be updated when scripts change:
```rust
/// Thread-safe container for an AST that can be hot reloaded
pub struct HotReloadableAST {
ast: Arc<RwLock<AST>>,
factory: Arc<RhaiFactory>,
handle: Option<HotReloadHandle>,
}
impl HotReloadableAST {
/// Create a new hot reloadable AST
pub fn new(ast: AST, factory: Arc<RhaiFactory>) -> Self;
/// Enable hot reloading for this AST
pub fn enable_hot_reload<P: AsRef<Path>>(
&mut self,
module_paths: &[P],
base_path: Option<P>,
callback: Option<Box<dyn Fn() + Send + Sync>>
) -> Result<(), RhaiFactoryError>;
/// Disable hot reloading for this AST
pub fn disable_hot_reload(&mut self);
/// Get a reference to the underlying AST
pub fn ast(&self) -> &Arc<RwLock<AST>>;
}
```
This follows the pattern in the hot reload documentation where the AST is kept with interior mutability using `Rc<RefCell<AST>>`, but adapted for thread safety using `Arc<RwLock<AST>>` as recommended in the documentation for multi-threaded environments.
#### 4. Module Cache Extensions
We'll extend the ModuleCache to support invalidation when files change:
```rust
impl ModuleCache {
// Existing methods...
/// Invalidate a specific module in the cache
pub fn invalidate<P: AsRef<Path>>(&self, path: P);
/// Check if a module in the cache is outdated
pub fn is_outdated<P: AsRef<Path>>(&self, path: P) -> bool;
/// Update the cache timestamp for a module
pub fn update_timestamp<P: AsRef<Path>>(&self, path: P);
}
```
These methods will help manage the cache when files change, ensuring that the cache is invalidated when necessary.
#### 5. File Monitoring
We'll implement file monitoring using the `notify` crate:
```rust
/// File watcher for hot reloading
struct FileWatcher {
watcher: notify::RecommendedWatcher,
event_receiver: mpsc::Receiver<notify::Result<notify::Event>>,
watched_paths: HashMap<PathBuf, Vec<uuid::Uuid>>,
}
impl FileWatcher {
/// Create a new file watcher
pub fn new() -> Result<Self, RhaiFactoryError>;
/// Watch a file or directory
pub fn watch<P: AsRef<Path>>(&mut self, path: P, id: uuid::Uuid) -> Result<(), RhaiFactoryError>;
/// Stop watching a file or directory
pub fn unwatch<P: AsRef<Path>>(&mut self, path: P, id: uuid::Uuid);
/// Check for file changes
pub fn check_for_changes(&mut self) -> Vec<PathBuf>;
}
```
This implements the file watching functionality needed for hot reloading, similar to the "watch for script file change" functionality mentioned in the hot reload documentation.
### Error Handling
We'll extend the RhaiFactoryError to include hot reload specific errors:
```rust
impl RhaiFactoryError {
// Existing methods...
/// Create a new hot reload error
pub fn hot_reload_error(message: impl Into<String>) -> Self;
/// Add file watcher context to the error
pub fn with_watcher_context(mut self, context: impl Into<String>) -> Self;
}
```
These methods will help provide detailed error information when hot reloading fails.
## Testing Strategy for Hot Reload
### 1. Unit Tests
We'll add unit tests for the hot reload functionality:
```rust
#[cfg(test)]
mod tests {
// Existing tests...
#[test]
fn hot_reload_detects_file_changes() {
// Test that file changes are detected
}
#[test]
fn hot_reload_recompiles_changed_scripts() {
// Test that scripts are recompiled when changed
}
#[test]
fn hot_reload_updates_ast() {
// Test that the AST is updated when scripts change
}
#[test]
fn hot_reload_triggers_callbacks() {
// Test that callbacks are triggered when scripts are reloaded
}
#[test]
fn hot_reload_handles_errors() {
// Test error handling during hot reload
}
}
```
These tests will verify that the hot reload functionality works as expected.
### 2. Integration Tests
We'll add integration tests for the hot reload functionality:
```rust
#[test]
fn factory_hot_reloads_scripts() {
// Create a temporary script file
let temp_dir = tempfile::tempdir().unwrap();
let script_path = temp_dir.path().join("test.rhai");
std::fs::write(&script_path, "40 + 2").unwrap();
// Create a factory and compile the script
let factory = Arc::new(RhaiFactory::with_caching());
let ast = factory.compile_modules(&[&script_path], None).unwrap();
let ast = Arc::new(RwLock::new(ast));
// Enable hot reloading
let reload_detected = Arc::new(AtomicBool::new(false));
let reload_detected_clone = reload_detected.clone();
let callback = Box::new(move || {
reload_detected_clone.store(true, Ordering::SeqCst);
});
let handle = factory.enable_hot_reload(
ast.clone(),
&[&script_path],
None,
Some(callback)
).unwrap();
// Modify the script
std::fs::write(&script_path, "50 + 10").unwrap();
// Wait for the file system to register the change
std::thread::sleep(std::time::Duration::from_millis(100));
// Check for changes
factory.check_for_changes().unwrap();
// Verify the callback was triggered
assert!(reload_detected.load(Ordering::SeqCst));
// Verify the AST was updated
let engine = factory.create_engine();
let result: i64 = engine.eval_ast(&ast.read().unwrap()).unwrap();
assert_eq!(result, 60);
// Disable hot reloading
factory.disable_hot_reload(handle);
}
```
This test verifies that the hot reload functionality works end-to-end, following the pattern described in the Rhai documentation.
### 3. Thread Safety Tests
We'll add tests to verify thread safety:
```rust
#[test]
fn hot_reload_is_thread_safe() {
// Create a temporary script file
let temp_dir = tempfile::tempdir().unwrap();
let script_path = temp_dir.path().join("test.rhai");
std::fs::write(&script_path, "40 + 2").unwrap();
// Create a factory and compile the script
let factory = Arc::new(RhaiFactory::with_caching());
let ast = factory.compile_modules(&[&script_path], None).unwrap();
let ast = Arc::new(RwLock::new(ast));
// Enable hot reloading
let handle = factory.enable_hot_reload(
ast.clone(),
&[&script_path],
None,
None
).unwrap();
// Create threads that read the AST while it's being modified
let threads: Vec<_> = (0..10).map(|_| {
let factory_clone = factory.clone();
let ast_clone = ast.clone();
std::thread::spawn(move || {
for _ in 0..100 {
let engine = factory_clone.create_engine();
let _: Result<i64, _> = engine.eval_ast(&ast_clone.read().unwrap());
std::thread::yield_now();
}
})
}).collect();
// Modify the script multiple times
for i in 0..5 {
std::fs::write(&script_path, format!("40 + {}", i)).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
factory.check_for_changes().unwrap();
}
// Wait for all threads to complete
for thread in threads {
thread.join().unwrap();
}
// Disable hot reloading
factory.disable_hot_reload(handle);
}
```
This test verifies that the hot reload functionality is thread-safe, following the recommendation in the Rhai documentation to use `Arc`, `RwLock`, and the `sync` feature for multi-threaded environments.
## Usage Examples
### Basic Usage
```rust
// Create a factory with caching enabled
let factory = Arc::new(RhaiFactory::with_caching());
// Compile the initial script
let ast = factory.compile_modules(&["main.rhai"], Some("scripts")).unwrap();
let ast = Arc::new(RwLock::new(ast));
// Enable hot reloading
let handle = factory.enable_hot_reload(
ast.clone(),
&["main.rhai"],
Some("scripts"),
Some(Box::new(|| println!("Script reloaded!")))
).unwrap();
// Create an engine and use the AST
let engine = factory.create_engine();
// In your application loop
loop {
// Check for script changes
if factory.check_for_changes().unwrap() {
println!("Scripts were reloaded");
}
// Use the latest version of the script
let result: i64 = engine.eval_ast(&ast.read().unwrap()).unwrap();
// Do something with the result
std::thread::sleep(std::time::Duration::from_secs(1));
}
// When done
factory.disable_hot_reload(handle);
```
This example shows how to use the hot reload functionality in a basic application, following the pattern described in the Rhai documentation.
### With HotReloadableAST
```rust
// Create a factory with caching enabled
let factory = Arc::new(RhaiFactory::with_caching());
// Compile the initial script
let ast = factory.compile_modules(&["main.rhai"], Some("scripts")).unwrap();
// Create a hot reloadable AST
let mut hot_ast = HotReloadableAST::new(ast, factory.clone());
// Enable hot reloading
hot_ast.enable_hot_reload(
&["main.rhai"],
Some("scripts"),
Some(Box::new(|| println!("Script reloaded!")))
).unwrap();
// Create an engine and use the AST
let engine = factory.create_engine();
// In your application loop
loop {
// Check for script changes
factory.check_for_changes().unwrap();
// Use the latest version of the script
let result: i64 = engine.eval_ast(&hot_ast.ast().read().unwrap()).unwrap();
// Do something with the result
std::thread::sleep(std::time::Duration::from_secs(1));
}
// When done
hot_ast.disable_hot_reload();
```
This example shows how to use the `HotReloadableAST` wrapper for a more convenient API.
## Implementation Steps
1. **Add Dependencies**
- Add the `notify` crate for file system monitoring
- Add the `uuid` crate for unique identifiers
2. **Create Hot Reload Manager**
- Implement the `HotReloadManager` struct
- Implement the `FileWatcher` struct
- Implement the `ScriptRegistry` struct
3. **Extend RhaiFactory**
- Add hot reload methods to `RhaiFactory`
- Implement the `HotReloadHandle` struct
- Implement the `HotReloadableAST` struct
4. **Extend ModuleCache**
- Add methods for cache invalidation
- Add timestamp tracking for modules
5. **Implement Error Handling**
- Extend `RhaiFactoryError` for hot reload errors
6. **Write Tests**
- Implement unit tests
- Implement integration tests
- Implement thread safety tests
7. **Update Documentation**
- Add hot reload documentation to README
- Add examples and usage guidelines
## Considerations and Trade-offs
1. **Performance Impact**
- File system monitoring can have a performance impact
- We'll provide options to control the frequency of checks
2. **Memory Usage**
- Keeping multiple versions of scripts in memory can increase memory usage
- We'll provide options to control caching behavior
3. **Thread Safety**
- Hot reloading in a multi-threaded environment requires careful synchronization
- We'll use `RwLock` to allow multiple readers but exclusive writers
- This follows the recommendation in the Rhai documentation to use `Arc`, `RwLock`, and the `sync` feature for multi-threaded environments
4. **Error Handling**
- Script compilation errors during hot reload need to be handled gracefully
- We'll provide options to keep the old script or propagate errors
5. **Dependency Tracking**
- Changes to imported modules need to trigger recompilation of dependent modules
- We'll implement dependency tracking in the `ScriptRegistry`
## References
- Rhai Documentation: `_archive/rhai_engine/rhaibook/`
- Hot Reload Pattern: `_archive/rhai_engine/rhaibook/patterns/hot-reload.md`
- Rhai Engine: `rhai::Engine`
- Rhai AST: `rhai::AST`
- Rhai Module: `rhai::Module`
- Rhai Scope: `rhai::Scope`
- Rhai Sync Feature: `sync`
## Conclusion
The hot reload feature will enhance the `rhai_factory` module by allowing scripts to be modified dynamically without requiring application restart. This will improve the development experience and enable more flexible runtime behavior.
By following the patterns described in the Rhai documentation, we can implement a robust hot reload feature that is thread-safe and provides a good developer experience.
## Tera Engine Factory with Hot Reloadable Rhai Integration
### Overview
We'll create a `TeraFactory` module that provides a factory for creating Tera template engines with integrated Rhai scripting support. The factory will:
1. Create Tera engines with specified template directories
2. Integrate with the hot reloadable Rhai AST from the `RhaiFactory`
3. Allow Rhai functions to be called from Tera templates
4. Automatically update available functions when Rhai scripts are hot reloaded
### Architecture Extension
```mermaid
graph TD
A[TeraFactory] --> B[create_tera_engine]
A --> C[create_tera_with_rhai]
C --> D[Tera Engine with Rhai Functions]
E[RhaiFactory] --> F[HotReloadableAST]
F --> C
G[RhaiFunctionAdapter] --> D
H[Template Directories] --> B
```
### Component Details
#### 1. TeraFactory Module Structure
```
tera_factory/
├── Cargo.toml # Dependencies including tera and rhai_factory
└── src/
├── lib.rs # Main module exports and unit tests
├── factory.rs # Factory implementation and unit tests
├── error.rs # Custom error types and unit tests
└── function_adapter.rs # Rhai function adapter for Tera
```
#### 2. TeraFactory Implementation
The core factory will provide these main functions:
1. **create_tera_engine(template_dirs)** - Creates a basic Tera engine with the specified template directories
2. **create_tera_with_rhai(template_dirs, hot_ast)** - Creates a Tera engine with Rhai function integration using a hot reloadable AST
#### 3. RhaiFunctionAdapter
We'll enhance the existing `RhaiFunctionAdapter` to work with the hot reloadable AST:
```rust
/// Thread-safe adapter to use Rhai functions in Tera templates with hot reload support
pub struct RhaiFunctionAdapter {
fn_name: String,
hot_ast: Arc<RwLock<AST>>,
}
impl TeraFunction for RhaiFunctionAdapter {
fn call(&self, args: &HashMap<String, Value>) -> TeraResult<Value> {
// Convert args from Tera into Rhai's Dynamic
let mut scope = Scope::new();
for (key, value) in args {
// Convert Tera value to Rhai Dynamic
let dynamic = convert_tera_to_rhai(value);
scope.push_dynamic(key.clone(), dynamic);
}
// Create a new engine for each call
let engine = Engine::new();
// Get a read lock on the AST
let ast = self.hot_ast.read().unwrap();
// Call the function using the latest AST
let result = engine
.call_fn::<Dynamic>(&mut scope, &ast, &self.fn_name, ())
.map_err(|e| tera::Error::msg(format!("Rhai error: {}", e)))?;
// Convert Rhai result to Tera value
let tera_value = convert_rhai_to_tera(&result);
Ok(tera_value)
}
}
```
#### 4. TeraFactory API
```rust
pub struct TeraFactory {
// Configuration options
}
impl TeraFactory {
/// Create a new TeraFactory with default settings
pub fn new() -> Self {
Self {}
}
/// Create a Tera engine with the specified template directories
pub fn create_tera_engine<P: AsRef<Path>>(&self, template_dirs: &[P])
-> Result<Tera, TeraFactoryError> {
// Create a Tera engine with the specified template directories
}
/// Create a Tera engine with Rhai function integration
pub fn create_tera_with_rhai<P: AsRef<Path>>(
&self,
template_dirs: &[P],
hot_ast: Arc<RwLock<AST>>
) -> Result<Tera, TeraFactoryError> {
// Create a Tera engine with Rhai function integration
}
}
```
### Implementation Details
#### 1. Creating a Tera Engine
```rust
impl TeraFactory {
pub fn create_tera_engine<P: AsRef<Path>>(&self, template_dirs: &[P])
-> Result<Tera, TeraFactoryError> {
let mut tera = Tera::default();
// Add templates from each directory
for template_dir in template_dirs {
let pattern = format!("{}/**/*.html", template_dir.as_ref().display());
match Tera::parse(&pattern) {
Ok(parsed_tera) => {
tera.extend(&parsed_tera).map_err(|e| {

View File

@ -0,0 +1,950 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"const-random",
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base_path_imports"
version = "0.1.0"
dependencies = [
"chrono",
"rhai",
"rhai_factory",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cc"
version = "1.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.15",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "kqueue"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.9.0",
"libc",
"redox_syscall",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]]
name = "no-std-compat"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
dependencies = [
"spin",
]
[[package]]
name = "notify"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "729f63e1ca555a43fe3efa4f3efdf4801c479da85b432242a7b726f353c88486"
dependencies = [
"bitflags 1.3.2",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"mio",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
dependencies = [
"portable-atomic",
]
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "redox_syscall"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "rhai"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
dependencies = [
"ahash",
"bitflags 2.9.0",
"instant",
"no-std-compat",
"num-traits",
"once_cell",
"rhai_codegen",
"smallvec",
"smartstring",
"thin-vec",
]
[[package]]
name = "rhai_codegen"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rhai_factory"
version = "0.1.0"
dependencies = [
"log",
"notify",
"rhai",
"thiserror",
"uuid",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thin-vec"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "uuid"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.2",
"serde",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-result"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -0,0 +1,9 @@
[package]
name = "base_path_imports"
version = "0.1.0"
edition = "2021"
[dependencies]
rhai_factory = { path = "../.." }
rhai = "1.14.0"
chrono = "0.4.24"

View File

@ -0,0 +1,89 @@
# Base Path Module Resolver Example
This example demonstrates the use of the `BasePathModuleResolver` for simplifying Rhai script module imports.
## Overview
The `BasePathModuleResolver` provides a more predictable and consistent approach to module resolution by:
1. Using a single base path for resolving all module imports
2. Eliminating the complexity of resolving imports relative to the importing file
3. Requiring all import paths to be specified relative to the base path
## Directory Structure
The example follows a hierarchical directory structure:
```
base_path_imports/
├── components/
│ ├── calendar/
│ │ └── controller/
│ │ └── mock/
│ │ └── calendar_model.rhai
│ ├── common/
│ │ └── utils/
│ │ ├── date_utils.rhai
│ │ └── string_utils.rhai
│ └── website/
│ └── controller/
│ └── mock/
│ └── website_model.rhai
├── main.rhai
└── src/
└── main.rs
```
## Key Components
### 1. BasePathModuleResolver
Located in `rhai_factory/src/relative_resolver.rs`, this resolver simplifies module imports by:
- Taking a base path during initialization
- Resolving all module imports relative to this base path
- Providing clear logging of the resolution process
### 2. Utility Modules
Common utility functions are organized in the `components/common/utils/` directory:
- `string_utils.rhai`: Basic string manipulation functions
- `date_utils.rhai`: Date formatting and validation functions
### 3. Component-Specific Modules
Component-specific functionality is organized in dedicated directories:
- `components/calendar/controller/mock/calendar_model.rhai`: Calendar event creation and validation
- `components/website/controller/mock/website_model.rhai`: Website page creation and validation
### 4. Main Script
The `main.rhai` script demonstrates importing and using modules from different components.
## Running the Example
To run the example:
```bash
cd rhai_factory/examples/base_path_imports
cargo run
```
## Key Benefits
1. **Simplified Imports**: All imports are relative to a single base path
2. **Predictable Resolution**: No need to calculate relative paths between files
3. **Cleaner Code**: No need for complex "../../../" style paths
4. **Better Organization**: Encourages a modular, component-based structure
5. **Improved Debugging**: Clear logging of the module resolution process
## Implementation Details
The example demonstrates:
1. Setting up the `BasePathModuleResolver` with a base path
2. Importing modules using paths relative to the base path
3. Using utility functions from common modules
4. Creating component-specific functionality that leverages common utilities

View File

@ -0,0 +1,33 @@
// Calendar model functions
// Import common utility functions
import "components/common/utils/date_utils" as date_utils;
import "components/common/utils/string_utils" as string_utils;
// Creates a new calendar event
fn create_event(title, description, year, month, day) {
let formatted_title = string_utils::to_upper(title);
let formatted_date = date_utils::format_date(year, month, day);
return #{
title: formatted_title,
description: description,
date: formatted_date,
is_valid: is_valid_event_date(year, month, day)
};
}
// Checks if an event date is valid
fn is_valid_event_date(year, month, day) {
// Convert all parameters to integers to ensure consistent comparison
let y = year.to_int();
let m = month.to_int();
let d = day.to_int();
if m < 1 || m > 12 {
return false;
}
let max_days = date_utils::days_in_month(y, m);
return d >= 1 && d <= max_days;
}

View File

@ -0,0 +1,31 @@
// Simple date utility functions
// Format a date as YYYY-MM-DD
fn format_date(year, month, day) {
let month_str = month;
let day_str = day;
return year + "-" + month_str + "-" + day_str;
}
// Check if a year is a leap year
fn is_leap_year(year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
// Get the number of days in a month
fn days_in_month(year, month) {
if month == 2 {
if is_leap_year(year) {
return 29;
} else {
return 28;
}
}
if month == 4 || month == 6 || month == 9 || month == 11 {
return 30;
}
return 31;
}

View File

@ -0,0 +1,16 @@
// Simple string utility functions
// Make a string uppercase
fn to_upper(s) {
return s.to_upper();
}
// Make a string lowercase
fn to_lower(s) {
return s.to_lower();
}
// Get string length
fn length(s) {
return s.len();
}

View File

@ -0,0 +1,28 @@
// Website model functions
// Import common utility functions
import "components/common/utils/string_utils" as string_utils;
// Creates a new website page
fn create_page(title, content, slug) {
let formatted_title = string_utils::to_upper(title);
let slug_length = string_utils::length(slug);
return #{
title: formatted_title,
content: content,
slug: slug,
slug_length: slug_length,
is_valid: is_valid_slug(slug)
};
}
// Validates a page slug
fn is_valid_slug(slug) {
// Simple validation - check if it's not empty and has reasonable length
if slug.len() == 0 || slug.len() > 100 {
return false;
}
return true;
}

View File

@ -0,0 +1,36 @@
// Main script that demonstrates importing modules from different components
// Import the calendar and website models
import "components/calendar/controller/mock/calendar_model" as calendar;
import "components/website/controller/mock/website_model" as website;
// Create a calendar event
fn create_calendar_event(title, description, year, month, day) {
if !calendar::is_valid_event_date(year, month, day) {
return #{ error: "Invalid date" };
}
return calendar::create_event(title, description, year, month, day);
}
// Create a website page
fn create_website_page(title, content, slug) {
if !website::is_valid_slug(slug) {
return #{ error: "Invalid slug" };
}
return website::create_page(title, content, slug);
}
// Example function that combines calendar and website functionality
fn demo() {
// Create a calendar event
let event = create_calendar_event("Team Meeting", "Weekly sync", 2025, 4, 15);
print("Calendar event: " + event);
// Create a website page
let page = create_website_page("About Us", "Company information", "about-us");
print("Website page: " + page);
return "Demo completed successfully";
}

View File

@ -0,0 +1,69 @@
use rhai_factory::{RhaiFactory, BasePathModuleResolver};
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Get the base path for our scripts
let base_path = Path::new(env!("CARGO_MANIFEST_DIR"));
println!("Base path: {}", base_path.display());
println!("This example demonstrates the BasePathModuleResolver which resolves all imports relative to a base path");
// Create a new RhaiFactory
let factory = RhaiFactory::new();
// Create an engine with the base path for module resolution
let mut engine = factory.create_engine();
// Set up the BasePathModuleResolver - this is the key component that simplifies module imports
// All module imports will be resolved relative to this base path
let resolver = BasePathModuleResolver::new_with_path(&base_path);
engine.set_module_resolver(resolver);
// Register basic functions needed by our scripts
engine.register_fn("now", || chrono::Utc::now().to_rfc3339());
// Path to our main script
let main_script_path = base_path.join("main.rhai");
println!("\nEvaluating script: {}", main_script_path.display());
// Compile the main script to an AST
let ast = engine.compile_file(main_script_path)?;
// Create a scope for evaluation
let mut scope = rhai::Scope::new();
// Example 1: Call the demo function which demonstrates both calendar and website functionality
println!("\nExample 1: Running demo function");
let result: String = engine.call_fn(
&mut scope,
&ast,
"demo",
()
)?;
println!("Demo result: {}", result);
// Example 2: Call the calendar event creation function
println!("\nExample 2: Creating a calendar event");
let result: rhai::Map = engine.call_fn(
&mut scope,
&ast,
"create_calendar_event",
("Conference", "Annual tech conference", 2025, 6, 15)
)?;
println!("Calendar event: {:#?}", result);
// Example 3: Call the website page creation function
println!("\nExample 3: Creating a website page");
let result: rhai::Map = engine.call_fn(
&mut scope,
&ast,
"create_website_page",
("Contact", "Our contact information", "contact-us")
)?;
println!("Website page: {:#?}", result);
println!("\nThe BasePathModuleResolver successfully resolved all module imports relative to the base path.");
println!("This approach simplifies module imports and makes the resolution process more predictable.");
Ok(())
}

View File

@ -0,0 +1,105 @@
# Hot Reload Example
This example demonstrates hot reloading of multiple Rhai script files using the `rhai_system` crate. It shows how to:
1. Load and execute multiple script files
2. Watch for changes to these files
3. Automatically reload scripts when they change
4. Call functions across different script files
## How It Works
The example uses two main components:
1. **The System**: Created by `create_hot_reloadable_system` which:
- Loads multiple script files (`script.rhai` and `utils.rhai`)
- Compiles them into a single AST
- Sets up file watchers for each script file
- Provides a clean API for calling functions
2. **Two Threads**:
- **Execution Thread**: Continuously executes functions from the scripts
- **Modification Thread**: Modifies the script files at specific intervals to demonstrate hot reloading
## Cross-Script Function Calls
The example demonstrates how functions in one script can call functions in another:
- `script.rhai` contains the main functions (`greet`, `advanced_calculation`, `multiply`, `divide`)
- `utils.rhai` contains utility functions (`calculate`, `greet_from_utils`)
- Functions in `script.rhai` call functions in `utils.rhai`
This shows how you can organize your code into multiple script files while still maintaining the ability to call functions across files.
## Running the Example
To run this example, navigate to the root of the rhai_system project and execute:
```bash
cargo run --example hot_reload
```
## What to Expect
When you run the example, you'll see:
1. The system loads both `script.rhai` and `utils.rhai`
2. The execution thread calls functions from the scripts every second
3. After 5 seconds, the modification thread updates `script.rhai` to add new functions
4. The execution thread automatically starts using the updated script
5. After another 5 seconds, both script files are modified again
6. The system reloads both scripts and the execution thread uses the latest versions
This demonstrates how the system automatically detects and reloads scripts when they change, without requiring any restart of the application.
## Key Implementation Details
### Multiple Script Support
The system supports multiple script files through:
```rust
// Create a hot reloadable system with multiple script files
let script_paths = vec![
PathBuf::from("examples/hot_reload/script.rhai"),
PathBuf::from("examples/hot_reload/utils.rhai"),
];
let system = create_hot_reloadable_system(&script_paths, None).unwrap();
```
### File Watching
The system automatically sets up file watchers for all script files:
```rust
// Start watching for changes to the script files
system.watch();
```
### Thread-Safe Usage
The system is thread-safe and can be used from multiple threads:
```rust
// Clone the system for use in another thread
let system_clone = Arc::clone(&system);
// Create a thread-local clone for the execution thread
let thread_system = system_clone.clone_for_thread();
```
## Modifying Scripts at Runtime
The example includes functions to modify the script files programmatically:
```rust
// Modify the script file with new content
modify_script(
&script_path,
"examples/hot_reload/modified_script.rhai",
5,
"Modifying the script to add multiply function..."
);
```
This simulates a developer editing the script files during development, demonstrating how the system automatically detects and reloads the scripts.

View File

@ -0,0 +1,21 @@
// This is a simple Rhai script that will be hot reloaded
// It contains functions that will be called by the main program
// It also uses functions from the utils.rhai script
// A simple greeting function
fn greet(name) {
// Use the format_greeting function from utils.rhai
let utils_greeting = format_greeting(name);
"Hello, " + name + "! This is the original script. " + utils_greeting
}
// A function to calculate the sum of two numbers
fn add(a, b) {
a + b
}
// A function that uses the calculate function from utils.rhai
fn advanced_calculation(x, y) {
// Use the calculate function from utils.rhai
calculate(x, y) * 2
}

View File

@ -0,0 +1,13 @@
// Utility functions for the hot reload example
// A function to format a greeting message
fn format_greeting(name) {
"Greetings, " + name + "! (from utils.rhai)"
}
// A function to perform a calculation
// Keep it simple to avoid type issues
fn calculate(a, b) {
// Simple integer arithmetic
(a * b) + 10
}

View File

@ -0,0 +1,152 @@
use std::thread;
use std::time::Duration;
use std::path::PathBuf;
use std::fs::{self, File};
use std::io::Write;
// Import the create_hot_reloadable_system from the library
use rhai_system::create_hot_reloadable_system;
/// Function to modify a script file with content from another file
fn modify_script(target_path: &PathBuf, source_path: &str, delay_secs: u64, message: &str) {
println!("\n🔄 {}", message);
// Read the source script content
let source_script_path = PathBuf::from(source_path);
let source_content = fs::read_to_string(&source_script_path)
.expect(&format!("Failed to read source script file: {}", source_path));
// 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 main_script_path = PathBuf::from("examples/hot_reload/script.rhai");
let utils_script_path = PathBuf::from("examples/hot_reload/utils.rhai");
println!("Main script path: {:?}", main_script_path);
println!("Utils script path: {:?}", utils_script_path);
// Initialize script.rhai with the content from initial_script.rhai
let initial_script_path = PathBuf::from("examples/hot_reload/initial_script.rhai");
let initial_content = fs::read_to_string(&initial_script_path)
.expect("Failed to read initial script file");
let mut file = File::create(&main_script_path)
.expect("Failed to open script file for writing");
file.write_all(initial_content.as_bytes())
.expect("Failed to write to script file");
// Initialize utils.rhai with the content from initial_utils.rhai
let initial_utils_path = PathBuf::from("examples/hot_reload/initial_utils.rhai");
let initial_utils_content = fs::read_to_string(&initial_utils_path)
.expect("Failed to read initial utils file");
let mut utils_file = File::create(&utils_script_path)
.expect("Failed to open utils file for writing");
utils_file.write_all(initial_utils_content.as_bytes())
.expect("Failed to write to utils file");
// Create the hot-reloadable system with both script paths
// We're passing a slice with both paths and using None for main_script_index
// to use the default (first script in the slice)
let system = create_hot_reloadable_system(&[main_script_path.clone(), utils_script_path.clone()], None)?;
// Start a thread that periodically executes the script
let execution_thread = thread::spawn(move || {
// Every second, call the greet function from the script
loop {
// Call the greet function
match system.call_fn::<String>("greet", ("User",)) {
Ok(result) => println!("Execution result: {}", result),
Err(err) => println!("Error executing script: {}", err),
}
// Call the add function
match system.call_fn::<i32>("add", (40, 2)) {
Ok(result) => println!("Add result: {}", result),
Err(err) => println!("Error executing add function: {}", err),
}
// Call the advanced_calculation function that uses utils.rhai
match system.call_fn::<i64>("advanced_calculation", (5_i64, 7_i64)) {
Ok(result) => println!("Advanced calculation result: {}", result),
Err(err) => println!("Error executing advanced_calculation function: {}", err),
}
// Try to call the multiply function, catch any errors
match system.call_fn::<i32>("multiply", (40, 2)) {
Ok(result) => println!("Multiply result: {}", result),
Err(err) => {
if err.to_string().contains("function not found") {
println!("Multiply function not available yet");
} else {
println!("Error executing multiply function: {}", err);
}
}
}
// Try to call the divide function, catch any errors
match system.call_fn::<i32>("divide", (40, 2)) {
Ok(result) => println!("Divide result: {}", result),
Err(err) => {
if err.to_string().contains("function not found") {
println!("Divide function not available yet");
} else {
println!("Error executing divide function: {}", err);
}
}
}
// Wait before the next execution
thread::sleep(Duration::from_secs(1));
}
});
// Start a thread to modify the script files at intervals
let main_script_path_clone = main_script_path.clone();
let utils_script_path_clone = utils_script_path.clone();
thread::spawn(move || {
// Wait 5 seconds before first modification
thread::sleep(Duration::from_secs(5));
// First modification - add multiply function
modify_script(
&main_script_path_clone,
"examples/hot_reload/modified_script.rhai",
10,
"Modifying the script to add multiply function..."
);
// Second modification - add divide function
modify_script(
&main_script_path_clone,
"examples/hot_reload/second_modified_script.rhai",
0,
"Modifying the script again to add divide function..."
);
// Third modification - modify utils.rhai
modify_script(
&utils_script_path_clone,
"examples/hot_reload/modified_utils.rhai",
0,
"Modifying the utils script..."
);
});
// Wait for the execution thread to finish (it won't, but this keeps the main thread alive)
execution_thread.join().unwrap();
Ok(())
}

View File

@ -0,0 +1,22 @@
// This is a modified script
// The AST will be replaced with this new version
// A simple greeting function with modified message
fn greet(name) {
"Hello, " + name + "! This is the MODIFIED script! Hot reloading works!"
}
// A function to calculate the sum of two numbers
fn add(a, b) {
a + b
}
// A new function added during hot reload
fn multiply(a, b) {
a * b
}
// A new function added during hot reload
fn divide(a, b) {
a / b
}

View File

@ -0,0 +1,18 @@
// Utility functions for the hot reload example - MODIFIED VERSION
// A function to format a greeting message
fn format_greeting(name) {
"ENHANCED Greetings, " + name + "! (from modified utils.rhai)"
}
// A function to perform a calculation
// Keep it simple to avoid type issues
fn calculate(a, b) {
// Enhanced calculation with additional operations
(a * b * 2) + 20
}
// A new utility function
fn format_message(text) {
"*** " + text + " ***"
}

View File

@ -0,0 +1,22 @@
// This is a completely overwritten script
// The AST will be replaced with this new version
// A simple greeting function with modified message
fn greet(name) {
"Hello, " + name + "! This is the COMPLETELY OVERWRITTEN script!"
}
// A function to calculate the sum of two numbers
fn add(a, b) {
a + b
}
// A new function added during hot reload
fn multiply(a, b) {
a * b
}
// Another new function added during hot reload
fn divide(a, b) {
a / b
}

View File

@ -0,0 +1,22 @@
// This is a completely overwritten script
// The AST will be replaced with this new version
// A simple greeting function with modified message
fn greet(name) {
"Hello, " + name + "! This is the COMPLETELY OVERWRITTEN script!"
}
// A function to calculate the sum of two numbers
fn add(a, b) {
a + b
}
// A new function added during hot reload
fn multiply(a, b) {
a * b
}
// Another new function added during hot reload
fn divide(a, b) {
a / b
}

View File

@ -0,0 +1,18 @@
// Utility functions for the hot reload example - MODIFIED VERSION
// A function to format a greeting message
fn format_greeting(name) {
"ENHANCED Greetings, " + name + "! (from modified utils.rhai)"
}
// A function to perform a calculation
// Keep it simple to avoid type issues
fn calculate(a, b) {
// Enhanced calculation with additional operations
(a * b * 2) + 20
}
// A new utility function
fn format_message(text) {
"*** " + text + " ***"
}

View File

@ -0,0 +1,10 @@
[package]
name = "relative_imports_example"
version = "0.1.0"
edition = "2021"
[dependencies]
rhai_factory = { path = "../.." }
rhai = { version = "1.21.0", features = ["serde"] }
log = "0.4"
env_logger = "0.10"

View File

@ -0,0 +1,67 @@
// Calendar controller module
// This demonstrates importing from a different directory level
// Import the common utils using a relative path
import "../../../utils/common" as common;
/// Returns data for a single calendar event
fn get_event_data() {
let event = #{
id: 1,
title: "Team Meeting",
date: "2025-04-15",
time: "10:00 AM",
location: "Conference Room A",
description: "Weekly team sync meeting"
};
// Use the common utils to check if the date is in the future
if common::is_future_date(event.date) {
event.status = "Upcoming";
} else {
event.status = "Past";
}
return event;
}
/// Returns a list of all calendar events
fn get_all_events() {
let events = [
#{
id: 1,
title: "Team Meeting",
date: "2025-04-15",
time: "10:00 AM",
location: "Conference Room A",
description: "Weekly team sync meeting"
},
#{
id: 2,
title: "Project Review",
date: "2025-04-17",
time: "2:00 PM",
location: "Meeting Room B",
description: "Review project progress and next steps"
},
#{
id: 3,
title: "Client Presentation",
date: "2025-04-20",
time: "11:30 AM",
location: "Main Auditorium",
description: "Present new features to the client"
}
];
// Update the status of each event using the common utils
for event in events {
if common::is_future_date(event.date) {
event.status = "Upcoming";
} else {
event.status = "Past";
}
}
return events;
}

View File

@ -0,0 +1,23 @@
// Import modules using relative paths
import "utils/common" as common;
import "components/calendar/controller/calendar" as calendar;
// Main function that demonstrates the relative imports
fn main() {
// Print a message using the common utils
let greeting = common::get_greeting("User");
print(greeting);
// Get calendar events using the calendar controller
let events = calendar::get_all_events();
// Print the events using the common utils format function
print("\nCalendar Events:");
for event in events {
let formatted = common::format_event(event);
print(formatted);
}
// Return a success message
"All imports worked correctly!"
}

View File

@ -0,0 +1,18 @@
// Common utility functions used across the application
/// Returns a greeting message for the given name
fn get_greeting(name) {
return `Hello, ${name}! Welcome to the Rhai relative imports example.`;
}
/// Formats an event object into a readable string
fn format_event(event) {
return `- ${event.title} on ${event.date} at ${event.time} (${event.location})`;
}
/// Utility function to check if a date is in the future
fn is_future_date(date_str) {
// Simple implementation for the example
// In a real application, you would parse the date and compare with current date
return true;
}

View File

@ -0,0 +1,58 @@
use std::path::Path;
use rhai_factory::{RhaiFactory, RelativeFileModuleResolver};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize the logger with debug level
std::env::set_var("RUST_LOG", "debug");
env_logger::init();
println!("Starting Relative Imports Example...");
// Get the paths to our script files
let scripts_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("scripts");
let main_script_path = scripts_dir.join("main.rhai");
println!("Scripts directory: {:?}", scripts_dir);
println!("Main script path: {:?}", main_script_path);
// Create a RhaiFactory instance
let factory = RhaiFactory::new();
// Method 1: Using the RhaiFactory which now uses RelativeFileModuleResolver by default
println!("\nMethod 1: Using RhaiFactory with RelativeFileModuleResolver (default)");
let ast = factory.compile_modules(&[&main_script_path], Some(&scripts_dir))?;
let engine = factory.create_engine();
// Evaluate the main script
let result: String = engine.eval_ast(&ast)?;
println!("Result: {}", result);
// Method 2: Creating an engine manually with RelativeFileModuleResolver
println!("\nMethod 2: Creating an engine manually with RelativeFileModuleResolver");
let mut manual_engine = rhai::Engine::new();
manual_engine.set_module_resolver(RelativeFileModuleResolver::new_with_path(&scripts_dir));
// Evaluate the main script directly
let result: String = manual_engine.eval_file(main_script_path)?;
println!("Result: {}", result);
// Demonstrate hot reloading capability
println!("\nDemonstrating hot reload capability:");
println!("1. The engine will watch for changes to the script files");
println!("2. If you modify any of the script files, the changes will be detected");
println!("3. The engine will automatically reload the modified scripts");
// Create a hot reloadable engine
let (hot_engine, hot_ast, hot_reload_handle) =
factory.create_hot_reloadable_rhai_engine(&[&main_script_path], Some(&scripts_dir))?;
// Evaluate the main script with hot reloading
let result: String = hot_engine.eval_ast(&hot_ast.read().unwrap())?;
println!("Hot reload result: {}", result);
println!("\nExample completed successfully!");
println!("This example demonstrates how the RelativeFileModuleResolver makes imports work");
println!("relative to the file importing them, rather than just relative to a fixed base path.");
Ok(())
}

View File

@ -0,0 +1,702 @@
# Rhai Factory Project Structure
This document outlines the structure and content of the implementation files for the Rhai Factory project.
## Directory Structure
```
rhai_factory/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── factory.rs
│ ├── error.rs
│ └── module_cache.rs
└── tests/
├── common/
│ └── mod.rs
├── integration_tests.rs
└── rhai_scripts/
├── main.rhai
├── module1.rhai
└── module2.rhai
```
## File Contents
### Cargo.toml
```toml
[package]
name = "rhai_factory"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <your.email@example.com>"]
description = "A thread-safe factory for creating and managing Rhai script engines"
repository = "https://github.com/yourusername/rhai_factory"
license = "MIT"
readme = "README.md"
[dependencies]
rhai = { version = "1.15.0", features = ["sync"] }
thiserror = "1.0"
log = "0.4"
[dev-dependencies]
tokio = { version = "1.28", features = ["full"] }
tempfile = "3.5"
```
### src/lib.rs
```rust
//! A thread-safe factory for creating and managing Rhai script engines.
//!
//! This crate provides a factory for creating thread-safe Rhai engines with
//! pre-compiled scripts. It handles module imports, provides detailed error
//! information, and ensures thread safety.
mod factory;
mod error;
mod module_cache;
pub use factory::RhaiFactory;
pub use error::RhaiFactoryError;
/// Re-export commonly used Rhai types for convenience
pub use rhai::{Engine, AST, Scope, Module};
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
// Test fixture for common setup
struct TestFixture {
factory: RhaiFactory,
}
impl TestFixture {
fn new() -> Self {
Self {
factory: RhaiFactory::new(),
}
}
fn with_caching() -> Self {
Self {
factory: RhaiFactory::with_caching(),
}
}
}
#[test]
fn engine_can_evaluate_simple_expressions() {
let fixture = TestFixture::new();
let engine = fixture.factory.create_engine();
let result: i64 = engine.eval("40 + 2").unwrap();
assert_eq!(result, 42);
}
#[test]
fn factory_creates_thread_safe_engine() {
let fixture = TestFixture::new();
let engine = fixture.factory.create_engine();
// This test verifies that the engine can be sent between threads
std::thread::spawn(move || {
let result: i64 = engine.eval("40 + 2").unwrap();
assert_eq!(result, 42);
}).join().unwrap();
}
#[test]
fn module_cache_improves_performance() {
let fixture_no_cache = TestFixture::new();
let fixture_with_cache = TestFixture::with_caching();
// First compilation without cache
let start = std::time::Instant::now();
let _ = fixture_no_cache.factory.compile_modules(
&[Path::new("tests/rhai_scripts/main.rhai")],
Some(Path::new(".")),
).unwrap();
let no_cache_time = start.elapsed();
// First compilation with cache
let start = std::time::Instant::now();
let _ = fixture_with_cache.factory.compile_modules(
&[Path::new("tests/rhai_scripts/main.rhai")],
Some(Path::new(".")),
).unwrap();
let first_cache_time = start.elapsed();
// Second compilation with cache should be faster
let start = std::time::Instant::now();
let _ = fixture_with_cache.factory.compile_modules(
&[Path::new("tests/rhai_scripts/main.rhai")],
Some(Path::new(".")),
).unwrap();
let second_cache_time = start.elapsed();
// The second compilation with cache should be faster than the first
assert!(second_cache_time < first_cache_time);
}
}
```
### src/factory.rs
```rust
//! Implementation of the RhaiFactory.
use rhai::{Engine, AST, Scope, Module, EvalAltResult};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::error::RhaiFactoryError;
use crate::module_cache::ModuleCache;
/// A factory for creating thread-safe Rhai engines with pre-compiled scripts.
pub struct RhaiFactory {
/// Optional module cache for improved performance
module_cache: Option<ModuleCache>,
}
impl RhaiFactory {
/// Create a new RhaiFactory with default settings.
pub fn new() -> Self {
Self {
module_cache: None,
}
}
/// Create a new RhaiFactory with module caching enabled.
pub fn with_caching() -> Self {
Self {
module_cache: Some(ModuleCache::new()),
}
}
/// Create a thread-safe Rhai engine.
pub fn create_engine(&self) -> Engine {
let mut engine = Engine::new();
// Configure the engine for thread safety
// The sync feature ensures the engine is Send + Sync
engine
}
/// Compile a list of Rhai modules into a self-contained AST.
///
/// # Arguments
///
/// * `module_paths` - A list of paths to Rhai script modules
/// * `base_path` - An optional base path for resolving relative module paths
///
/// # Returns
///
/// A Result containing either the compiled AST or a RhaiFactoryError
pub fn compile_modules<P: AsRef<Path>>(&self, module_paths: &[P], base_path: Option<P>)
-> Result<AST, RhaiFactoryError> {
// Implementation details...
// 1. Create a new engine
// 2. Set up a file module resolver with the base path
// 3. Compile the main module
// 4. Compile into a self-contained AST to handle imports
// 5. Return the compiled AST
}
/// Create an engine with pre-compiled modules.
///
/// # Arguments
///
/// * `module_paths` - A list of paths to Rhai script modules
/// * `base_path` - An optional base path for resolving relative module paths
///
/// # Returns
///
/// A Result containing either a tuple of (Engine, AST) or a RhaiFactoryError
pub fn create_engine_with_modules<P: AsRef<Path>>(&self, module_paths: &[P], base_path: Option<P>)
-> Result<(Engine, AST), RhaiFactoryError> {
// Implementation details...
// 1. Create a new engine
// 2. Compile the modules
// 3. Return the engine and compiled AST
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn compile_modules_handles_single_module() {
let factory = RhaiFactory::new();
let result = factory.compile_modules(
&[Path::new("tests/rhai_scripts/module2.rhai")],
Some(Path::new(".")),
);
assert!(result.is_ok());
let ast = result.unwrap();
let engine = factory.create_engine();
// Verify the module was compiled correctly
let scope = Scope::new();
let result = engine.eval_ast_with_scope::<()>(&mut scope.clone(), &ast);
assert!(result.is_ok());
// Verify the function was defined
let result = engine.call_fn::<i64>(&mut scope, &ast, "multiply", (6, 7));
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[test]
fn compile_modules_handles_module_imports() {
let factory = RhaiFactory::new();
let result = factory.compile_modules(
&[Path::new("tests/rhai_scripts/main.rhai")],
Some(Path::new(".")),
);
assert!(result.is_ok());
let ast = result.unwrap();
let engine = factory.create_engine();
let result: i64 = engine.eval_ast(&ast).unwrap();
assert_eq!(result, 42);
}
#[test]
fn create_engine_with_modules_returns_usable_engine_and_ast() {
let factory = RhaiFactory::new();
let result = factory.create_engine_with_modules(
&[Path::new("tests/rhai_scripts/main.rhai")],
Some(Path::new(".")),
);
assert!(result.is_ok());
let (engine, ast) = result.unwrap();
let result: i64 = engine.eval_ast(&ast).unwrap();
assert_eq!(result, 42);
}
}
```
### src/error.rs
```rust
//! Error types for the RhaiFactory.
use std::error::Error;
use std::fmt;
use std::path::PathBuf;
/// Error type for RhaiFactory operations.
#[derive(Debug)]
pub struct RhaiFactoryError {
/// Path to the module that caused the error, if any
module_path: Option<PathBuf>,
/// Error message
message: String,
/// Source error, if any
source: Option<Box<dyn Error + Send + Sync>>,
}
impl RhaiFactoryError {
/// Create a new RhaiFactoryError with the given message.
pub fn new(message: impl Into<String>) -> Self {
Self {
module_path: None,
message: message.into(),
source: None,
}
}
/// Add a module path to the error.
pub fn with_module(mut self, module_path: impl Into<PathBuf>) -> Self {
self.module_path = Some(module_path.into());
self
}
/// Add a source error to the 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 RhaiFactoryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ref path) = self.module_path {
write!(f, "Error in module '{}': {}", path.display(), self.message)
} else {
write!(f, "{}", self.message)
}
}
}
impl Error for RhaiFactoryError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source.as_ref().map(|s| s.as_ref() as &(dyn Error + 'static))
}
}
/// Convert Rhai's EvalAltResult to RhaiFactoryError.
impl From<Box<rhai::EvalAltResult>> for RhaiFactoryError {
fn from(err: Box<rhai::EvalAltResult>) -> Self {
RhaiFactoryError::new(format!("Rhai evaluation error: {}", err))
.with_source(RhaiEvalError(err))
}
}
/// Wrapper for Rhai's EvalAltResult to implement Error.
#[derive(Debug)]
struct RhaiEvalError(Box<rhai::EvalAltResult>);
impl fmt::Display for RhaiEvalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for RhaiEvalError {}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Error as IoError, ErrorKind};
#[test]
fn error_displays_module_path_when_available() {
let error = RhaiFactoryError::new("test error")
.with_module("test/path.rhai");
assert_eq!(
format!("{}", error),
"Error in module 'test/path.rhai': test error"
);
}
#[test]
fn error_displays_message_without_module_path() {
let error = RhaiFactoryError::new("test error");
assert_eq!(format!("{}", error), "test error");
}
#[test]
fn error_preserves_source_error() {
let io_error = IoError::new(ErrorKind::NotFound, "file not found");
let error = RhaiFactoryError::new("test error")
.with_source(io_error);
assert!(error.source().is_some());
assert_eq!(
error.source().unwrap().to_string(),
"file not found"
);
}
}
```
### src/module_cache.rs
```rust
//! Module caching for improved performance.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use rhai::AST;
/// A cache for compiled Rhai modules.
pub struct ModuleCache {
/// Map of module paths to compiled ASTs
cache: Arc<Mutex<HashMap<PathBuf, Arc<AST>>>>,
}
impl ModuleCache {
/// Create a new empty module cache.
pub fn new() -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::new())),
}
}
/// Get a cached AST for the given module path, if available.
pub fn get<P: AsRef<Path>>(&self, path: P) -> Option<Arc<AST>> {
let path = path.as_ref().to_path_buf();
let cache = self.cache.lock().unwrap();
cache.get(&path).cloned()
}
/// Store an AST in the cache for the given module path.
pub fn put<P: AsRef<Path>>(&self, path: P, ast: AST) -> Arc<AST> {
let path = path.as_ref().to_path_buf();
let ast = Arc::new(ast);
let mut cache = self.cache.lock().unwrap();
cache.insert(path, ast.clone());
ast
}
/// Clear the cache.
pub fn clear(&self) {
let mut cache = self.cache.lock().unwrap();
cache.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use rhai::{Engine, Scope};
#[test]
fn cache_stores_and_retrieves_ast() {
let cache = ModuleCache::new();
let engine = Engine::new();
let ast = engine.compile("40 + 2").unwrap();
let path = PathBuf::from("test.rhai");
// Store the AST in the cache
let cached_ast = cache.put(&path, ast);
// Retrieve the AST from the cache
let retrieved_ast = cache.get(&path);
assert!(retrieved_ast.is_some());
// Verify the retrieved AST works correctly
let retrieved_ast = retrieved_ast.unwrap();
let result: i64 = engine.eval_ast(&retrieved_ast).unwrap();
assert_eq!(result, 42);
}
#[test]
fn cache_returns_none_for_missing_ast() {
let cache = ModuleCache::new();
let path = PathBuf::from("nonexistent.rhai");
let retrieved_ast = cache.get(&path);
assert!(retrieved_ast.is_none());
}
#[test]
fn cache_clear_removes_all_entries() {
let cache = ModuleCache::new();
let engine = Engine::new();
// Add multiple ASTs to the cache
let ast1 = engine.compile("40 + 2").unwrap();
let ast2 = engine.compile("50 + 3").unwrap();
cache.put("test1.rhai", ast1);
cache.put("test2.rhai", ast2);
// Verify the ASTs are in the cache
assert!(cache.get("test1.rhai").is_some());
assert!(cache.get("test2.rhai").is_some());
// Clear the cache
cache.clear();
// Verify the ASTs are no longer in the cache
assert!(cache.get("test1.rhai").is_none());
assert!(cache.get("test2.rhai").is_none());
}
}
```
### tests/common/mod.rs
```rust
//! Common utilities for integration tests.
use rhai_factory::RhaiFactory;
use std::path::{Path, PathBuf};
/// Test fixture for integration tests.
pub struct TestFixture {
pub factory: RhaiFactory,
pub scripts_dir: PathBuf,
}
impl TestFixture {
/// Create a new test fixture.
pub fn new() -> Self {
let factory = RhaiFactory::new();
let scripts_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("rhai_scripts");
Self {
factory,
scripts_dir,
}
}
/// Get the path to a test script.
pub fn script_path(&self, name: &str) -> PathBuf {
self.scripts_dir.join(name)
}
}
```
### tests/integration_tests.rs
```rust
//! Integration tests for the RhaiFactory.
mod common;
use common::TestFixture;
use rhai_factory::{RhaiFactory, RhaiFactoryError};
use std::path::Path;
#[test]
fn factory_compiles_and_runs_scripts() {
let fixture = TestFixture::new();
// Compile the main script
let result = fixture.factory.compile_modules(
&[fixture.script_path("main.rhai")],
Some(&fixture.scripts_dir),
);
assert!(result.is_ok());
// Run the compiled script
let ast = result.unwrap();
let engine = fixture.factory.create_engine();
let result: i64 = engine.eval_ast(&ast).unwrap();
assert_eq!(result, 42);
}
#[test]
fn factory_handles_recursive_imports() {
let fixture = TestFixture::new();
// Compile the main script which imports module1, which imports module2
let result = fixture.factory.compile_modules(
&[fixture.script_path("main.rhai")],
Some(&fixture.scripts_dir),
);
assert!(result.is_ok());
// Run the compiled script
let ast = result.unwrap();
let engine = fixture.factory.create_engine();
let result: i64 = engine.eval_ast(&ast).unwrap();
assert_eq!(result, 42);
}
#[test]
fn factory_provides_detailed_error_for_missing_module() {
let fixture = TestFixture::new();
// Try to compile a non-existent script
let result = fixture.factory.compile_modules(
&[fixture.script_path("non_existent.rhai")],
Some(&fixture.scripts_dir),
);
assert!(result.is_err());
// Verify the error contains the module path
let err = result.unwrap_err();
assert!(format!("{}", err).contains("non_existent.rhai"));
}
#[test]
fn factory_creates_thread_safe_engine_and_ast() {
let fixture = TestFixture::new();
// Compile the main script
let result = fixture.factory.compile_modules(
&[fixture.script_path("main.rhai")],
Some(&fixture.scripts_dir),
);
assert!(result.is_ok());
let ast = result.unwrap();
let engine = fixture.factory.create_engine();
// Verify the engine and AST can be sent to another thread
let handle = std::thread::spawn(move || {
let result: i64 = engine.eval_ast(&ast).unwrap();
result
});
let result = handle.join().unwrap();
assert_eq!(result, 42);
}
```
### tests/rhai_scripts/main.rhai
```rhai
// Import the module1 module
import "module1" as m1;
// Call a function from the imported module
let result = m1::add(40, 2);
// Return the result
result
```
### tests/rhai_scripts/module1.rhai
```rhai
// Import the module2 module
import "module2" as m2;
// Define a function that uses a function from module2
fn add(a, b) {
// Call the multiply function from module2
let product = m2::multiply(a, 1);
// Add b to the product
product + b
}
```
### tests/rhai_scripts/module2.rhai
```rhai
// Define a function that multiplies two numbers
fn multiply(a, b) {
a * b
}
```
## Implementation Notes
1. The `sync` feature of Rhai is used to ensure thread safety
2. Module compilation uses the `compile_into_self_contained` method to handle imports
3. Error handling provides detailed information about which module failed to import and why
4. Module caching is optional but can improve performance when repeatedly using the same modules
5. Tests follow Rust's standard approach with unit tests in each module and integration tests in the tests directory
## Next Steps
To implement this project:
1. Create the directory structure as outlined above
2. Create the implementation files with the provided content
3. Run the tests to verify that everything works as expected
4. Add additional features or optimizations as needed

View File

@ -0,0 +1,69 @@
use rhai::Engine;
use std::sync::{Arc, RwLock};
use crate::system::System;
use crate::hot_reload::hot_reload_callback;
/// Creates a hot-reloadable system from script files
///
/// # Arguments
///
/// * `script_paths` - A slice of paths to Rhai script files
/// * `main_script_index` - Optional index of the main script in the paths slice. If None, the first script is used.
///
/// # Returns
///
/// A Result containing either the System or an error
pub fn create_hot_reloadable_system<P: AsRef<std::path::Path> + Clone>(
script_paths: &[P],
main_script_index: Option<usize>
) -> Result<System, Box<dyn std::error::Error>> {
if script_paths.is_empty() {
return Err("No script paths provided".into());
}
// Determine which script is the main script
let main_index = main_script_index.unwrap_or(0);
if main_index >= script_paths.len() {
return Err(format!("Invalid main script index: {}, max index: {}", main_index, script_paths.len() - 1).into());
}
// Create a new engine
let engine = Engine::new();
// Compile the main script first
let main_script_path = script_paths[main_index].as_ref();
let mut combined_ast = engine.compile_file(main_script_path.to_path_buf())?;
// Compile and merge all other scripts
for (i, script_path) in script_paths.iter().enumerate() {
if i == main_index {
continue; // Skip the main script as it's already compiled
}
// Compile the additional script
let path = script_path.as_ref();
let ast = engine.compile_file(path.to_path_buf())?;
// Merge the AST with the main AST
// This appends statements and functions from the additional script
// Functions with the same name and arity will override previous definitions
combined_ast = combined_ast.merge(&ast);
}
// Wrap the combined AST in a thread-safe container
let shared_ast = Arc::new(RwLock::new(combined_ast));
// Convert script paths to PathBuf
let script_paths_vec: Vec<std::path::PathBuf> = script_paths.iter()
.map(|path| path.as_ref().to_path_buf())
.collect();
// Create the system with the engine, AST, and script paths
let system = System::new(engine, shared_ast, script_paths_vec);
// Watch for script file change using the hot_reload_callback
system.watch(hot_reload_callback)?;
// Return a thread-safe version of the system
Ok(system.clone_for_thread())
}

View File

@ -0,0 +1,15 @@
use crate::system::System;
/// Callback function for hot reloading a script
///
/// This function is called when a script file is modified.
/// It compiles the new script and replaces the old AST with the new one.
pub fn hot_reload_callback(sys: &System, file: &str) -> Result<(), Box<dyn std::error::Error>> {
// Compile the new script
let ast = sys.engine.compile_file(file.into())?;
// Hot reload - just replace the old script!
*sys.script.write().unwrap() += ast;
Ok(())
}

41
rhai_system/src/lib.rs Normal file
View File

@ -0,0 +1,41 @@
//! A thread-safe system for creating and managing Rhai script engines.
//!
//! This crate provides a system for creating thread-safe Rhai engines with
//! pre-compiled scripts. It supports hot reloading of scripts and handles
//! multiple script files.
mod hot_reload;
mod system;
mod factory;
pub use system::System;
pub use factory::create_hot_reloadable_system;
pub use hot_reload::hot_reload_callback;
/// Re-export commonly used Rhai types for convenience
pub use rhai::{Engine, AST, Scope, Module};
#[cfg(test)]
mod tests {
use super::*;
// Test fixture for common setup
struct TestFixture {
engine: Engine,
}
impl TestFixture {
fn new() -> Self {
Self {
engine: Engine::new(),
}
}
}
#[test]
fn engine_can_evaluate_simple_expressions() {
let fixture = TestFixture::new();
let result: i64 = fixture.engine.eval("40 + 2").unwrap();
assert_eq!(result, 42);
}
}

101
rhai_system/src/system.rs Normal file
View File

@ -0,0 +1,101 @@
use rhai::{Engine, Scope, AST};
use std::sync::{Arc, RwLock};
use std::thread;
use std::path::PathBuf;
use notify::{Watcher, RecursiveMode};
use std::sync::mpsc::channel;
/// System struct to encapsulate the engine and script AST
pub struct System {
pub engine: Engine,
pub script: Arc<RwLock<AST>>,
pub script_paths: Vec<PathBuf>,
}
impl System {
/// Create a new System with the given script
pub fn new(engine: Engine, script: Arc<RwLock<AST>>, script_paths: Vec<PathBuf>) -> Self {
Self { engine, script, script_paths }
}
/// Execute a function from the script
pub fn call_fn<T: Clone + Send + Sync + 'static>(&self, fn_name: &str, args: impl rhai::FuncArgs) -> Result<T, Box<dyn std::error::Error>> {
let mut scope = Scope::new();
let ast_guard = self.script.read().unwrap();
let result = self.engine.call_fn(&mut scope, &ast_guard, fn_name, args)?;
Ok(result)
}
/// Get a reference to the shared AST
pub fn get_script(&self) -> &Arc<RwLock<AST>> {
&self.script
}
/// Clone this system for another thread
pub fn clone_for_thread(&self) -> Self {
Self {
engine: Engine::new(),
script: Arc::clone(&self.script),
script_paths: self.script_paths.clone(),
}
}
/// Watch for script file changes and automatically reload the AST
pub fn watch<F>(&self, callback: F) -> Result<(), Box<dyn std::error::Error>>
where
F: Fn(&System, &str) -> Result<(), Box<dyn std::error::Error>> + Send + 'static
{
if self.script_paths.is_empty() {
return Err("No script paths available to watch".into());
}
let system_clone = self.clone_for_thread();
// Create a channel to receive file system events
let (tx, rx) = channel();
// Create a watcher that will watch the specified paths for changes
let mut watcher = notify::recommended_watcher(tx)?;
// Clone script paths for the thread
let script_paths = self.script_paths.clone();
// Watch all script files for changes
for script_path in &script_paths {
watcher.watch(script_path, RecursiveMode::NonRecursive)?;
println!("🔍 Watching for changes to script file: {:?}", script_path);
}
// Start a thread to handle file system events
thread::spawn(move || {
// Move watcher into the thread to keep it alive
let _watcher = watcher;
loop {
match rx.recv() {
Ok(event) => {
println!("📝 Detected file system event: {:?}", event);
// Extract the path from the event
// The event is a Result<Event, Error>, so we need to unwrap it first
if let Ok(event_data) = event {
if let Some(path) = event_data.paths.first() {
// Convert path to string
if let Some(path_str) = path.to_str() {
// Call the callback with the system and script path
match callback(&system_clone, path_str) {
Ok(_) => println!("✅ Script reloaded successfully"),
Err(err) => println!("❌ Error reloading script: {}", err),
}
}
}
}
}
Err(e) => println!("❌ Error receiving file system event: {:?}", e),
}
}
});
Ok(())
}
}

View File

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

View File

@ -0,0 +1,52 @@
//! Common utilities for integration tests.
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use rand;
/// Test fixture for integration tests.
pub struct TestFixture {
pub scripts_dir: PathBuf,
}
impl TestFixture {
/// Create a new test fixture.
pub fn new() -> Self {
let scripts_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("rhai_scripts");
Self {
scripts_dir,
}
}
/// Get the path to a test script.
pub fn script_path(&self, name: &str) -> PathBuf {
self.scripts_dir.join(name)
}
}
/// Create a temporary test directory with a unique name.
pub fn setup_test_dir(prefix: &str) -> PathBuf {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("temp")
.join(format!("{}_{}_{}", prefix, timestamp, rand::random::<u16>()));
fs::create_dir_all(&test_dir).unwrap();
test_dir
}
/// Clean up a temporary test directory.
pub fn cleanup_test_dir(dir: PathBuf) {
if dir.exists() && dir.starts_with(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("temp")) {
let _ = fs::remove_dir_all(dir);
}
}

View File

@ -0,0 +1,125 @@
use std::fs;
use std::sync::{Arc, RwLock};
use rhai::Engine;
use rhai_system::{System, hot_reload_callback};
#[test]
fn test_hot_reload_callback() {
// Create temporary script files
let temp_dir = tempfile::tempdir().unwrap();
let script_path = temp_dir.path().join("test_script.rhai");
// Write initial script content
let initial_script = r#"
fn greet(name) {
"Hello, " + name + "! This is the original script."
}
fn add(a, b) {
a + b
}
"#;
fs::write(&script_path, initial_script).unwrap();
// Create engine and compile initial script
let engine = Engine::new();
let ast = engine.compile_file(script_path.clone()).unwrap();
let shared_ast = Arc::new(RwLock::new(ast));
// Create a system with the initial script
let script_paths = vec![script_path.clone()];
let system = System::new(engine, shared_ast, script_paths);
// Test initial script functionality
let result: String = system.call_fn("greet", ("User",)).unwrap();
assert_eq!(result, "Hello, User! This is the original script.");
let add_result: i32 = system.call_fn("add", (40, 2)).unwrap();
assert_eq!(add_result, 42);
// Write modified script content with new functions
let modified_script = r#"
fn greet(name) {
"Hello, " + name + "! This is the MODIFIED script!"
}
fn add(a, b) {
a + b
}
fn multiply(a, b) {
a * b
}
"#;
fs::write(&script_path, modified_script).unwrap();
// Call the hot reload callback
hot_reload_callback(&system, script_path.to_str().unwrap()).unwrap();
// Test that the script was updated
let result: String = system.call_fn("greet", ("User",)).unwrap();
assert_eq!(result, "Hello, User! This is the MODIFIED script!");
// Test that the new function is available
let multiply_result: i32 = system.call_fn("multiply", (6, 7)).unwrap();
assert_eq!(multiply_result, 42);
}
#[test]
fn test_hot_reload_callback_with_syntax_error() {
// Create temporary script files
let temp_dir = tempfile::tempdir().unwrap();
let script_path = temp_dir.path().join("test_script.rhai");
// Write initial script content
let initial_script = r#"
fn greet(name) {
"Hello, " + name + "! This is the original script."
}
fn add(a, b) {
a + b
}
"#;
fs::write(&script_path, initial_script).unwrap();
// Create engine and compile initial script
let engine = Engine::new();
let ast = engine.compile_file(script_path.clone()).unwrap();
let shared_ast = Arc::new(RwLock::new(ast));
// Create a system with the initial script
let script_paths = vec![script_path.clone()];
let system = System::new(engine, shared_ast, script_paths);
// Test initial script functionality
let result: String = system.call_fn("greet", ("User",)).unwrap();
assert_eq!(result, "Hello, User! This is the original script.");
// Write modified script content with syntax error
let modified_script = r#"
fn greet(name) {
"Hello, " + name + "! This is the MODIFIED script!"
}
fn add(a, b) {
a + b
}
fn syntax_error() {
// Missing closing brace
if (true) {
"This will cause a syntax error"
}
"#;
fs::write(&script_path, modified_script).unwrap();
// Call the hot reload callback - it should return an error
let result = hot_reload_callback(&system, script_path.to_str().unwrap());
assert!(result.is_err());
// Test that the original script functionality is still available
let result: String = system.call_fn("greet", ("User",)).unwrap();
assert_eq!(result, "Hello, User! This is the original script.");
}

View File

@ -0,0 +1,9 @@
// main.rhai - Main script that imports module1
import "module1" as m1;
// Call the calculate function from module1, which in turn calls multiply from module2
let answer = m1::calculate();
// Return the answer
answer

View File

@ -0,0 +1,9 @@
// module1.rhai - A simple module that imports module2
import "module2" as m2;
fn calculate() {
// Call the multiply function from module2
let result = m2::multiply(6, 7);
result
}

View File

@ -0,0 +1,5 @@
// module2.rhai - A simple module with a multiply function
fn multiply(x, y) {
x * y
}

View File

@ -0,0 +1 @@
fn get_value() { 84 ; ; }

View File

@ -0,0 +1 @@
fn get_value() { 84 ; ; }

View File

@ -0,0 +1,2 @@
import "module" as m;
fn calculate() { 21 * m::get_multiplier() }

View File

@ -0,0 +1 @@
fn get_multiplier() { 4 }

View File

@ -0,0 +1,2 @@
import "module" as m;
fn calculate() { 21 * m::get_multiplier() }

View File

@ -0,0 +1 @@
fn get_multiplier() { 4 }

View File

@ -0,0 +1,2 @@
import "module" as m;
fn calculate() { 21 * m::get_multiplier() }

View File

@ -0,0 +1 @@
fn get_multiplier() { 4 }

View File

@ -0,0 +1,2 @@
import "module" as m;
fn calculate() { 21 * m::get_multiplier() }

View File

@ -0,0 +1 @@
fn get_multiplier() { 4 }

View File

@ -0,0 +1,2 @@
fn get_multiplier() { 4 }
fn calculate() { 21 * get_multiplier() }

View File

@ -0,0 +1 @@
// This is a secondary file that will be modified

View File

@ -0,0 +1,2 @@
fn get_multiplier() { 4 }
fn calculate() { 21 * get_multiplier() }

View File

@ -0,0 +1 @@
// This is a secondary file that will be modified

View File

@ -0,0 +1 @@
fn get_value() { 84 }

View File

@ -0,0 +1 @@
fn get_value() { 84 }