Compare commits

...
This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.

8 Commits

Author SHA1 Message Date
timurgordon
db5b9a0a42 archive old code 2025-06-03 21:47:59 +03:00
timurgordon
b20140785e add engine and rpc client, archive old code 2025-06-03 21:47:36 +03:00
timurgordon
061aee6f1d rhai rpc queue worker and client 2025-06-01 02:10:58 +03:00
timurgordon
ec4769a6b0 more efforts to automate rhai bindings 2025-05-13 02:00:35 +03:00
timurgordon
16ad4f5743 create macros for generating rhai wrappers and add tests 2025-05-12 02:31:45 +03:00
Timur Gordon
22032f329a feat(tera_factory): Implement hot reload example for Tera templates with Rhai
This commit adds a comprehensive hot reload example that demonstrates how to use the rhai_system for dynamic template rendering with Tera. Key improvements include:

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

This example showcases how changes to Rhai scripts are automatically detected and applied to rendered templates without restarting the application, providing a smooth development experience.
2025-05-02 21:34:28 +02:00
Timur Gordon
c23de6871b remove old examples 2025-05-02 21:05:30 +02:00
Timur Gordon
372b7a2772 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.
2025-05-02 21:04:33 +02:00
135 changed files with 23154 additions and 509 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

515
_archive/lib.rs Normal file
View File

@ -0,0 +1,515 @@
// rhai_macros_derive/src/lib.rs
// We will add our derive macro implementations here.
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, format_ident, quote_spanned};
use syn::{parse_macro_input, Ident, Type, ItemFn, spanned::Spanned, PathArguments, GenericArgument, DeriveInput, Data, LitStr};
// Old ToRhaiMap and FromRhaiMap definitions will be removed from here.
// The export_fn macro definition starts after this.
// Trait definitions removed from here as proc-macro crates cannot export them.
// They should be defined in a regular library crate (e.g., rhai_wrapper or a new rhai_traits crate).
#[proc_macro_attribute]
pub fn export_fn(_attr: TokenStream, item: TokenStream) -> TokenStream {
let func = parse_macro_input!(item as ItemFn);
let fn_name = &func.sig.ident;
let wrapper_fn_name = Ident::new(&format!("rhai_wrapper_{}", fn_name), fn_name.span());
let original_fn_generics = &func.sig.generics;
let fn_inputs = &func.sig.inputs;
// num_expected_args has been removed as Rhai handles arity with typed arguments
let mut arg_conversions: Vec<TokenStream2> = Vec::new(); // For let converted_arg_0 = ...
let mut original_fn_call_args: Vec<TokenStream2> = Vec::new(); // For fn_name(converted_arg_0, ...)
let mut wrapper_args_dynamic_defs: Vec<TokenStream2> = Vec::new(); // Stores definitions like `wrapper_arg_0: ::rhai::Dynamic`
for (i, fn_arg) in fn_inputs.iter().enumerate() {
if let syn::FnArg::Typed(pat_type) = fn_arg {
let arg_ident_original_pat = match *pat_type.pat.clone() {
syn::Pat::Ident(pat_ident) => pat_ident.ident,
_ => panic!("#[export_fn] only supports simple identifiers as argument patterns, not complex patterns."),
};
let arg_ident_original_pat_str = arg_ident_original_pat.to_string();
let wrapper_arg_name_ident = Ident::new(&format!("wrapper_arg_{}", i), pat_type.pat.span());
let wrapper_arg_name_str = wrapper_arg_name_ident.to_string();
// Populate definitions for the wrapper function signature
wrapper_args_dynamic_defs.push(quote! { #wrapper_arg_name_ident: ::rhai::Dynamic });
// Generate a unique ident for the converted argument variable
let converted_rhai_arg_ident = Ident::new(&format!("converted_rhai_arg_{}", i), pat_type.pat.span());
let arg_rust_type = &*pat_type.ty;
let arg_type_str = quote!(#arg_rust_type).to_string().replace(' ', "");
if let Type::Reference(ref type_ref) = *pat_type.ty {
let is_mutable = type_ref.mutability.is_some();
let referent_type = &*type_ref.elem;
let lock_guard_ident = Ident::new(&format!("lock_guard_{}", i), pat_type.pat.span());
let conversion_logic = if is_mutable {
quote! {
let mut #lock_guard_ident = #wrapper_arg_name_ident.write_lock::<#referent_type>()
.ok_or_else(|| Box::new(::rhai::EvalAltResult::ErrorMismatchDataType(
format!("Argument '{}' (dynamic arg '{}') at index {} must be a mutable reference to {}. Ensure it's not already locked.",
#arg_ident_original_pat_str, #wrapper_arg_name_str, #i, stringify!(#referent_type)
),
format!("but found type {}", #wrapper_arg_name_ident.type_name()),
::rhai::Position::NONE
)))?;
let #converted_rhai_arg_ident = &mut *#lock_guard_ident;
}
} else {
quote! {
let #lock_guard_ident = #wrapper_arg_name_ident.read_lock::<#referent_type>()
.ok_or_else(|| Box::new(::rhai::EvalAltResult::ErrorMismatchDataType(
format!("Argument '{}' (dynamic arg '{}') at index {} must be an immutable reference to {}.",
#arg_ident_original_pat_str, #wrapper_arg_name_str, #i, stringify!(#referent_type)
),
format!("but found type {}", #wrapper_arg_name_ident.type_name()),
::rhai::Position::NONE
)))?;
let #converted_rhai_arg_ident = &*#lock_guard_ident;
}
};
arg_conversions.push(conversion_logic);
original_fn_call_args.push(quote! { #converted_rhai_arg_ident });
} else if arg_type_str == "&str" {
let str_lock_guard_ident = Ident::new(&format!("str_lock_guard_{}", i), pat_type.pat.span());
let conversion_logic = quote! {
let #str_lock_guard_ident = #wrapper_arg_name_ident.read_lock::<rhai::ImmutableString>()
.ok_or_else(|| Box::new(::rhai::EvalAltResult::ErrorMismatchDataType(
format!("Argument '{}' (dynamic arg '{}') at index {} must be of type &str (requires read_lock on ImmutableString)",
#arg_ident_original_pat_str, #wrapper_arg_name_str, #i
),
format!("but found type {}", #wrapper_arg_name_ident.type_name()),
::rhai::Position::NONE
)))?;
let #converted_rhai_arg_ident = #str_lock_guard_ident.as_str();
};
arg_conversions.push(conversion_logic);
original_fn_call_args.push(quote! { #converted_rhai_arg_ident });
} else if arg_type_str == "INT" || arg_type_str == "i64" {
let conversion_logic = quote! {
let int_option: Option<::rhai::INT> = #wrapper_arg_name_ident.as_int();
let #converted_rhai_arg_ident = int_option
.ok_or_else(|| Box::new(::rhai::EvalAltResult::ErrorMismatchDataType(
format!("Argument '{}' (dynamic arg '{}') at index {} must be of type i64 (INT)",
#arg_ident_original_pat_str, #wrapper_arg_name_str, #i
),
format!("but found type {}", #wrapper_arg_name_ident.type_name()),
::rhai::Position::NONE
)))?;
};
arg_conversions.push(conversion_logic);
original_fn_call_args.push(quote! { #converted_rhai_arg_ident });
} else if arg_type_str == "FLOAT" || arg_type_str == "f64" {
let conversion_logic = quote! {
let float_option: Option<::rhai::FLOAT> = #wrapper_arg_name_ident.as_float();
let #converted_rhai_arg_ident = float_option
.ok_or_else(|| Box::new(::rhai::EvalAltResult::ErrorMismatchDataType(
format!("Argument '{}' (dynamic arg '{}') at index {} must be of type f64 (FLOAT)",
#arg_ident_original_pat_str, #wrapper_arg_name_str, #i
),
format!("but found type {}", #wrapper_arg_name_ident.type_name()),
::rhai::Position::NONE
)))?;
};
arg_conversions.push(conversion_logic);
original_fn_call_args.push(quote! { #converted_rhai_arg_ident });
} else if arg_type_str == "bool" {
let conversion_logic = quote! {
let bool_option: Option<bool> = #wrapper_arg_name_ident.as_bool();
let #converted_rhai_arg_ident = bool_option
.ok_or_else(|| Box::new(::rhai::EvalAltResult::ErrorMismatchDataType(
format!("Argument '{}' (dynamic arg '{}') at index {} must be of type bool",
#arg_ident_original_pat_str, #wrapper_arg_name_str, #i
),
format!("but found type {}", #wrapper_arg_name_ident.type_name()),
::rhai::Position::NONE
)))?;
};
arg_conversions.push(conversion_logic);
original_fn_call_args.push(quote! { #converted_rhai_arg_ident });
} else if arg_type_str == "String" {
let conversion_logic = quote! {
let #converted_rhai_arg_ident = #wrapper_arg_name_ident.clone().into_string()
.map_err(|_| Box::new(::rhai::EvalAltResult::ErrorMismatchDataType(
format!("Argument '{}' (dynamic arg '{}') at index {} must be of type String",
#arg_ident_original_pat_str, #wrapper_arg_name_str, #i
),
format!("but found type {}", #wrapper_arg_name_ident.type_name()),
::rhai::Position::NONE
)))?;
};
arg_conversions.push(conversion_logic);
original_fn_call_args.push(quote! { #converted_rhai_arg_ident });
} else {
let owned_type_path = &*pat_type.ty; // No deref needed here, quote! can handle &Type
let arg_type_for_conversion_code = quote!{#owned_type_path};
let conversion_logic = quote! {
let dyn_val_for_cast = #wrapper_arg_name_ident.clone(); // Clone for try_cast
let actual_type_name = #wrapper_arg_name_ident.type_name();
let #converted_rhai_arg_ident:#arg_type_for_conversion_code = match <#arg_type_for_conversion_code as std::convert::TryFrom<::rhai::Dynamic>>::try_from(dyn_val_for_cast) {
Ok(val) => val,
Err(_e) => {
let cast_option: Option<#arg_type_for_conversion_code> = #wrapper_arg_name_ident.clone().try_cast::<#arg_type_for_conversion_code>();
cast_option
.ok_or_else(|| Box::new(::rhai::EvalAltResult::ErrorMismatchDataType(
format!("Argument '{}' (dynamic arg '{}') at index {} must be convertible to type {}. TryFrom and try_cast failed.",
#arg_ident_original_pat_str, #wrapper_arg_name_str, #i, stringify!(#owned_type_path)
),
format!("but found type {}", actual_type_name),
::rhai::Position::NONE
)))?
}
};
};
arg_conversions.push(conversion_logic);
original_fn_call_args.push(quote! { #converted_rhai_arg_ident });
}
} else {
// Handle receivers like self, &self, &mut self if necessary
panic!("#[export_fn] does not support methods with 'self' receivers. Apply to static or free functions.");
}
}
let return_type = match &func.sig.output {
syn::ReturnType::Default => quote!( () ),
syn::ReturnType::Type(_, ty) => quote!( #ty ),
};
// Determine the return type for the wrapper function's Result
// If original is (), wrapper returns Dynamic (representing unit). Otherwise, original return type.
let return_type_for_wrapper = if quote!(#return_type).to_string().replace(" ", "") == "()" {
quote!(::rhai::Dynamic)
} else {
quote!(#return_type)
};
// How to convert the result of the original function call into the wrapper's Ok Result.
let return_conversion = if let syn::ReturnType::Type(_, _ty) = &func.sig.output {
// If original function returns non-(), result is already #return_type_for_wrapper (which is #return_type)
// So, it can be directly used.
quote! { Ok(result) }
} else {
// If original function returns (), result is (), convert to Dynamic::UNIT for Rhai
quote! { Ok(().into()) }
};
let (impl_generics, _ty_generics, where_clause) = original_fn_generics.split_for_impl();
let expanded = quote! {
#func // Re-emit the original function
// _cx is NativeCallContext. The arguments are now individual ::rhai::Dynamic types.
pub fn #wrapper_fn_name #impl_generics (_cx: ::rhai::NativeCallContext, #(#wrapper_args_dynamic_defs),*) -> Result<#return_type_for_wrapper, Box<::rhai::EvalAltResult>> #where_clause {
// Arity check is implicitly handled by Rhai's function registration for typed arguments.
// If the script calls with wrong number of args, Rhai won't find a matching function signature.
#(#arg_conversions)*
// The following allow might be needed if the original function's result is directly usable
// without explicit conversion to Dynamic, but the wrapper expects Dynamic for unit returns.
#[allow(clippy::useless_conversion)]
let result = #fn_name(#(#original_fn_call_args),*);
#return_conversion
}
};
// eprintln!("Generated code for {}:\n{}", stringify!(#fn_name), expanded.to_string());
proc_macro::TokenStream::from(expanded)
}
#[proc_macro_derive(FromRhaiMap, attributes(rhai_map_field))]
pub fn derive_from_rhai_map(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// Helper function to check if a type is Option<T> and extract T
// Returns (is_option, inner_type_quote_option)
fn get_option_inner_type(ty: &Type) -> (bool, Option<&Type>) {
if let Type::Path(type_path) = ty {
if type_path.path.segments.len() == 1 && type_path.path.segments.first().unwrap().ident == "Option" {
if let PathArguments::AngleBracketed(params) = &type_path.path.segments.first().unwrap().arguments {
if params.args.len() == 1 {
if let GenericArgument::Type(inner_ty) = params.args.first().unwrap() {
return (true, Some(inner_ty));
}
}
}
}
}
(false, None)
}
// Helper function to check if a type is Vec<T> and extract T
// Returns (is_vec, inner_type_quote_option)
fn get_vec_inner_type(ty: &Type) -> (bool, Option<&Type>) {
if let Type::Path(type_path) = ty {
if type_path.path.segments.len() == 1 && type_path.path.segments.first().unwrap().ident == "Vec" {
if let PathArguments::AngleBracketed(params) = &type_path.path.segments.first().unwrap().arguments {
if params.args.len() == 1 {
if let GenericArgument::Type(inner_ty) = params.args.first().unwrap() {
return (true, Some(inner_ty));
}
}
}
}
}
(false, None)
}
// Helper to get the string representation of a type, simplified (e.g., "MyStruct", "String", "i64")
fn get_simple_type_str(ty: &Type) -> String {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident.to_string();
}
}
quote!(#ty).to_string().replace(' ', "").replace("::", "_") // fallback, might need refinement
}
// Helper to check if a type is a primitive type based on its string representation
fn is_primitive_type_str(simple_type_str: &str) -> bool {
["String", "INT", "i64", "FLOAT", "f64", "bool"].contains(&simple_type_str)
}
let fields_data = match &input.data {
Data::Struct(syn::DataStruct { fields: syn::Fields::Named(fields), .. }) => &fields.named,
_ => panic!("FromRhaiMapDerive only supports structs with named fields"),
};
let mut field_value_declarations = Vec::new();
let mut struct_field_assignments = Vec::new();
for field in fields_data.iter() {
let field_name_ident = field.ident.as_ref().unwrap(); // Changed variable name for clarity
let field_name_str = field_name_ident.to_string();
let field_name_str_lit = LitStr::new(&field_name_str, field_name_ident.span()); // Corrected: Use LitStr for string literals
let field_ty = &field.ty;
let field_value_ident = format_ident!("__field_val_{}", field_name_str);
let (is_option, option_inner_ty_opt) = get_option_inner_type(field_ty);
let type_for_vec_check = if is_option { option_inner_ty_opt.unwrap() } else { field_ty };
let (is_vec, vec_inner_ty_opt) = get_vec_inner_type(type_for_vec_check);
let assignment_code = if is_option {
let option_inner_ty = option_inner_ty_opt.expect("Option inner type not found");
if is_vec { // Option<Vec<T>>
let vec_element_ty = vec_inner_ty_opt.expect("Option<Vec<T>> inner T not found");
let vec_element_ty_str = get_simple_type_str(vec_element_ty);
let element_conversion_logic = if is_primitive_type_str(&vec_element_ty_str) {
quote! {
let el_for_cast = el.clone();
let actual_type_name = el.type_name();
el_for_cast.try_cast::<#vec_element_ty>().ok_or_else(|| format!(
"Element in Option<Vec> for key '{}' is not of type {}, but of type {}",
#field_name_str_lit, stringify!(#vec_element_ty), actual_type_name
))
}
} else {
quote! {
let map_val = el.try_cast::<::rhai::Map>().ok_or_else(|| format!("Expected map for element in Option<Vec> for key '{}', got {}", #field_name_str_lit, el.type_name()))?;
#vec_element_ty::from_rhai_map(map_val)
}
};
quote! {
map.get(#field_name_str_lit).cloned().map(|dyn_val_array| {
dyn_val_array.into_array().map_err(|e| e.to_string())?.into_iter().map(|el| {
#element_conversion_logic
}).collect::<Result<Vec<#vec_element_ty>, String>>()
}).transpose()?
}
} else { // Option<T>
let option_inner_ty_str = get_simple_type_str(option_inner_ty);
let conversion_logic = if is_primitive_type_str(&option_inner_ty_str) {
quote! {
let val_for_cast = val.clone();
let actual_type_name = val.type_name();
val_for_cast.try_cast::<#option_inner_ty>().ok_or_else(|| format!(
"Value in Option for key '{}' is not of type {}, but of type {}",
#field_name_str_lit, stringify!(#option_inner_ty), actual_type_name
))
}
} else {
quote! {
let map_val = val.try_cast::<::rhai::Map>().ok_or_else(|| format!("Expected map for Option key '{}', got {}", #field_name_str_lit, val.type_name()))?;
#option_inner_ty::from_rhai_map(map_val)
}
};
quote! {
map.get(#field_name_str_lit).cloned().map(|val| {
#conversion_logic
}).transpose()?
}
}
} else if is_vec { // Vec<T>
let vec_inner_ty = vec_inner_ty_opt.expect("Vec inner type not found");
let vec_inner_ty_str = get_simple_type_str(vec_inner_ty);
let element_conversion_logic = if is_primitive_type_str(&vec_inner_ty_str) {
quote! {
let el_for_cast = el.clone();
let actual_type_name = el.type_name();
el_for_cast.try_cast::<#vec_inner_ty>().ok_or_else(|| format!(
"Element in Vec for key '{}' is not of type {}, but of type {}",
#field_name_str_lit, stringify!(#vec_inner_ty), actual_type_name
))
}
} else {
quote! {
let map_val = el.try_cast::<::rhai::Map>().ok_or_else(|| format!("Expected map for element in Vec for key '{}', got {}", #field_name_str_lit, el.type_name()))?;
#vec_inner_ty::from_rhai_map(map_val)
}
};
quote! {{
let dyn_val_array = map.get(#field_name_str_lit).cloned()
.ok_or_else(|| format!("Key '{}' not found for Vec", #field_name_str_lit))?
.into_array().map_err(|e| e.to_string())?;
dyn_val_array.into_iter().map(|el| {
#element_conversion_logic
}).collect::<Result<Vec<#vec_inner_ty>, String>>()?
}}
} else { // Direct field T
let field_ty_str = get_simple_type_str(field_ty);
let conversion_logic = if is_primitive_type_str(&field_ty_str) {
quote! {
let dyn_val_for_cast = dyn_val_cloned.clone();
let actual_type_name = dyn_val_cloned.type_name();
dyn_val_for_cast.try_cast::<#field_ty>().ok_or_else(|| format!(
"Value in map for key '{}' is not of type {}, but of type {}",
#field_name_str_lit, stringify!(#field_ty), actual_type_name
))
}
} else {
quote! {
let map_val = dyn_val_cloned.try_cast::<::rhai::Map>().ok_or_else(|| format!("Expected map for key '{}', got {}", #field_name_str_lit, dyn_val_cloned.type_name()))?;
#field_ty::from_rhai_map(map_val)
}
};
quote! {{
let dyn_val_cloned = map.get(#field_name_str_lit).cloned()
.ok_or_else(|| format!("Key '{}' not found", #field_name_str_lit))?;
#conversion_logic?
}}
};
field_value_declarations.push(quote! { let #field_value_ident = #assignment_code; });
struct_field_assignments.push(quote_spanned!(field_name_ident.span()=> #field_name_ident: #field_value_ident));
}
let gen = quote! {
impl #name {
pub fn from_rhai_map(map: ::rhai::Map) -> Result<Self, String> {
#(#field_value_declarations)*
Ok(Self {
#(#struct_field_assignments),*
})
}
}
};
proc_macro::TokenStream::from(gen)
}
#[proc_macro_derive(ToRhaiMap)]
pub fn derive_to_rhai_map(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = syn::parse_macro_input!(input as syn::DeriveInput);
let name = &ast.ident;
let mut field_insertions = Vec::new();
if let syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(fields), .. }) = ast.data {
for field in fields.named {
let field_name = field.ident.as_ref().unwrap();
let field_name_str = field_name.to_string();
let ty = &field.ty;
let mut _is_vec_of_custom_struct = false;
let mut is_direct_custom_struct = false;
let _field_type_name_for_logic = quote!(#ty).to_string().replace(" ", "");
if let syn::Type::Path(type_path) = ty {
if type_path.path.segments.len() == 1 && type_path.path.segments.first().unwrap().ident == "Vec" {
if let syn::PathArguments::AngleBracketed(angle_bracketed_args) = &type_path.path.segments.first().unwrap().arguments {
if let Some(syn::GenericArgument::Type(_inner_ty_syn @ syn::Type::Path(inner_type_path))) = angle_bracketed_args.args.first() {
if let Some(inner_segment) = inner_type_path.path.segments.last() {
let inner_type_str = inner_segment.ident.to_string();
if !["String", "INT", "i64", "FLOAT", "f64", "bool"].contains(&inner_type_str.as_str()) {
_is_vec_of_custom_struct = true;
}
}
}
}
} else if !["String", "INT", "i64", "FLOAT", "f64", "bool"].contains(&type_path.path.segments.first().unwrap().ident.to_string().as_str()) {
// It's not a Vec, and not a primitive, so assume it's a direct custom struct
is_direct_custom_struct = true;
}
}
if _is_vec_of_custom_struct {
field_insertions.push(quote! {
{
let rhai_array: rhai::Array = self.#field_name.iter().map(|item| {
// item is &CustomStruct, to_rhai_map() takes &self
rhai::Dynamic::from(item.to_rhai_map())
}).collect();
map.insert(#field_name_str.into(), rhai::Dynamic::from(rhai_array));
}
});
// FIX: Logic for direct custom struct field
} else if is_direct_custom_struct {
field_insertions.push(quote! {
map.insert(#field_name_str.into(), rhai::Dynamic::from(self.#field_name.to_rhai_map()));
});
} else {
// This handles Vec<Primitive> and direct Primitives
let type_str = quote!(#ty).to_string().replace(" ", "");
if type_str.starts_with("Vec<") {
field_insertions.push(quote! {
{
let rhai_array: rhai::Array = self.#field_name.iter().map(|item| {
// item is &Primitive, clone it for .into()
item.clone().into()
}).collect();
map.insert(#field_name_str.into(), rhai::Dynamic::from(rhai_array));
}
});
} else {
// Direct primitive field
field_insertions.push(quote! {
map.insert(#field_name_str.into(), self.#field_name.clone().into());
});
}
}
}
}
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let expanded = quote! {
impl #impl_generics ToRhaiMap for #name #ty_generics #where_clause {
fn to_rhai_map(&self) -> rhai::Map {
let mut map = rhai::Map::new();
#(#field_insertions)*
map
}
}
};
proc_macro::TokenStream::from(expanded)
}
// Rest of the code remains the same

BIN
_archive/listen/.DS_Store vendored Normal file

Binary file not shown.

2620
_archive/listen/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
[dependencies]
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }
rhai = { version = "1.15.0", features = ["sync"] }
rhai_system = { path = "../rhai_system" }
bytes = "1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tera = "1.0"
once_cell = "1"
rhai_tera = { path = "../rhai_tera" }
calendar = { path = "../components/calendar" }
[[example]]
name = "send_rhai_script"
path = "examples/send_rhai_script.rs"
required-features = []
[dev-dependencies] # Examples often use dev-dependencies, but reqwest is more like a direct dep for the example's purpose.
reqwest = { version = "0.11", features = ["json", "rustls-tls"] } # Updated for async example client
# tokio is already a main dependency, so the example can use it.

View File

@ -0,0 +1,38 @@
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let server_url = "http://127.0.0.1:8000";
// Simple Rhai script that creates a map
let rhai_script = r#"
let message = "Hello from Rhai script!";
let number = 40 + 2;
#{
greeting: message,
calculation_result: number
}
"#;
println!("Sending Rhai script to server:\n{}", rhai_script);
let response = client
.post(server_url)
.header("Content-Type", "text/plain") // Or application/rhai, but plain text is fine
.body(rhai_script.to_string())
.send()
.await?;
let status = response.status();
let response_text = response.text().await?;
println!("\nServer responded with status: {}", status);
println!("Response body:\n{}", response_text);
if !status.is_success() {
return Err(format!("Server returned error: {} - {}", status, response_text).into());
}
Ok(())
}

View File

@ -0,0 +1,3 @@
// This script is used to initialize the Rhai system.
// It can be left empty or used to define globally available functions/variables.
// print("init.rhai loaded!");

111
_archive/listen/src/main.rs Normal file
View File

@ -0,0 +1,111 @@
use hyper::{
service::{make_service_fn, service_fn},
Body,
Request,
Response,
Server,
StatusCode,
Method,
};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use std::path::Path;
use rhai_system::{create_hot_reloadable_system, System};
use rhai::Dynamic;
use hyper::body::to_bytes;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = SocketAddr::from(([127, 0, 0, 1], 8000));
let init_script_path = Path::new("scripts/init.rhai");
let rhai_sys = match create_hot_reloadable_system(&[init_script_path], Some(0)) {
Ok(system) => Arc::new(system),
Err(e) => {
eprintln!("Failed to create Rhai system: {}", e);
return Err(e.into());
}
};
let rhai_sys_clone = Arc::clone(&rhai_sys);
let make_svc = make_service_fn(move |_conn| {
let rhai_system_for_service = Arc::clone(&rhai_sys_clone);
async {
Ok::<_, Infallible>(service_fn(move |req: Request<Body>| {
let rhai_system_for_request = Arc::clone(&rhai_system_for_service);
handle_request(rhai_system_for_request, req)
}))
}
});
println!("Rhai script server running at http://{}", addr);
println!("Send POST requests with Rhai script in the body to execute.");
Server::bind(&addr).serve(make_svc).await?;
Ok(())
}
async fn handle_request(rhai_sys: Arc<System>, req: Request<Body>) -> Result<Response<Body>, Infallible> {
match *req.method() {
Method::POST => {
let body_bytes = match to_bytes(req.into_body()).await {
Ok(bytes) => bytes,
Err(e) => {
eprintln!("Error reading request body: {}", e);
return Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Error reading request body: {}", e)))
.unwrap());
}
};
let script_string = match String::from_utf8(body_bytes.to_vec()) {
Ok(s) => s,
Err(e) => {
eprintln!("Request body is not valid UTF-8: {}", e);
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::from(format!("Request body is not valid UTF-8: {}", e)))
.unwrap());
}
};
if script_string.trim().is_empty() {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::from("Rhai script body cannot be empty."))
.unwrap());
}
println!("Executing Rhai script: \n{}", script_string);
match rhai_sys.engine.eval::<Dynamic>(&script_string) {
Ok(result) => {
let response_body = format!("{}", result);
println!("Script result: {}", response_body);
Ok(Response::new(Body::from(response_body)))
}
Err(e) => {
let error_msg = format!("Rhai script execution error: {}", e);
eprintln!("{}", error_msg);
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(error_msg))
.unwrap())
}
}
}
_ => {
Ok(Response::builder()
.status(StatusCode::METHOD_NOT_ALLOWED)
.header("Allow", "POST")
.body(Body::from("Method Not Allowed. Please use POST with Rhai script in the body."))
.unwrap())
}
}
}

282
_archive/rhai_autobind_macros/Cargo.lock generated Normal file
View File

@ -0,0 +1,282 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b"
dependencies = [
"cfg-if",
"const-random",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[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",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[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.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
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 = "rhai"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1acc213aa1e33611a4b20b31b738af675113e1c9944d6e3d79e3e7318ce0205"
dependencies = [
"ahash",
"bitflags",
"instant",
"num-traits",
"once_cell",
"rhai_codegen",
"smallvec",
"smartstring",
"thin-vec",
]
[[package]]
name = "rhai_autobind_macros"
version = "0.1.0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rhai",
"serde",
"syn",
]
[[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 = "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 = "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 = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
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 = "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 = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[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,17 @@
[package]
name = "rhai_autobind_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full", "extra-traits"] }
quote = "1.0"
proc-macro2 = "1.0"
heck = "0.4"
[dev-dependencies]
rhai = "1.18.0"
serde = { version = "1.0", features = ["derive"] }

View File

@ -0,0 +1,33 @@
// calculator.rhai
// This script demonstrates using the Calculator struct from Rust in Rhai
// Create a new calculator
let calc = new_calculator();
println("Created a new calculator with value: " + calc.value);
// Perform some calculations
calc.add(5);
println("After adding 5: " + calc.value);
calc.multiply(2);
println("After multiplying by 2: " + calc.value);
calc.subtract(3);
println("After subtracting 3: " + calc.value);
calc.divide(2);
println("After dividing by 2: " + calc.value);
// Set value directly
calc.value = 100;
println("After setting value to 100: " + calc.value);
// Clear the calculator
calc.clear();
println("After clearing: " + calc.value);
// Chain operations
let result = calc.add(10).multiply(2).subtract(5).divide(3);
println("Result of chained operations: " + result);
println("Final calculator value: " + calc.value);

View File

@ -0,0 +1,97 @@
use rhai::{Engine, EvalAltResult, CustomType, TypeBuilder};
use rhai_autobind_macros::rhai_model_export;
use serde::{Deserialize, Serialize};
use std::path::Path;
// Dummy DB type for the example, as rhai_model_export requires a db_type
struct DummyDb;
impl DummyDb {
fn new() -> Self { DummyDb }
}
// Define a simple Calculator struct with the rhai_autobind attribute
#[derive(Debug, Clone, Serialize, Deserialize, rhai::CustomType)]
#[rhai_model_export(db_type = "DummyDb")] // Provide the required db_type
pub struct Calculator {
pub value: f64,
}
impl Calculator {
// Constructor
pub fn new() -> Self {
Calculator { value: 0.0 }
}
// Add method
pub fn add(&mut self, x: f64) -> f64 {
self.value += x;
self.value
}
// Subtract method
pub fn subtract(&mut self, x: f64) -> f64 {
self.value -= x;
self.value
}
// Multiply method
pub fn multiply(&mut self, x: f64) -> f64 {
self.value *= x;
self.value
}
// Divide method
pub fn divide(&mut self, x: f64) -> f64 {
if x == 0.0 {
println!("Error: Division by zero!");
return self.value;
}
self.value /= x;
self.value
}
// Clear method
pub fn clear(&mut self) -> f64 {
self.value = 0.0;
self.value
}
// Get value method
pub fn get_value(&self) -> f64 {
self.value
}
}
fn main() -> Result<(), Box<EvalAltResult>> {
println!("Rhai Calculator Example");
println!("======================");
// Create a new Rhai engine
let mut engine = Engine::new();
// Register the Calculator type with the engine using the generated function
let dummy_db = DummyDb::new(); // Create an instance of the dummy DB
Calculator::register_rhai_bindings_for_calculator(&mut engine, dummy_db);
// Register print function for output
engine.register_fn("println", |s: &str| println!("{}", s));
// Create a calculator instance to demonstrate it works
let calc = Calculator::new();
println!("Initial value: {}", calc.value);
// Load and run the Rhai script
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join("rhai_autobind_example")
.join("calculator.rhai");
println!("Loading Rhai script from: {}", script_path.display());
match engine.eval_file::<()>(script_path) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Error executing script: {}\nAt: {:?}", e, e.position()),
}
Ok(())
}

View File

@ -0,0 +1,152 @@
use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::{parse_macro_input, Ident, Fields, Visibility, ItemStruct, LitStr, ExprArray, Expr, Lit};
use syn::spanned::Spanned; // Add the Spanned trait for span() method
use heck::ToSnakeCase; // For converting struct name to snake_case for function names
#[derive(Debug)]
struct MacroArgs {
db_type: syn::Type,
collection_name: Option<String>,
methods: Vec<String>, // To store method names to be registered
}
impl syn::parse::Parse for MacroArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut db_type_str: Option<LitStr> = None;
let mut collection_name_str: Option<LitStr> = None;
let mut methods_array: Option<ExprArray> = None;
while !input.is_empty() {
let ident: Ident = input.parse()?;
let _eq_token: syn::token::Eq = input.parse()?;
if ident == "db_type" {
db_type_str = Some(input.parse()?);
} else if ident == "collection_name" {
collection_name_str = Some(input.parse()?);
} else if ident == "methods" {
methods_array = Some(input.parse()?);
} else {
return Err(syn::Error::new(ident.span(), "Unknown attribute argument"));
}
if !input.is_empty() {
let _comma: syn::token::Comma = input.parse()?;
}
}
let db_type_str = db_type_str.ok_or_else(|| syn::Error::new(input.span(), "Missing required attribute `db_type`"))?;
let db_type: syn::Type = syn::parse_str(&db_type_str.value())?;
let collection_name = collection_name_str.map(|s| s.value());
let mut methods = Vec::new();
if let Some(array_expr) = methods_array {
for expr in array_expr.elems {
if let Expr::Lit(expr_lit) = expr {
if let Lit::Str(lit_str) = expr_lit.lit {
methods.push(lit_str.value());
} else {
return Err(syn::Error::new(expr_lit.lit.span(), "Method name must be a string literal"));
}
} else {
return Err(syn::Error::new(expr.span(), "Method name must be a string literal"));
}
}
}
Ok(MacroArgs { db_type, collection_name, methods })
}
}
#[proc_macro_attribute]
pub fn rhai_model_export(attr: TokenStream, item: TokenStream) -> TokenStream {
let macro_args = parse_macro_input!(attr as MacroArgs);
let input_struct = parse_macro_input!(item as ItemStruct);
let struct_name = &input_struct.ident;
let struct_name_str = struct_name.to_string();
let struct_name_snake_case = struct_name_str.to_snake_case(); // Use heck for snake_case
let db_type = macro_args.db_type;
let collection_name = macro_args.collection_name.unwrap_or_else(|| {
// Basic pluralization
if struct_name_snake_case.ends_with('y') {
format!("{}ies", &struct_name_snake_case[..struct_name_snake_case.len()-1])
} else if struct_name_snake_case.ends_with('s') {
format!("{}es", struct_name_snake_case)
} else {
format!("{}s", struct_name_snake_case)
}
});
let generated_registration_fn_name = format_ident!("register_rhai_bindings_for_{}", struct_name_snake_case);
let constructor_fn_name = format!("new_{}", struct_name_snake_case);
let mut field_registrations = Vec::new();
if let Fields::Named(fields) = &input_struct.fields {
for field in fields.named.iter() {
if let (Some(field_name), Visibility::Public(_)) = (&field.ident, &field.vis) {
let field_name_str = field_name.to_string();
let field_type = &field.ty;
// Register getter
field_registrations.push(quote! {
engine.register_get(#field_name_str, |s: &mut #struct_name| s.#field_name.clone());
});
// Register setter
field_registrations.push(quote! {
engine.register_set(#field_name_str, |s: &mut #struct_name, val: #field_type| s.#field_name = val);
});
}
}
}
let mut method_registrations = Vec::new();
for method_name_str in &macro_args.methods {
let method_ident = format_ident!("{}", method_name_str);
let rhai_method_name = method_name_str.clone(); // Rhai function name can be the same as Rust method name
method_registrations.push(quote! {
engine.register_fn(#rhai_method_name, #struct_name::#method_ident);
});
}
// Create a formatted string of method names for printing
let methods_str = format!("{:?}", macro_args.methods);
let output = quote! {
#input_struct // Re-emit the original struct definition
impl #struct_name {
pub fn #generated_registration_fn_name(
engine: &mut rhai::Engine,
db: #db_type // db is now passed but not used by default for non-DB methods
) {
// For rhai_wrapper if/when DB operations are added
// use rhai_wrapper::*;
engine.build_type::<#struct_name>();
// Register constructor (e.g., new_calculator for Calculator::new())
engine.register_fn(#constructor_fn_name, #struct_name::new);
// Register field getters and setters
#(#field_registrations)*
// Register specified instance methods
#(#method_registrations)*
// Placeholder for DB function registrations
// e.g., get_by_id, get_all, save, delete
println!("Registered {} with Rhai (collection: '{}'). Constructor: '{}'. Methods: {}. Fields accessible.",
#struct_name_str,
#collection_name,
#constructor_fn_name,
#methods_str
);
}
}
};
TokenStream::from(output)
}

BIN
_archive/talk/.DS_Store vendored Normal file

Binary file not shown.

2146
_archive/talk/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
_archive/talk/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "talk"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.4.0"
actix-files = "0.6.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rhai = "1.18.0"
heromodels = { path = "../../db/heromodels" }
rhai_wrapper = { path = "../rhai_wrapper" }
chrono = { version = "0.4", features = ["serde"] }

331
_archive/talk/src/main.rs Normal file
View File

@ -0,0 +1,331 @@
use actix_files::Files;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use heromodels::db::hero::OurDB;
use heromodels::models::calendar::Calendar;
use heromodels::models::governance::{Proposal, Ballot, VoteOption, ProposalStatus, VoteEventStatus};
use rhai::Engine;
use rhai_wrapper::wrap_vec_return;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use chrono::{Utc, Duration};
#[derive(Deserialize)]
struct ScriptRequest {
script: String,
model_type: String,
}
#[derive(Serialize)]
struct ScriptResponse {
output: String,
success: bool,
}
// Function to set up the calendar model in the Rhai engine
fn setup_calendar_engine(engine: &mut Engine, db: Arc<OurDB>) {
// Register the Calendar type with Rhai
Calendar::register_rhai_bindings_for_calendar(engine, db.clone());
// Register a function to get the database instance
engine.register_fn("get_db", move || db.clone());
// Register a calendar builder function
engine.register_fn("calendar__builder", |id: i64| {
Calendar::new(id as u32)
});
// Register setter methods for Calendar properties
engine.register_fn("set_description", |calendar: &mut Calendar, desc: String| {
calendar.description = Some(desc);
});
// Register getter methods for Calendar properties
engine.register_fn("get_description", |calendar: Calendar| -> String {
calendar.description.clone().unwrap_or_default()
});
// Register getter for base_data.id
engine.register_fn("get_id", |calendar: Calendar| -> i64 {
calendar.base_data.id as i64
});
// Register additional functions needed by the script
engine.register_fn("set_calendar", |_db: Arc<OurDB>, _calendar: Calendar| {
// In a real implementation, this would save the calendar to the database
println!("Calendar saved: {}", _calendar.name);
});
engine.register_fn("get_calendar_by_id", |_db: Arc<OurDB>, id: i64| -> Calendar {
// In a real implementation, this would retrieve the calendar from the database
Calendar::new(id as u32)
});
// Register a function to check if a calendar exists
engine.register_fn("calendar_exists", |_db: Arc<OurDB>, id: i64| -> bool {
// In a real implementation, this would check if the calendar exists in the database
id == 1 || id == 2
});
// Define the function separately to use with the wrap_vec_return macro
fn get_all_calendars(_db: Arc<OurDB>) -> Vec<Calendar> {
// In a real implementation, this would retrieve all calendars from the database
vec![Calendar::new(1), Calendar::new(2)]
}
// Register the function with the wrap_vec_return macro
engine.register_fn("get_all_calendars", wrap_vec_return!(get_all_calendars, Arc<OurDB> => Calendar));
engine.register_fn("delete_calendar_by_id", |_db: Arc<OurDB>, _id: i64| {
// In a real implementation, this would delete the calendar from the database
println!("Calendar deleted with ID: {}", _id);
});
}
// Function to set up the governance model in the Rhai engine
fn setup_governance_engine(engine: &mut Engine, db: Arc<OurDB>) {
// Register the Proposal type with Rhai
Proposal::register_rhai_bindings_for_proposal(engine, db.clone());
// Register the Ballot type with Rhai
Ballot::register_rhai_bindings_for_ballot(engine, db.clone());
// Register a function to get the database instance
engine.register_fn("get_db", move || db.clone());
// Register builder functions for Proposal and related types
engine.register_fn("create_proposal", |id: i64, creator_id: String, title: String, description: String| {
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
Proposal::new(id as u32, creator_id, title, description, start_date, end_date)
});
engine.register_fn("create_vote_option", |id: i64, text: String| {
VoteOption::new(id as u8, text)
});
engine.register_fn("create_ballot", |id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| {
Ballot::new(id as u32, user_id as u32, vote_option_id as u8, shares_count)
});
// Register getter and setter methods for Proposal properties
engine.register_fn("get_title", |proposal: Proposal| -> String {
proposal.title.clone()
});
engine.register_fn("get_description", |proposal: Proposal| -> String {
proposal.description.clone()
});
engine.register_fn("get_creator_id", |proposal: Proposal| -> String {
proposal.creator_id.clone()
});
engine.register_fn("get_id", |proposal: Proposal| -> i64 {
proposal.base_data.id as i64
});
engine.register_fn("get_status", |proposal: Proposal| -> String {
format!("{:?}", proposal.status)
});
engine.register_fn("get_vote_status", |proposal: Proposal| -> String {
format!("{:?}", proposal.vote_status)
});
// Register methods for proposal operations
engine.register_fn("add_option_to_proposal", |proposal: Proposal, option_id: i64, option_text: String| -> Proposal {
proposal.add_option(option_id as u8, option_text)
});
engine.register_fn("cast_vote_on_proposal", |proposal: Proposal, ballot_id: i64, user_id: i64, option_id: i64, shares: i64| -> Proposal {
proposal.cast_vote(ballot_id as u32, user_id as u32, option_id as u8, shares)
});
engine.register_fn("change_proposal_status", |proposal: Proposal, status_str: String| -> Proposal {
let new_status = match status_str.as_str() {
"Draft" => ProposalStatus::Draft,
"Active" => ProposalStatus::Active,
"Approved" => ProposalStatus::Approved,
"Rejected" => ProposalStatus::Rejected,
"Cancelled" => ProposalStatus::Cancelled,
_ => ProposalStatus::Draft,
};
proposal.change_proposal_status(new_status)
});
engine.register_fn("change_vote_event_status", |proposal: Proposal, status_str: String| -> Proposal {
let new_status = match status_str.as_str() {
"Open" => VoteEventStatus::Open,
"Closed" => VoteEventStatus::Closed,
"Cancelled" => VoteEventStatus::Cancelled,
_ => VoteEventStatus::Open,
};
proposal.change_vote_event_status(new_status)
});
// Register functions for database operations
engine.register_fn("save_proposal", |_db: Arc<OurDB>, proposal: Proposal| {
println!("Proposal saved: {}", proposal.title);
});
engine.register_fn("get_proposal_by_id", |_db: Arc<OurDB>, id: i64| -> Proposal {
// In a real implementation, this would retrieve the proposal from the database
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
Proposal::new(id as u32, "Retrieved Creator", "Retrieved Proposal", "Retrieved Description", start_date, end_date)
});
// Register a function to check if a proposal exists
engine.register_fn("proposal_exists", |_db: Arc<OurDB>, id: i64| -> bool {
// In a real implementation, this would check if the proposal exists in the database
id == 1 || id == 2
});
// Define the function for get_all_proposals
fn get_all_proposals(_db: Arc<OurDB>) -> Vec<Proposal> {
// In a real implementation, this would retrieve all proposals from the database
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
vec![
Proposal::new(1, "Creator 1", "Proposal 1", "Description 1", start_date, end_date),
Proposal::new(2, "Creator 2", "Proposal 2", "Description 2", start_date, end_date)
]
}
// Register the function with the wrap_vec_return macro
engine.register_fn("get_all_proposals", wrap_vec_return!(get_all_proposals, Arc<OurDB> => Proposal));
engine.register_fn("delete_proposal_by_id", |_db: Arc<OurDB>, _id: i64| {
// In a real implementation, this would delete the proposal from the database
println!("Proposal deleted with ID: {}", _id);
});
// Register helper functions for accessing proposal options and ballots
engine.register_fn("get_option_count", |proposal: Proposal| -> i64 {
proposal.options.len() as i64
});
engine.register_fn("get_option_at", |proposal: Proposal, index: i64| -> VoteOption {
if index >= 0 && index < proposal.options.len() as i64 {
proposal.options[index as usize].clone()
} else {
VoteOption::new(0, "Invalid Option")
}
});
engine.register_fn("get_option_text", |option: VoteOption| -> String {
option.text.clone()
});
engine.register_fn("get_option_votes", |option: VoteOption| -> i64 {
option.count
});
engine.register_fn("get_ballot_count", |proposal: Proposal| -> i64 {
proposal.ballots.len() as i64
});
engine.register_fn("get_ballot_at", |proposal: Proposal, index: i64| -> Ballot {
if index >= 0 && index < proposal.ballots.len() as i64 {
proposal.ballots[index as usize].clone()
} else {
Ballot::new(0, 0, 0, 0)
}
});
engine.register_fn("get_ballot_user_id", |ballot: Ballot| -> i64 {
ballot.user_id as i64
});
engine.register_fn("get_ballot_option_id", |ballot: Ballot| -> i64 {
ballot.vote_option_id as i64
});
engine.register_fn("get_ballot_shares", |ballot: Ballot| -> i64 {
ballot.shares_count
});
}
// Endpoint to execute Rhai scripts
async fn execute_script(req: web::Json<ScriptRequest>) -> impl Responder {
// Create a string to capture stdout
let output = Arc::new(Mutex::new(String::new()));
// Initialize Rhai engine
let mut engine = Engine::new();
// Register print function to capture output
let output_clone = output.clone();
engine.register_fn("print", move |text: String| {
if let Ok(mut output_guard) = output_clone.lock() {
output_guard.push_str(&text);
output_guard.push('\n');
}
});
// Initialize database
let db = Arc::new(OurDB::new("temp_rhai_playground_db", true).expect("Failed to create database"));
// Set up the engine based on the model type
match req.model_type.as_str() {
"calendar" => setup_calendar_engine(&mut engine, db),
"governance" => setup_governance_engine(&mut engine, db),
_ => {
return HttpResponse::BadRequest().json(ScriptResponse {
output: "Invalid model type. Supported types: 'calendar', 'governance'".to_string(),
success: false,
});
}
}
// Execute the script
match engine.eval::<()>(&req.script) {
Ok(_) => {
let output_str = output.lock().unwrap_or_else(|_| panic!("Failed to lock output")).clone();
HttpResponse::Ok().json(ScriptResponse {
output: output_str,
success: true,
})
}
Err(e) => {
HttpResponse::Ok().json(ScriptResponse {
output: format!("Script execution failed: {}", e),
success: false,
})
}
}
}
// Endpoint to get example scripts
async fn get_example_script(path: web::Path<String>) -> impl Responder {
let script_type = path.into_inner();
let script_content = match script_type.as_str() {
"calendar" => {
std::fs::read_to_string("../../db/heromodels/examples/calendar_rhai/calendar.rhai")
.unwrap_or_else(|_| "// Failed to load calendar example".to_string())
}
"governance" => {
std::fs::read_to_string("../../db/heromodels/examples/governance_rhai/governance.rhai")
.unwrap_or_else(|_| "// Failed to load governance example".to_string())
}
_ => "// Invalid example type".to_string(),
};
HttpResponse::Ok().body(script_content)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Starting Rhai Web Playground server at http://localhost:8080");
HttpServer::new(|| {
App::new()
.service(web::resource("/api/execute").route(web::post().to(execute_script)))
.service(web::resource("/api/example/{script_type}").route(web::get().to(get_example_script)))
.service(Files::new("/", "./static").index_file("index.html"))
})
.bind("127.0.0.1:8080")?
.run()
.await
}

View File

@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rhai Script Playground</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.2/lib/codemirror.css">
<link rel="stylesheet" href="style.css">
<style>
.btn-success { background-color: #20c997 !important; border-color: #20c997 !important; color: white !important; }
.btn-danger { background-color: #f87171 !important; border-color: #f87171 !important; color: white !important; }
.btn-warning { background-color: #ffc107 !important; border-color: #ffc107 !important; color: black !important; }
/* Toast styling */
.toast-container {
z-index: 1050;
}
.toast {
min-width: 300px;
opacity: 1 !important;
}
</style>
</head>
<body>
<div class="container-fluid">
<header class="py-3 mb-3 border-bottom">
<h1 class="text-center">Rhai Script Talk</h1>
</header>
<div class="row">
<div class="col-md-6 pe-md-2">
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Script Input</h5>
<div>
<select id="modelSelect" class="form-select form-select-sm">
<option value="calendar">Calendar Model</option>
<option value="governance">Governance Model</option>
</select>
</div>
</div>
<div class="card-body p-0">
<textarea id="scriptInput"></textarea>
</div>
<div class="card-footer d-flex justify-content-between">
<button id="loadExampleBtn" class="btn btn-outline-secondary">Load Example</button>
<button id="runScriptBtn" class="btn btn-primary">Run Script</button>
</div>
</div>
</div>
<div class="col-md-6 ps-md-2">
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Output</h5>
<div class="d-flex align-items-center">
<div class="input-group me-2" style="width: 250px;">
<select id="listenerSelect" class="form-select form-select-sm">
<option value="">Custom URL...</option>
<option value="http://localhost:3000/governance">Governance Listener</option>
<option value="http://localhost:3000/finance">Finance Listener</option>
<option value="http://localhost:3000/calendar">Calendar Listener</option>
</select>
<input type="text" id="listenerUrl" class="form-control form-control-sm" placeholder="Listener URL" style="display: none;">
</div>
<button id="connectListenerBtn" class="btn btn-sm btn-outline-secondary d-flex align-items-center">
<span id="statusCircle" class="me-2" style="width: 8px; height: 8px; border-radius: 50%; background-color: #6c757d; display: inline-block;"></span>
<span id="connectBtnText">Connect</span>
</button>
</div>
</div>
<div class="card-body p-0">
<pre id="outputArea" class="p-3"></pre>
</div>
</div>
</div>
</div>
</div>
<!-- Toast container for notifications -->
<div class="toast-container position-fixed bottom-0 end-0 p-3"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const listenerUrlInput = document.getElementById('listenerUrl');
const listenerSelect = document.getElementById('listenerSelect');
const connectListenerBtn = document.getElementById('connectListenerBtn');
const statusCircle = document.getElementById('statusCircle');
const toastContainer = document.querySelector('.toast-container');
// Handle listener selection dropdown
listenerSelect.addEventListener('change', () => {
const selectedValue = listenerSelect.value;
if (selectedValue === '') {
// Show custom URL input when "Custom URL..." is selected
listenerUrlInput.style.display = 'block';
listenerUrlInput.focus();
} else {
// Hide custom URL input when a predefined option is selected
listenerUrlInput.style.display = 'none';
// Clear any previous custom URL
listenerUrlInput.value = '';
}
});
// Function to show toast notifications
function showToast(message, type = 'error') {
const toastId = 'toast-' + Date.now();
const bgClass = type === 'error' ? 'bg-danger' : (type === 'warning' ? 'bg-warning' : 'bg-info');
const headerText = type === 'error' ? 'Error' : (type === 'warning' ? 'Warning' : 'Info');
// Create the toast element
const toastDiv = document.createElement('div');
toastDiv.className = 'toast mb-3';
toastDiv.id = toastId;
toastDiv.setAttribute('role', 'alert');
toastDiv.setAttribute('aria-live', 'assertive');
toastDiv.setAttribute('aria-atomic', 'true');
toastDiv.innerHTML = `
<div class="toast-header ${bgClass} text-white">
<strong class="me-auto">${headerText}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${message}
</div>
`;
// Add to container
toastContainer.appendChild(toastDiv);
// Initialize and show the toast
const toast = new bootstrap.Toast(toastDiv, {
autohide: true,
delay: 10000
});
toast.show();
// Remove the toast element after it's hidden
toastDiv.addEventListener('hidden.bs.toast', () => {
toastDiv.remove();
});
}
connectListenerBtn.addEventListener('click', async () => {
// Get URL from either dropdown or text input
let url = listenerSelect.value;
// If custom URL option is selected, use the text input value
if (url === '') {
url = listenerUrlInput.value.trim();
if (!url) {
showToast('Please enter a listener URL.');
return;
}
}
const connectBtnText = document.getElementById('connectBtnText');
const statusCircle = document.getElementById('statusCircle');
// Set to connecting state
connectBtnText.textContent = 'Connecting...';
connectListenerBtn.classList.remove('btn-outline-secondary', 'btn-success', 'btn-danger');
connectListenerBtn.classList.add('btn-warning');
statusCircle.style.backgroundColor = '#e6a800'; // Darker yellow for the circle
// Disable the button during connection attempts
connectListenerBtn.disabled = true;
// Function to attempt a ping
const attemptPing = async () => {
try {
const response = await fetch(url, {
method: 'HEAD',
mode: 'no-cors',
// Set a short timeout to fail faster
signal: AbortSignal.timeout(800)
});
return true; // Connection successful
} catch (error) {
console.error('Ping attempt failed:', error);
return false; // Connection failed
}
};
// Try pinging multiple times with 1-second intervals
let isConnected = false;
const maxAttempts = 5;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
connectBtnText.textContent = `Connecting... (${attempt}/${maxAttempts})`;
isConnected = await attemptPing();
if (isConnected) {
break; // Exit the loop if connection is successful
}
if (attempt < maxAttempts) {
// Wait 1 second before the next attempt
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// Re-enable the button
connectListenerBtn.disabled = false;
if (isConnected) {
// Set to connected state
connectBtnText.textContent = 'Connected';
connectListenerBtn.classList.remove('btn-outline-secondary', 'btn-warning', 'btn-danger');
connectListenerBtn.classList.add('btn-success');
statusCircle.style.backgroundColor = '#198754'; // Darker green for the circle
} else {
// Set to offline state
connectBtnText.textContent = 'Connect';
connectListenerBtn.classList.remove('btn-outline-secondary', 'btn-warning', 'btn-success');
connectListenerBtn.classList.add('btn-danger');
statusCircle.style.backgroundColor = '#dc3545'; // Darker red for the circle
// Show error toast
showToast(`Could not connect to ${url}. Please check the URL and try again.`);
}
});
});
</script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.2/lib/codemirror.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.2/mode/javascript/javascript.js"></script>
<script src="script.js"></script>
</body>
</html>

View File

@ -0,0 +1,131 @@
document.addEventListener('DOMContentLoaded', () => {
// Initialize CodeMirror editor
const editor = CodeMirror.fromTextArea(document.getElementById('scriptInput'), {
mode: 'javascript',
lineNumbers: true,
theme: 'default',
indentUnit: 4,
tabSize: 4,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true
});
// Get DOM elements
const runScriptBtn = document.getElementById('runScriptBtn');
const loadExampleBtn = document.getElementById('loadExampleBtn');
const modelSelect = document.getElementById('modelSelect');
const outputArea = document.getElementById('outputArea');
const toastContainer = document.querySelector('.toast-container');
// Function to show toast notifications
function showToast(message, type = 'error') {
const toastId = 'toast-' + Date.now();
const bgClass = type === 'error' ? 'bg-danger' : (type === 'warning' ? 'bg-warning' : 'bg-info');
const headerText = type === 'error' ? 'Error' : (type === 'warning' ? 'Warning' : 'Info');
// Create the toast element
const toastDiv = document.createElement('div');
toastDiv.className = 'toast mb-3';
toastDiv.id = toastId;
toastDiv.setAttribute('role', 'alert');
toastDiv.setAttribute('aria-live', 'assertive');
toastDiv.setAttribute('aria-atomic', 'true');
toastDiv.innerHTML = `
<div class="toast-header ${bgClass} text-white">
<strong class="me-auto">${headerText}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${message}
</div>
`;
// Add to container
toastContainer.appendChild(toastDiv);
// Initialize and show the toast
const toast = new bootstrap.Toast(toastDiv, {
autohide: true,
delay: 10000
});
toast.show();
// Remove the toast element after it's hidden
toastDiv.addEventListener('hidden.bs.toast', () => {
toastDiv.remove();
});
}
// Function to run the script
async function runScript() {
const script = editor.getValue();
const modelType = modelSelect.value;
if (!script.trim()) {
showToast('Please enter a script to run.');
return;
}
outputArea.textContent = 'Running script...';
try {
const response = await fetch('/api/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
script,
model_type: modelType
})
});
const result = await response.json();
if (result.success) {
outputArea.textContent = result.output || 'Script executed successfully with no output.';
} else {
outputArea.textContent = result.output || 'Script execution failed.';
showToast(result.error || 'Script execution failed.', 'error');
}
} catch (error) {
outputArea.textContent = 'Script execution failed. See error details.';
showToast(`Error: ${error.message}`, 'error');
}
}
// Function to load example script
async function loadExample() {
const modelType = modelSelect.value;
try {
const response = await fetch(`/api/example/${modelType}`);
if (!response.ok) {
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
}
const exampleScript = await response.text();
editor.setValue(exampleScript);
outputArea.textContent = `Loaded ${modelType} example script. Click "Run Script" to execute.`;
} catch (error) {
// Don't show error in output area, use toast instead
showToast(`Failed to load ${modelType} example: ${error.message}`, 'error');
// Clear the editor if it was a new load (not if there was already content)
if (!editor.getValue().trim()) {
editor.setValue('// Write your Rhai script here');
}
}
}
// Event listeners
runScriptBtn.addEventListener('click', runScript);
loadExampleBtn.addEventListener('click', loadExample);
// Load default example on page load
loadExample();
});

View File

@ -0,0 +1,49 @@
.card {
height: calc(100vh - 150px);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: #f1f3f5;
border-bottom: 1px solid #dee2e6;
}
.card-body {
overflow: hidden;
height: calc(100% - 110px);
}
.card-footer {
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
padding: 10px 15px;
}
.CodeMirror {
height: 100%;
font-size: 14px;
font-family: 'Courier New', monospace;
}
#outputArea {
height: 100%;
overflow: auto;
margin: 0;
font-family: 'Courier New', monospace;
font-size: 14px;
white-space: pre-wrap;
background-color: #f8f9fa;
color: #212529;
}
#modelSelect {
width: auto;
}
@media (max-width: 767.98px) {
.card {
height: 400px;
margin-bottom: 20px;
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
2

794
engine/Cargo.lock generated Normal file
View File

@ -0,0 +1,794 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"const-random",
"getrandom 0.3.3",
"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 = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"bincode_derive",
"serde",
"unty",
]
[[package]]
name = "bincode_derive"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
dependencies = [
"virtue",
]
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[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.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
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.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"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.16",
"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 = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "engine"
version = "0.1.0"
dependencies = [
"chrono",
"heromodels",
"heromodels-derive",
"heromodels_core",
"rhai",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "heromodels"
version = "0.1.0"
dependencies = [
"bincode",
"chrono",
"heromodels-derive",
"heromodels_core",
"ourdb",
"rhai",
"rhai_client_macros",
"serde",
"serde_json",
"strum",
"strum_macros",
"tst",
"uuid",
]
[[package]]
name = "heromodels-derive"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "heromodels_core"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
]
[[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 = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[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 = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[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 = "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 = "ourdb"
version = "0.1.0"
dependencies = [
"crc32fast",
"log",
"rand",
"thiserror",
]
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
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 = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "rhai"
version = "1.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2780e813b755850e50b178931aaf94ed24f6817f46aaaf5d21c13c12d939a249"
dependencies = [
"ahash",
"bitflags",
"instant",
"no-std-compat",
"num-traits",
"once_cell",
"rhai_codegen",
"rust_decimal",
"smallvec",
"smartstring",
"thin-vec",
]
[[package]]
name = "rhai_client_macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"rhai",
"syn",
]
[[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 = "rust_decimal"
version = "1.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50"
dependencies = [
"arrayvec",
"num-traits",
]
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[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 = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[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 = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
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 = "tst"
version = "0.1.0"
dependencies = [
"ourdb",
"thiserror",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "uuid"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom 0.3.3",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[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 = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
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.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

37
engine/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "engine"
version = "0.1.0"
edition = "2021"
description = "Central Rhai engine for heromodels"
[dependencies]
rhai = { version = "1.21.0", features = ["std", "sync", "decimal", "internals"] }
heromodels = { path = "../../db/heromodels", features = ["rhai"] }
heromodels_core = { path = "../../db/heromodels_core" }
chrono = "0.4"
heromodels-derive = { path = "../../db/heromodels-derive" }
[features]
default = ["calendar", "finance"]
calendar = []
finance = []
# Flow module is now updated to use our approach to Rhai engine registration
flow = []
legal = []
projects = []
biz = []
[[example]]
name = "calendar_example"
path = "examples/calendar/example.rs"
required-features = ["calendar"]
[[example]]
name = "flow_example"
path = "examples/flow/example.rs"
required-features = ["flow"]
[[example]]
name = "finance_example"
path = "examples/finance/example.rs"
required-features = ["finance"]

93
engine/README.md Normal file
View File

@ -0,0 +1,93 @@
# HeroModels Rhai
A central Rhai scripting engine for the HeroModels project that provides a unified way to interact with all HeroModels types through Rhai scripts.
## Overview
This crate provides:
1. A central Rhai engine that registers all HeroModels modules
2. Helper functions for evaluating Rhai scripts and ASTs
3. Example scripts demonstrating how to use HeroModels with Rhai
4. A mock database implementation for testing and examples
## Usage
### Basic Usage
```rust
use std::sync::Arc;
use engine::{create_heromodels_engine, eval_script};
use engine::mock_db::create_mock_db;
// Create a mock database
let db = create_mock_db();
// Create the Rhai engine with all modules registered
let engine = create_heromodels_engine(db);
// Run a Rhai script
let result = eval_script(&engine, r#"
let calendar = new_calendar("My Calendar");
calendar.set_description("My personal calendar");
print(`Created calendar: ${calendar.get_name()}`);
calendar
"#);
match result {
Ok(val) => println!("Script returned: {:?}", val),
Err(err) => eprintln!("Script error: {}", err),
}
```
### Using Specific Modules
If you only need specific modules, you can register them individually:
```rust
use std::sync::Arc;
use rhai::Engine;
use engine::mock_db::create_mock_db;
// Create a mock database
let db = create_mock_db();
// Create a new Rhai engine
let mut engine = Engine::new();
// Register only the calendar module
heromodels::models::calendar::register_calendar_rhai_module(&mut engine, db.clone());
// Now you can use calendar-related functions in your scripts
```
## Examples
This crate includes several examples demonstrating how to use different HeroModels modules with Rhai:
- `calendar_example`: Working with calendars, events, and attendees
- `flow_example`: Working with flows, steps, and signature requirements
- `legal_example`: Working with contracts, revisions, and signers
- `projects_example`: Working with projects and their properties
- `finance_example`: Working with financial models (if available)
To run an example:
```bash
cargo run --example calendar_example
```
## Features
The crate supports the following features:
- `flow`: Enable the Flow module
- `legal`: Enable the Legal module
- `projects`: Enable the Projects module
- `biz`: Enable the Business module
By default, only the Calendar module is always enabled.
## License
Same as the HeroModels project.

16
engine/build.rs Normal file
View File

@ -0,0 +1,16 @@
fn main() {
// Tell Cargo to re-run this build script if the calendar/rhai.rs file changes
println!("cargo:rerun-if-changed=../heromodels/src/models/calendar/rhai.rs");
// Tell Cargo to re-run this build script if the flow/rhai.rs file changes
println!("cargo:rerun-if-changed=../heromodels/src/models/flow/rhai.rs");
// Tell Cargo to re-run this build script if the legal/rhai.rs file changes
println!("cargo:rerun-if-changed=../heromodels/src/models/legal/rhai.rs");
// Tell Cargo to re-run this build script if the projects/rhai.rs file changes
println!("cargo:rerun-if-changed=../heromodels/src/models/projects/rhai.rs");
// Tell Cargo to re-run this build script if the biz/rhai.rs file changes
println!("cargo:rerun-if-changed=../heromodels/src/models/biz/rhai.rs");
}

View File

@ -0,0 +1,101 @@
// calendar_script.rhai
// Example Rhai script for working with Calendar models
// Constants for AttendanceStatus
const NO_RESPONSE = "NoResponse";
const ACCEPTED = "Accepted";
const DECLINED = "Declined";
const TENTATIVE = "Tentative";
// Create a new calendar using builder pattern
let my_calendar = new_calendar()
.name("Team Calendar")
.description("Calendar for team events and meetings");
print(`Created calendar: ${my_calendar.name} (${my_calendar.id})`);
// Add attendees to the event
let alice = new_attendee()
.with_contact_id(1)
.with_status(NO_RESPONSE);
let bob = new_attendee()
.with_contact_id(2)
.with_status(ACCEPTED);
let charlie = new_attendee()
.with_contact_id(3)
.with_status(TENTATIVE);
// Create a new event using builder pattern
// Note: Timestamps are in seconds since epoch
let now = timestamp_now();
let one_hour = 60 * 60;
let meeting = new_event()
.title("Weekly Sync")
.reschedule(now, now + one_hour)
.location("Conference Room A")
.description("Regular team sync meeting")
.add_attendee(alice)
.add_attendee(bob)
.add_attendee(charlie)
.save_event();
print(`Created event: ${meeting.title}`);
meeting.delete_event();
print(`Deleted event: ${meeting.title}`);
// Print attendees info
let attendees = meeting.attendees;
print(`Added attendees to the event`);
// Update Charlie's attendee status directly
meeting.update_attendee_status(3, ACCEPTED);
print(`Updated Charlie's status to: ${ACCEPTED}`);
// Add the event to the calendar
my_calendar.add_event_to_calendar(meeting);
// Print events info
print(`Added event to calendar`);
// Save the calendar to the database
let saved_calendar = my_calendar.save_calendar();
print(`Calendar saved to database with ID: ${saved_calendar.id}`);
// Retrieve the calendar from the database using the ID from the saved calendar
let retrieved_calendar = get_calendar_by_id(saved_calendar.id);
if retrieved_calendar != () {
print(`Retrieved calendar: ${retrieved_calendar.name}`);
print(`Retrieved calendar successfully`);
} else {
print("Failed to retrieve calendar from database");
}
// List all calendars in the database
let all_calendars = list_calendars();
print("\nListing all calendars in database:");
let calendar_count = 0;
for calendar in all_calendars {
print(` - Calendar: ${calendar.name} (ID: ${calendar.id})`);
calendar_count += 1;
}
print(`Total calendars: ${calendar_count}`);
// List all events in the database
let all_events = list_events();
print("\nListing all events in database:");
let event_count = 0;
for event in all_events {
print(` - Event: ${event.title} (ID: ${event.id})`);
event_count += 1;
}
print(`Total events: ${event_count}`);
// Helper function to get current timestamp
fn timestamp_now() {
// This would typically be provided by the host application
// For this example, we'll use a fixed timestamp
1685620800 // June 1, 2023, 12:00 PM
}

View File

@ -0,0 +1,66 @@
use std::sync::Arc;
use std::path::Path;
use rhai::{Engine, Scope};
use heromodels::models::calendar::{Calendar, Event, Attendee, AttendanceStatus};
use engine::{create_heromodels_engine, eval_file};
use engine::mock_db::{create_mock_db, seed_mock_db};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Calendar Rhai Example");
println!("=====================");
// Create a mock database
let db = create_mock_db();
// Seed the database with some initial data
seed_mock_db(db.clone());
// Create the Rhai engine using our central engine creator
let mut engine = create_heromodels_engine(db.clone());
// Register timestamp helper functions
register_timestamp_helpers(&mut engine);
// Get the path to the script
let script_path = Path::new(file!())
.parent()
.unwrap()
.join("calendar_script.rhai");
println!("\nRunning script: {}", script_path.display());
println!("---------------------");
// Run the script
match eval_file(&engine, &script_path.to_string_lossy()) {
Ok(result) => {
if !result.is_unit() {
println!("\nScript returned: {:?}", result);
}
println!("\nScript executed successfully!");
Ok(())
},
Err(err) => {
eprintln!("\nError running script: {}", err);
Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, err.to_string())))
}
}
}
// Register timestamp helper functions with the engine
fn register_timestamp_helpers(engine: &mut Engine) {
use chrono::{DateTime, Utc, TimeZone, NaiveDateTime};
// Function to get current timestamp
engine.register_fn("timestamp_now", || {
Utc::now().timestamp() as i64
});
// Function to format a timestamp
engine.register_fn("format_timestamp", |ts: i64| {
let dt = Utc.timestamp_opt(ts, 0).single()
.expect("Invalid timestamp");
dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
});
println!("Timestamp helper functions registered successfully.");
}

View File

@ -0,0 +1,68 @@
use std::sync::Arc;
use std::path::Path;
use rhai::{Engine, Scope};
use heromodels::models::finance::account::Account;
use heromodels::models::finance::asset::{Asset, AssetType};
use heromodels::models::finance::marketplace::{Listing, Bid, ListingStatus, ListingType, BidStatus};
use engine::{create_heromodels_engine, eval_file};
use engine::mock_db::{create_mock_db, seed_mock_db};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Finance Rhai Example");
println!("===================");
// Create a mock database
let db = create_mock_db();
// Seed the database with some initial data
seed_mock_db(db.clone());
// Create the Rhai engine using our central engine creator
let mut engine = create_heromodels_engine(db.clone());
// Register timestamp helper functions
register_timestamp_helpers(&mut engine);
// Get the path to the script
let script_path = Path::new(file!())
.parent()
.unwrap()
.join("finance_script.rhai");
println!("\nRunning script: {}", script_path.display());
println!("---------------------");
// Run the script
match eval_file(&engine, &script_path.to_string_lossy()) {
Ok(result) => {
if !result.is_unit() {
println!("\nScript returned: {:?}", result);
}
println!("\nScript executed successfully!");
Ok(())
},
Err(err) => {
eprintln!("\nError running script: {}", err);
Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, err.to_string())))
}
}
}
// Register timestamp helper functions with the engine
fn register_timestamp_helpers(engine: &mut Engine) {
use chrono::{DateTime, Utc, TimeZone, NaiveDateTime};
// Function to get current timestamp
engine.register_fn("timestamp_now", || {
Utc::now().timestamp() as i64
});
// Function to format a timestamp
engine.register_fn("format_timestamp", |ts: i64| {
let dt = Utc.timestamp_opt(ts, 0).single()
.expect("Invalid timestamp");
dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
});
println!("Timestamp helper functions registered successfully.");
}

View File

@ -0,0 +1,202 @@
// finance_script.rhai
// Example Rhai script for working with Finance models
// Constants for AssetType
const NATIVE = "Native";
const ERC20 = "Erc20";
const ERC721 = "Erc721";
const ERC1155 = "Erc1155";
// Constants for ListingStatus
const ACTIVE = "Active";
const SOLD = "Sold";
const CANCELLED = "Cancelled";
const EXPIRED = "Expired";
// Constants for ListingType
const FIXED_PRICE = "FixedPrice";
const AUCTION = "Auction";
const EXCHANGE = "Exchange";
// Constants for BidStatus
const BID_ACTIVE = "Active";
const BID_ACCEPTED = "Accepted";
const BID_REJECTED = "Rejected";
const BID_CANCELLED = "Cancelled";
// Create a new account using builder pattern
let alice_account = new_account()
.name("Alice's Account")
.user_id(101)
.description("Alice's primary trading account")
.ledger("ethereum")
.address("0x1234567890abcdef1234567890abcdef12345678")
.pubkey("0xabcdef1234567890abcdef1234567890abcdef12");
print(`Created account: ${alice_account.get_name()} (User ID: ${alice_account.get_user_id()})`);
// Save the account to the database
let saved_alice = set_account(alice_account);
print(`Account saved to database with ID: ${saved_alice.get_id()}`);
// Create a new asset using builder pattern
let token_asset = new_asset()
.name("HERO Token")
.description("Herocode governance token")
.amount(1000.0)
.address("0x9876543210abcdef9876543210abcdef98765432")
.asset_type(ERC20)
.decimals(18);
print(`Created asset: ${token_asset.get_name()} (${token_asset.get_amount()} ${token_asset.get_asset_type()})`);
// Save the asset to the database
let saved_token = set_asset(token_asset);
print(`Asset saved to database with ID: ${saved_token.get_id()}`);
// Add the asset to Alice's account
saved_alice = saved_alice.add_asset(saved_token.get_id());
saved_alice = set_account(saved_alice);
print(`Added asset ${saved_token.get_name()} to ${saved_alice.get_name()}`);
// Create a new NFT asset
let nft_asset = new_asset()
.name("Herocode #42")
.description("Unique digital collectible")
.amount(1.0)
.address("0xabcdef1234567890abcdef1234567890abcdef12")
.asset_type(ERC721)
.decimals(0);
// Save the NFT to the database
let saved_nft = set_asset(nft_asset);
print(`NFT saved to database with ID: ${saved_nft.get_id()}`);
// Create Bob's account
let bob_account = new_account()
.name("Bob's Account")
.user_id(102)
.description("Bob's trading account")
.ledger("ethereum")
.address("0xfedcba0987654321fedcba0987654321fedcba09")
.pubkey("0x654321fedcba0987654321fedcba0987654321fe");
// Save Bob's account
let saved_bob = set_account(bob_account);
print(`Created and saved Bob's account with ID: ${saved_bob.get_id()}`);
// Create a listing for the NFT
let nft_listing = new_listing()
.seller_id(saved_alice.get_id())
.asset_id(saved_nft.get_id())
.price(0.5)
.currency("ETH")
.listing_type(AUCTION)
.title("Rare Herocode NFT")
.description("One of a kind digital collectible")
.image_url("https://example.com/nft/42.png")
.expires_at(timestamp_now() + 86400) // 24 hours from now
.add_tag("rare")
.add_tag("collectible")
.add_tag("digital art")
.set_listing();
// Save the listing
print(`Created listing: ${nft_listing.get_title()} (ID: ${nft_listing.get_id()})`);
print(`Listing status: ${nft_listing.get_status()}, Type: ${nft_listing.get_listing_type()}`);
print(`Listing price: ${nft_listing.get_price()} ${nft_listing.get_currency()}`);
// Create a bid from Bob
let bob_bid = new_bid()
.listing_id(nft_listing.get_id().to_string())
.bidder_id(saved_bob.get_id())
.amount(1.5)
.currency("ETH")
.set_bid();
// Save the bid
print(`Created bid from ${saved_bob.get_name()} for ${bob_bid.get_amount()} ${bob_bid.get_currency()}`);
// Add the bid to the listing
nft_listing.add_bid(bob_bid);
nft_listing.set_listing();
print(`Added bid to listing ${nft_listing.get_title()}`);
// Create another bid with higher amount
let charlie_account = new_account()
.name("Charlie's Account")
.user_id(103)
.description("Charlie's trading account")
.ledger("ethereum")
.address("0x1122334455667788991122334455667788990011")
.pubkey("0x8877665544332211887766554433221188776655");
let saved_charlie = set_account(charlie_account);
print(`Created and saved Charlie's account with ID: ${saved_charlie.get_id()}`);
let charlie_bid = new_bid()
.listing_id(nft_listing.get_id().to_string())
.bidder_id(saved_charlie.get_id())
.amount(2.5)
.currency("ETH")
.set_bid();
print(`Created higher bid from ${saved_charlie.get_name()} for ${charlie_bid.get_amount()} ${charlie_bid.get_currency()}`);
// Add the higher bid to the listing
nft_listing.add_bid(charlie_bid)
.set_listing();
print(`Added higher bid to listing ${nft_listing.get_title()}`);
nft_listing.sale_price(2.5)
.set_listing();
// Complete the sale to the highest bidder (Charlie)
nft_listing.complete_sale(saved_charlie.get_id())
.set_listing();
print(`Completed sale of ${nft_listing.get_title()} to ${saved_charlie.get_name()}`);
print(`New listing status: ${saved_listing.get_status()}`);
// Retrieve the listing from the database
let retrieved_listing = get_listing_by_id(saved_listing.get_id());
print(`Retrieved listing: ${retrieved_listing.get_title()} (Status: ${retrieved_listing.get_status()})`);
// Create a fixed price listing
let token_listing = new_listing()
.seller_id(saved_alice.get_id())
.asset_id(saved_token.get_id())
.price(100.0)
.currency("USDC")
.listing_type(FIXED_PRICE)
.title("HERO Tokens for Sale")
.description("100 HERO tokens at fixed price")
.set_listing();
// Save the fixed price listing
print(`Created fixed price listing: ${token_listing.get_title()} (ID: ${token_listing.get_id()})`);
// Cancel the listing
token_listing.cancel();
token_listing.set_listing();
print(`Cancelled listing: ${token_listing.get_title()}`);
print(`Listing status: ${token_listing.get_status()}`);
// Print summary of all accounts
print("\nAccount Summary:");
print(`Alice (ID: ${saved_alice.get_id()}): ${saved_alice.get_assets().len()} assets`);
print(`Bob (ID: ${saved_bob.get_id()}): ${saved_bob.get_assets().len()} assets`);
print(`Charlie (ID: ${saved_charlie.get_id()}): ${saved_charlie.get_assets().len()} assets`);
// Print summary of all listings
print("\nListing Summary:");
print(`NFT Auction (ID: ${nft_listing.get_id()}): ${nft_listing.get_status()}`);
print(`Token Sale (ID: ${token_listing.get_id()}): ${token_listing.get_status()}`);
// Print summary of all bids
print("\nBid Summary:");
print(`Bob's bid: ${bob_bid.get_amount()} ${bob_bid.get_currency()} (Status: ${bob_bid.get_status()})`);
print(`Charlie's bid: ${charlie_bid.get_amount()} ${charlie_bid.get_currency()} (Status: ${charlie_bid.get_status()})`);

View File

@ -0,0 +1,120 @@
use std::path::Path;
use rhai::{Scope};
use heromodels::models::flow::{Flow, FlowStep, SignatureRequirement};
use engine::{create_heromodels_engine, eval_file};
use engine::mock_db::{create_mock_db, seed_mock_db};
use heromodels_core::Model;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Flow Rhai Example");
println!("=================");
// Create a mock database
let db = create_mock_db();
// Seed the database with initial data
seed_mock_db(db.clone());
// Create the Rhai engine with all modules registered
let engine = create_heromodels_engine(db.clone());
// Get the path to the script
let script_path = Path::new(file!())
.parent()
.unwrap()
.join("flow_script.rhai");
println!("\nRunning script: {}", script_path.display());
println!("---------------------");
// Run the script
match eval_file(&engine, &script_path.to_string_lossy()) {
Ok(result) => {
if !result.is_unit() {
println!("\nScript returned: {:?}", result);
}
println!("\nScript executed successfully!");
},
Err(err) => {
eprintln!("\nError running script: {}", err);
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, err.to_string())));
}
}
// Demonstrate direct Rust interaction with the Rhai-exposed flow functionality
println!("\nDirect Rust interaction with Rhai-exposed flow functionality");
println!("----------------------------------------------------------");
// Create a new scope
let mut scope = Scope::new();
// Create a new flow using the Rhai function
let result = engine.eval::<Flow>("new_flow(0, \"Direct Rust Flow\")");
match result {
Ok(mut flow) => {
println!("Created flow from Rust: {} (ID: {})", flow.name, flow.get_id());
// Set flow status using the builder pattern
flow = flow.status("active".to_string());
println!("Set flow status to: {}", flow.status);
// Create a new flow step using the Rhai function
let result = engine.eval::<FlowStep>("new_flow_step(0, 1)");
match result {
Ok(mut step) => {
println!("Created flow step from Rust: Step Order {} (ID: {})",
step.step_order, step.get_id());
// Set step description
step = step.description("Direct Rust Step".to_string());
println!("Set step description to: {}",
step.description.clone().unwrap_or_else(|| "None".to_string()));
// Create a signature requirement using the Rhai function
let result = engine.eval::<SignatureRequirement>(
"new_signature_requirement(0, 1, \"Direct Rust Signer\", \"Please sign this document\")"
);
match result {
Ok(req) => {
println!("Created signature requirement from Rust: Public Key {} (ID: {})",
req.public_key, req.get_id());
// Add the step to the flow using the builder pattern
flow = flow.add_step(step);
println!("Added step to flow. Flow now has {} steps", flow.steps.len());
// Save the flow to the database using the Rhai function
let save_flow_script = "fn save_it(f) { return db::save_flow(f); }";
let save_flow_ast = engine.compile(save_flow_script).unwrap();
let result = engine.call_fn::<Flow>(&mut scope, &save_flow_ast, "save_it", (flow,));
match result {
Ok(saved_flow) => {
println!("Saved flow to database with ID: {}", saved_flow.get_id());
},
Err(err) => eprintln!("Error saving flow: {}", err),
}
// Save the signature requirement to the database using the Rhai function
let save_req_script = "fn save_it(r) { return db::save_signature_requirement(r); }";
let save_req_ast = engine.compile(save_req_script).unwrap();
let result = engine.call_fn::<SignatureRequirement>(&mut scope, &save_req_ast, "save_it", (req,));
match result {
Ok(saved_req) => {
println!("Saved signature requirement to database with ID: {}", saved_req.get_id());
},
Err(err) => eprintln!("Error saving signature requirement: {}", err),
}
},
Err(err) => eprintln!("Error creating signature requirement: {}", err),
}
},
Err(err) => eprintln!("Error creating flow step: {}", err),
}
},
Err(err) => eprintln!("Error creating flow: {}", err),
}
Ok(())
}

View File

@ -0,0 +1,111 @@
// flow_script.rhai
// Example Rhai script for working with Flow models
// Constants for Flow status
const STATUS_DRAFT = "draft";
const STATUS_ACTIVE = "active";
const STATUS_COMPLETED = "completed";
const STATUS_CANCELLED = "cancelled";
// Create a new flow using builder pattern
let my_flow = new_flow(0, "flow-123");
name(my_flow, "Document Approval Flow");
status(my_flow, STATUS_DRAFT);
print(`Created flow: ${get_flow_name(my_flow)} (ID: ${get_flow_id(my_flow)})`);
print(`Status: ${get_flow_status(my_flow)}`);
// Create flow steps using builder pattern
let step1 = new_flow_step(0, 1);
description(step1, "Initial review by legal team");
status(step1, STATUS_DRAFT);
let step2 = new_flow_step(0, 2);
description(step2, "Approval by department head");
status(step2, STATUS_DRAFT);
let step3 = new_flow_step(0, 3);
description(step3, "Final signature by CEO");
status(step3, STATUS_DRAFT);
// Create signature requirements using builder pattern
let req1 = new_signature_requirement(0, get_flow_step_id(step1), "legal@example.com", "Please review this document");
signed_by(req1, "Legal Team");
status(req1, STATUS_DRAFT);
let req2 = new_signature_requirement(0, get_flow_step_id(step2), "dept@example.com", "Department approval needed");
signed_by(req2, "Department Head");
status(req2, STATUS_DRAFT);
let req3 = new_signature_requirement(0, get_flow_step_id(step3), "ceo@example.com", "Final approval required");
signed_by(req3, "CEO");
status(req3, STATUS_DRAFT);
print(`Created flow steps with signature requirements`);
// Add steps to the flow
let flow_with_steps = my_flow;
add_step(flow_with_steps, step1);
add_step(flow_with_steps, step2);
add_step(flow_with_steps, step3);
print(`Added steps to flow. Flow now has ${get_flow_steps(flow_with_steps).len()} steps`);
// Activate the flow
let active_flow = flow_with_steps;
status(active_flow, STATUS_ACTIVE);
print(`Updated flow status to: ${get_flow_status(active_flow)}`);
// Save the flow to the database
let saved_flow = db::save_flow(active_flow);
print(`Flow saved to database with ID: ${get_flow_id(saved_flow)}`);
// Save signature requirements to the database
let saved_req1 = db::save_signature_requirement(req1);
let saved_req2 = db::save_signature_requirement(req2);
let saved_req3 = db::save_signature_requirement(req3);
print(`Signature requirements saved to database with IDs: ${get_signature_requirement_id(saved_req1)}, ${get_signature_requirement_id(saved_req2)}, ${get_signature_requirement_id(saved_req3)}`);
// Retrieve the flow from the database
let retrieved_flow = db::get_flow_by_id(get_flow_id(saved_flow));
print(`Retrieved flow: ${get_flow_name(retrieved_flow)}`);
print(`It has ${get_flow_steps(retrieved_flow).len()} steps`);
// Complete the flow
let completed_flow = retrieved_flow;
status(completed_flow, STATUS_COMPLETED);
print(`Updated retrieved flow status to: ${get_flow_status(completed_flow)}`);
// Save the updated flow
db::save_flow(completed_flow);
print("Updated flow saved to database");
// List all flows in the database
let all_flows = db::list_flows();
print("\nListing all flows in database:");
let flow_count = 0;
for flow in all_flows {
print(` - Flow: ${get_flow_name(flow)} (ID: ${get_flow_id(flow)})`);
flow_count += 1;
}
print(`Total flows: ${flow_count}`);
// List all signature requirements
let all_reqs = db::list_signature_requirements();
print("\nListing all signature requirements in database:");
let req_count = 0;
for req in all_reqs {
print(` - Requirement for step ${get_signature_requirement_flow_step_id(req)} (ID: ${get_signature_requirement_id(req)})`);
req_count += 1;
}
print(`Total signature requirements: ${req_count}`);
// Clean up - delete the flow
db::delete_flow(get_flow_id(completed_flow));
print(`Deleted flow with ID: ${get_flow_id(completed_flow)}`);
// Clean up - delete signature requirements
db::delete_signature_requirement(get_signature_requirement_id(saved_req1));
db::delete_signature_requirement(get_signature_requirement_id(saved_req2));
db::delete_signature_requirement(get_signature_requirement_id(saved_req3));
print("Deleted all signature requirements");

72
engine/src/lib.rs Normal file
View File

@ -0,0 +1,72 @@
use rhai::{Engine, AST, Scope};
use std::sync::Arc;
use std::sync::Mutex;
use std::collections::HashMap;
use heromodels::db::hero::OurDB;
// Export the mock database module
pub mod mock_db;
pub fn create_heromodels_engine(db: Arc<OurDB>) -> Engine {
let mut engine = Engine::new();
// Configure engine settings
engine.set_max_expr_depths(128, 128);
engine.set_max_string_size(10 * 1024 * 1024); // 10 MB
engine.set_max_array_size(10 * 1024); // 10K elements
engine.set_max_map_size(10 * 1024); // 10K elements
// Register all heromodels Rhai modules
register_all_modules(&mut engine, db);
engine
}
/// Register all heromodels Rhai modules with the engine
pub fn register_all_modules(engine: &mut Engine, db: Arc<OurDB>) {
// Register the calendar module if the feature is enabled
#[cfg(feature = "calendar")]
heromodels::models::calendar::register_calendar_rhai_module(engine, db.clone());
// Register the flow module if the feature is enabled
#[cfg(feature = "flow")]
heromodels::models::flow::register_flow_rhai_module(engine, db.clone());
// // Register the finance module if the feature is enabled
// #[cfg(feature = "finance")]
// heromodels::models::finance::register_finance_rhai_module(engine, db.clone());
// Register the legal module if the feature is enabled
#[cfg(feature = "legal")]
heromodels::models::legal::register_legal_rhai_module(engine, db.clone());
// Register the projects module if the feature is enabled
#[cfg(feature = "projects")]
heromodels::models::projects::register_projects_rhai_module(engine, db.clone());
// Register the biz module if the feature is enabled
#[cfg(feature = "biz")]
heromodels::models::biz::register_biz_rhai_module(engine, db.clone());
println!("Heromodels Rhai modules registered successfully.");
}
/// Evaluate a Rhai script file
pub fn eval_file(engine: &Engine, file_path: &str) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
engine.eval_file::<rhai::Dynamic>(file_path.into())
}
/// Evaluate a Rhai script string
pub fn eval_script(engine: &Engine, script: &str) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
engine.eval::<rhai::Dynamic>(script)
}
/// Compile a Rhai script to AST for repeated execution
pub fn compile_script(engine: &Engine, script: &str) -> Result<AST, Box<rhai::EvalAltResult>> {
Ok(engine.compile(script)?)
}
/// Run a compiled Rhai script AST
pub fn run_ast(engine: &Engine, ast: &AST, scope: &mut Scope) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
engine.eval_ast_with_scope(scope, ast)
}

315
engine/src/mock_db.rs Normal file
View File

@ -0,0 +1,315 @@
use std::sync::Arc;
use std::env;
use heromodels::db::hero::OurDB;
use heromodels::db::{Db, Collection}; // Import both Db and Collection traits
use heromodels::models::calendar::{Calendar, Event, Attendee, AttendanceStatus};
use heromodels_core::Model; // Import Model trait to use build method
use chrono::Utc;
use heromodels::models::userexample::User;
// Import finance models
use heromodels::models::finance::account::Account;
use heromodels::models::finance::asset::{Asset, AssetType};
use heromodels::models::finance::marketplace::{Listing, Bid, ListingStatus, ListingType, BidStatus};
// Conditionally import other modules based on features
#[cfg(feature = "flow")]
use heromodels::models::flow::{Flow, FlowStep, SignatureRequirement};
#[cfg(feature = "legal")]
use heromodels::models::legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus};
#[cfg(feature = "projects")]
use heromodels::models::projects::{Project, Status as ProjectStatus, Priority, ItemType};
/// Create a mock in-memory database for examples
pub fn create_mock_db() -> Arc<OurDB> {
// Create a temporary directory for the database files
let temp_dir = env::temp_dir().join("engine_examples");
std::fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
// Create a new OurDB instance with reset=true to ensure it's clean
let db = OurDB::new(temp_dir, true).expect("Failed to create OurDB instance");
Arc::new(db)
}
/// Seed the mock database with some initial data for all modules
pub fn seed_mock_db(db: Arc<OurDB>) {
// Seed calendar data
seed_calendar_data(db.clone());
// Seed finance data
seed_finance_data(db.clone());
// Seed flow data if the feature is enabled
#[cfg(feature = "flow")]
seed_flow_data(db.clone());
// Seed legal data if the feature is enabled
#[cfg(feature = "legal")]
seed_legal_data(db.clone());
// Seed projects data if the feature is enabled
#[cfg(feature = "projects")]
seed_projects_data(db.clone());
println!("Mock database seeded with initial data for all enabled modules.");
}
/// Seed the mock database with calendar data
fn seed_calendar_data(db: Arc<OurDB>) {
// Create a calendar
let mut calendar = Calendar::new(None, "Work Calendar".to_string());
calendar.description = Some("My work schedule".to_string());
// Store the calendar in the database
let (calendar_id, updated_calendar) = db.collection::<Calendar>()
.expect("Failed to get Calendar collection")
.set(&calendar)
.expect("Failed to store calendar");
// Create an event
let now = Utc::now().timestamp();
let end_time = now + 3600; // Add 1 hour in seconds
// Use the builder pattern for Event
let event = Event::new()
.title("Team Meeting".to_string())
.reschedule(now, end_time)
.location("Conference Room A".to_string())
.description("Weekly sync".to_string())
// .add_attendee(Attendee::new(1))
// .add_attendee(Attendee::new(2))
.build();
// // Add attendees to the event using the builder pattern
// let attendee1 = Attendee::new(1);
// let attendee2 = Attendee::new(2);
// // Add attendees using the builder pattern
// event = event.add_attendee(attendee1);
// event = event.add_attendee(attendee2);
// Call build and capture the returned value
// let event = event.build();
// Store the event in the database first to get its ID
let (event_id, updated_event) = db.collection()
.expect("Failed to get Event collection")
.set(&event)
.expect("Failed to store event");
// Add the event ID to the calendar
calendar = calendar.add_event(event_id as i64);
// Store the calendar in the database
let (calendar_id, updated_calendar) = db.collection::<Calendar>()
.expect("Failed to get Calendar collection")
.set(&calendar)
.expect("Failed to store calendar");
println!("Mock database seeded with calendar data:");
println!(" - Added calendar: {} (ID: {})", updated_calendar.name, updated_calendar.base_data.id);
println!(" - Added event: {} (ID: {})", updated_event.title, updated_event.base_data.id);
}
/// Seed the mock database with flow data
#[cfg(feature = "flow")]
fn seed_flow_data(db: Arc<OurDB>) {
// Create a flow
let mut flow = Flow::new(0, "Document Approval".to_string());
// Set flow properties using the builder pattern
flow = flow.status("draft".to_string());
flow = flow.name("Document Approval Flow".to_string());
// Create flow steps
let mut step1 = FlowStep::new(0, 1);
step1 = step1.description("Initial review by legal team".to_string());
step1 = step1.status("pending".to_string());
let mut step2 = FlowStep::new(0, 2);
step2 = step2.description("Approval by department head".to_string());
step2 = step2.status("pending".to_string());
// Add signature requirements
let mut req1 = SignatureRequirement::new(0, 1, "Legal Team".to_string(), "Please review this document".to_string());
let mut req2 = SignatureRequirement::new(0, 2, "Department Head".to_string(), "Please approve this document".to_string());
// Add steps to flow
flow = flow.add_step(step1);
flow = flow.add_step(step2);
// Store in the database
let (_, updated_flow) = db.collection::<Flow>()
.expect("Failed to get Flow collection")
.set(&flow)
.expect("Failed to store flow");
// Store signature requirements in the database
let (_, updated_req1) = db.collection::<SignatureRequirement>()
.expect("Failed to get SignatureRequirement collection")
.set(&req1)
.expect("Failed to store signature requirement");
let (_, updated_req2) = db.collection::<SignatureRequirement>()
.expect("Failed to get SignatureRequirement collection")
.set(&req2)
.expect("Failed to store signature requirement");
println!("Mock database seeded with flow data:");
println!(" - Added flow: {} (ID: {})", updated_flow.name, updated_flow.base_data.id);
println!(" - Added {} steps", updated_flow.steps.len());
println!(" - Added signature requirements with IDs: {} and {}",
updated_req1.base_data.id, updated_req2.base_data.id);
}
/// Seed the mock database with legal data
#[cfg(feature = "legal")]
fn seed_legal_data(db: Arc<OurDB>) {
// Create a contract
let mut contract = Contract::new(None, "Service Agreement".to_string());
contract.description = Some("Agreement for software development services".to_string());
contract.status = ContractStatus::Draft;
// Create a revision
let revision = ContractRevision::new(
None,
"Initial draft".to_string(),
"https://example.com/contract/v1".to_string(),
);
// Create signers
let signer1 = ContractSigner::new(None, 1, "Client".to_string());
let signer2 = ContractSigner::new(None, 2, "Provider".to_string());
// Add revision and signers to contract
contract.add_revision(revision);
contract.add_signer(signer1);
contract.add_signer(signer2);
// Store in the database
let (_, updated_contract) = db.collection::<Contract>()
.expect("Failed to get Contract collection")
.set(&contract)
.expect("Failed to store contract");
println!("Mock database seeded with legal data:");
println!(" - Added contract: {} (ID: {})", updated_contract.name, updated_contract.base_data.id);
println!(" - Added {} revisions and {} signers",
updated_contract.revisions.len(),
updated_contract.signers.len());
}
/// Seed the mock database with projects data
#[cfg(feature = "projects")]
fn seed_projects_data(db: Arc<OurDB>) {
// Create a project
let mut project = Project::new(None, "Website Redesign".to_string());
project.description = Some("Redesign the company website".to_string());
project.status = ProjectStatus::InProgress;
project.priority = Priority::High;
// Add members and tags
project.add_member_id(1);
project.add_member_id(2);
project.add_tag("design".to_string());
project.add_tag("web".to_string());
// Store in the database
let (_, updated_project) = db.collection::<Project>()
.expect("Failed to get Project collection")
.set(&project)
.expect("Failed to store project");
println!("Mock database seeded with projects data:");
println!(" - Added project: {} (ID: {})", updated_project.name, updated_project.base_data.id);
println!(" - Status: {}, Priority: {}", updated_project.status, updated_project.priority);
println!(" - Added {} members and {} tags",
updated_project.member_ids.len(),
updated_project.tags.len());
}
/// Seed the mock database with finance data
fn seed_finance_data(db: Arc<OurDB>) {
// Create a user account
let mut account = Account::new()
.name("Demo Account")
.user_id(1)
.description("Demo trading account")
.ledger("ethereum")
.address("0x1234567890abcdef1234567890abcdef12345678")
.pubkey("0xabcdef1234567890abcdef1234567890abcdef12");
// Store the account in the database
let (account_id, updated_account) = db.collection::<Account>()
.expect("Failed to get Account collection")
.set(&account)
.expect("Failed to store account");
// Create an ERC20 token asset
let token_asset = Asset::new()
.name("HERO Token")
.description("Herocode governance token")
.amount(1000.0)
.address("0x9876543210abcdef9876543210abcdef98765432")
.asset_type(AssetType::Erc20)
.decimals(18);
// Store the token asset in the database
let (token_id, updated_token) = db.collection::<Asset>()
.expect("Failed to get Asset collection")
.set(&token_asset)
.expect("Failed to store token asset");
// Create an NFT asset
let nft_asset = Asset::new()
.name("Herocode #1")
.description("Unique digital collectible")
.amount(1.0)
.address("0xabcdef1234567890abcdef1234567890abcdef12")
.asset_type(AssetType::Erc721)
.decimals(0);
// Store the NFT asset in the database
let (nft_id, updated_nft) = db.collection::<Asset>()
.expect("Failed to get Asset collection")
.set(&nft_asset)
.expect("Failed to store NFT asset");
// Add assets to the account
account = updated_account.add_asset(token_id);
account = account.add_asset(nft_id);
// Update the account in the database
let (_, updated_account) = db.collection::<Account>()
.expect("Failed to get Account collection")
.set(&account)
.expect("Failed to store updated account");
// Create a listing for the NFT
let listing = Listing::new()
.seller_id(account_id)
.asset_id(nft_id)
.price(0.5)
.currency("ETH")
.listing_type(ListingType::Auction)
.title("Rare Herocode NFT".to_string())
.description("One of a kind digital collectible".to_string())
.image_url(Some("hcttps://example.com/nft/1.png".to_string()))
.add_tag("rare".to_string())
.add_tag("collectible".to_string());
// Store the listing in the database
let (listing_id, updated_listing) = db.collection::<Listing>()
.expect("Failed to get Listing collection")
.set(&listing)
.expect("Failed to store listing");
println!("Mock database seeded with finance data:");
println!(" - Added account: {} (ID: {})", updated_account.name, updated_account.base_data.id);
println!(" - Added token asset: {} (ID: {})", updated_token.name, updated_token.base_data.id);
println!(" - Added NFT asset: {} (ID: {})", updated_nft.name, updated_nft.base_data.id);
println!(" - Added listing: {} (ID: {})", updated_listing.title, updated_listing.base_data.id);
}

View File

@ -0,0 +1,82 @@
/// Seed the mock database with finance data
fn seed_finance_data(db: Arc<OurDB>) {
// Create a user account
let mut account = Account::new()
.name("Demo Account")
.user_id(1)
.description("Demo trading account")
.ledger("ethereum")
.address("0x1234567890abcdef1234567890abcdef12345678")
.pubkey("0xabcdef1234567890abcdef1234567890abcdef12");
// Store the account in the database
let (account_id, updated_account) = db.collection::<Account>()
.expect("Failed to get Account collection")
.set(&account)
.expect("Failed to store account");
// Create an ERC20 token asset
let token_asset = Asset::new()
.name("HERO Token")
.description("Herocode governance token")
.amount(1000.0)
.address("0x9876543210abcdef9876543210abcdef98765432")
.asset_type(AssetType::Erc20)
.decimals(18);
// Store the token asset in the database
let (token_id, updated_token) = db.collection::<Asset>()
.expect("Failed to get Asset collection")
.set(&token_asset)
.expect("Failed to store token asset");
// Create an NFT asset
let nft_asset = Asset::new()
.name("Herocode #1")
.description("Unique digital collectible")
.amount(1.0)
.address("0xabcdef1234567890abcdef1234567890abcdef12")
.asset_type(AssetType::Erc721)
.decimals(0);
// Store the NFT asset in the database
let (nft_id, updated_nft) = db.collection::<Asset>()
.expect("Failed to get Asset collection")
.set(&nft_asset)
.expect("Failed to store NFT asset");
// Add assets to the account
account = updated_account.add_asset(token_id);
account = account.add_asset(nft_id);
// Update the account in the database
let (_, updated_account) = db.collection::<Account>()
.expect("Failed to get Account collection")
.set(&account)
.expect("Failed to store updated account");
// Create a listing for the NFT
let listing = Listing::new()
.seller_id(account_id)
.asset_id(nft_id)
.price(0.5)
.currency("ETH")
.listing_type(ListingType::Auction)
.title(Some("Rare Herocode NFT".to_string()))
.description(Some("One of a kind digital collectible".to_string()))
.image_url(Some("https://example.com/nft/1.png".to_string()))
.add_tag("rare".to_string())
.add_tag("collectible".to_string());
// Store the listing in the database
let (listing_id, updated_listing) = db.collection::<Listing>()
.expect("Failed to get Listing collection")
.set(&listing)
.expect("Failed to store listing");
println!("Mock database seeded with finance data:");
println!(" - Added account: {} (ID: {})", updated_account.name, updated_account.base_data.id);
println!(" - Added token asset: {} (ID: {})", updated_token.name, updated_token.base_data.id);
println!(" - Added NFT asset: {} (ID: {})", updated_nft.name, updated_nft.base_data.id);
println!(" - Added listing: {} (ID: {})", updated_listing.title.unwrap_or_default(), updated_listing.base_data.id);
}

1
rhai_client/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

17
rhai_client/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "rhai_client"
version = "0.1.0"
edition = "2021"
[dependencies]
redis = { version = "0.25.0", features = ["tokio-comp"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.6", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
log = "0.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } # For async main in examples, and general async
[dev-dependencies] # For examples later
env_logger = "0.10"
rhai = "1.18.0" # For examples that might need to show engine setup

212
rhai_client/src/lib.rs Normal file
View File

@ -0,0 +1,212 @@
use chrono::Utc;
use log::{debug, info, warn, error}; // Added error
use redis::AsyncCommands;
use tokio::time::{sleep, Instant}; // For polling with timeout
use std::time::Duration;
use serde::{Deserialize, Serialize};
use serde_json::Value; // For client_rpc_id, though not directly used by this client's submit method
use uuid::Uuid;
const REDIS_TASK_DETAILS_PREFIX: &str = "rhai_task_details:";
const REDIS_QUEUE_PREFIX: &str = "rhai_tasks:";
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RhaiTaskDetails {
pub script: String,
pub status: String, // "pending", "processing", "completed", "error"
#[serde(rename = "clientRpcId")]
pub client_rpc_id: Option<Value>, // Kept for compatibility with worker/server, but optional for client
pub output: Option<String>,
pub error: Option<String>, // Renamed from error_message for consistency
#[serde(rename = "createdAt")]
pub created_at: chrono::DateTime<chrono::Utc>,
#[serde(rename = "updatedAt")]
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug)]
pub enum RhaiClientError {
RedisError(redis::RedisError),
SerializationError(serde_json::Error),
Timeout(String), // task_id that timed out
TaskNotFound(String), // task_id not found after submission (should be rare)
}
impl From<redis::RedisError> for RhaiClientError {
fn from(err: redis::RedisError) -> Self {
RhaiClientError::RedisError(err)
}
}
impl From<serde_json::Error> for RhaiClientError {
fn from(err: serde_json::Error) -> Self {
RhaiClientError::SerializationError(err)
}
}
impl std::fmt::Display for RhaiClientError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RhaiClientError::RedisError(e) => write!(f, "Redis error: {}", e),
RhaiClientError::SerializationError(e) => write!(f, "Serialization error: {}", e),
RhaiClientError::Timeout(task_id) => write!(f, "Timeout waiting for task {} to complete", task_id),
RhaiClientError::TaskNotFound(task_id) => write!(f, "Task {} not found after submission", task_id),
}
}
}
impl std::error::Error for RhaiClientError {}
pub struct RhaiClient {
redis_client: redis::Client,
}
impl RhaiClient {
pub fn new(redis_url: &str) -> Result<Self, RhaiClientError> {
let client = redis::Client::open(redis_url)?;
Ok(Self { redis_client: client })
}
pub async fn submit_script(
&self,
circle_name: &str,
script: String,
client_rpc_id: Option<Value>, // Optional: if the caller has an RPC ID to associate
) -> Result<String, RhaiClientError> {
let mut conn = self.redis_client.get_multiplexed_async_connection().await?;
let task_id = Uuid::new_v4().to_string();
let now = Utc::now();
let task_details = RhaiTaskDetails {
script,
status: "pending".to_string(),
client_rpc_id,
output: None,
error: None,
created_at: now,
updated_at: now,
};
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
let queue_key = format!("{}{}", REDIS_QUEUE_PREFIX, circle_name.replace(" ", "_").to_lowercase());
debug!(
"Submitting task_id: {} for circle: {} to queue: {}. Details: {:?}",
task_id, circle_name, queue_key, task_details
);
// Using HSET_MULTIPLE for efficiency if redis-rs supports it directly for struct fields.
// Otherwise, individual HSETs are fine.
// For simplicity and directness with redis-rs async, individual HSETs are used here.
conn.hset::<_, _, _, ()>(&task_key, "script", &task_details.script).await?;
conn.hset::<_, _, _, ()>(&task_key, "status", &task_details.status).await?;
if let Some(rpc_id_val) = &task_details.client_rpc_id {
conn.hset::<_, _, _, ()>(&task_key, "clientRpcId", serde_json::to_string(rpc_id_val)?).await?;
} else {
// Ensure the field exists even if null, or decide if it should be omitted
conn.hset::<_, _, _, ()>(&task_key, "clientRpcId", Value::Null.to_string()).await?;
}
conn.hset::<_, _, _, ()>(&task_key, "createdAt", task_details.created_at.to_rfc3339()).await?;
conn.hset::<_, _, _, ()>(&task_key, "updatedAt", task_details.updated_at.to_rfc3339()).await?;
// output and error fields are initially None, so they might not be set here or set as empty strings/null
conn.lpush::<_, _, ()>(&queue_key, &task_id).await?;
Ok(task_id)
}
// Optional: A method to check task status, similar to what circle_server_ws polling does.
// This could be useful for a client that wants to poll for results itself.
pub async fn get_task_status(&self, task_id: &str) -> Result<Option<RhaiTaskDetails>, RhaiClientError> {
let mut conn = self.redis_client.get_multiplexed_async_connection().await?;
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
let result_map: Option<std::collections::HashMap<String, String>> = conn.hgetall(&task_key).await?;
match result_map {
Some(map) => {
// Reconstruct RhaiTaskDetails from HashMap
// This is a simplified reconstruction; ensure all fields are handled robustly
let details = RhaiTaskDetails {
script: map.get("script").cloned().unwrap_or_default(),
status: map.get("status").cloned().unwrap_or_default(),
client_rpc_id: map.get("clientRpcId")
.and_then(|s| serde_json::from_str(s).ok())
.or(Some(Value::Null)), // Default to Value::Null if missing or parse error
output: map.get("output").cloned(),
error: map.get("error").cloned(),
created_at: map.get("createdAt")
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(Utc::now), // Provide a default
updated_at: map.get("updatedAt")
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(Utc::now), // Provide a default
};
Ok(Some(details))
}
None => Ok(None),
}
}
pub async fn submit_script_and_await_result(
&self,
circle_name: &str,
script: String,
client_rpc_id: Option<Value>,
timeout: Duration,
poll_interval: Duration,
) -> Result<RhaiTaskDetails, RhaiClientError> {
let task_id = self.submit_script(circle_name, script, client_rpc_id).await?;
info!("Task {} submitted. Polling for result with timeout {:?}...", task_id, timeout);
let start_time = Instant::now();
loop {
if start_time.elapsed() > timeout {
warn!("Timeout waiting for task {}", task_id);
return Err(RhaiClientError::Timeout(task_id.clone()));
}
match self.get_task_status(&task_id).await {
Ok(Some(details)) => {
debug!("Polled task {}: status = {}", task_id, details.status);
if details.status == "completed" || details.status == "error" {
info!("Task {} finished with status: {}", task_id, details.status);
return Ok(details);
}
// else status is "pending" or "processing", continue polling
}
Ok(None) => {
// This case should ideally not happen if submit_script succeeded and worker is running,
// unless the task details were manually deleted from Redis.
warn!("Task {} not found during polling. This might indicate an issue.", task_id);
// Depending on desired robustness, could retry a few times or return an error immediately.
// For now, let it continue polling up to timeout, or return a specific error.
// If it persists, it's effectively a timeout or a lost task.
// Let's consider it a lost task if it's not found after a short while post-submission.
if start_time.elapsed() > Duration::from_secs(5) { // Arbitrary short duration
return Err(RhaiClientError::TaskNotFound(task_id.clone()));
}
}
Err(e) => {
// Log error but continue polling unless it's a critical Redis error
error!("Error polling task {}: {}. Will retry.", task_id, e);
}
}
sleep(poll_interval).await;
}
}
}
#[cfg(test)]
mod tests {
// use super::*;
// Basic tests can be added later, especially once examples are in place.
// For now, ensuring it compiles is the priority.
#[test]
fn it_compiles() {
assert_eq!(2 + 2, 4);
}
}

290
rhai_macros_derive/Cargo.lock generated Normal file
View File

@ -0,0 +1,290 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"const-random",
"getrandom 0.3.3",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[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.16",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[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.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
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 = "rhai"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
dependencies = [
"ahash",
"bitflags",
"instant",
"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_macros_derive"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"rhai",
"syn",
]
[[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 = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
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 = "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 = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[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 = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -0,0 +1,15 @@
[package]
name = "rhai_macros_derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full", "extra-traits"] }
quote = "1.0"
proc-macro2 = "1.0"
# We might need rhai types for some advanced scenarios, but start without it
# rhai = { version = "1.21.0" }

View File

@ -0,0 +1,200 @@
# Rhai Macros Derive Crate
This crate provides procedural macros to simplify the integration of custom Rust structs with the Rhai scripting engine, specifically for converting structs to and from `rhai::Map` objects. It is intended to be used alongside the `rhai_wrapper` crate.
## Provided Macros
- `#[derive(ToRhaiMap)]`
- `#[derive(FromRhaiMap)]`
## Dependencies
Make sure this crate is included in your `Cargo.toml` dependencies, typically as a local path dependency if working within the `rhaj` project:
```toml
[dependencies]
rhai_macros_derive = { path = "../rhai_macros_derive" }
# ... other dependencies
```
And `rhai` itself:
```toml
[dependencies]
rhai = "<version>" # e.g., "1.16.0"
```
## `#[derive(ToRhaiMap)]`
This macro automatically generates an implementation of a `to_rhai_map(&self) -> rhai::Map` method for your struct. This method converts an instance of your struct into a `rhai::Map`, which can then be easily used within Rhai scripts as an object map.
### Usage
```rust
use rhai_macros_derive::ToRhaiMap;
use rhai::{INT, FLOAT, Map};
// Forward declaration for Point if used in Vec<Point>
// Typically Point would also derive ToRhaiMap and FromRhaiMap
#[derive(Debug, Clone, PartialEq, ToRhaiMap)] // Assuming Point also derives ToRhaiMap
struct Point {
x: INT,
y: INT,
}
impl Point { // Minimal stub for example if not fully defined elsewhere
fn to_rhai_map(&self) -> Map {
let mut map = Map::new();
map.insert("x".into(), self.x.into());
map.insert("y".into(), self.y.into());
map
}
}
#[derive(ToRhaiMap)]
struct MyStruct {
id: INT,
name: String,
is_active: bool,
score: FLOAT,
position: Point, // Nested struct
tags: Vec<String>, // Vec of primitives
history: Vec<Point>, // Vec of custom structs
}
fn main() {
let p = Point { x: 10, y: 20 };
let my_instance = MyStruct {
id: 1,
name: "Test".to_string(),
is_active: true,
score: 99.5,
position: p.clone(),
tags: vec!["alpha".to_string(), "beta".to_string()],
history: vec![Point { x: 1, y: 2 }, Point { x: 3, y: 4}],
};
let rhai_map = my_instance.to_rhai_map();
assert_eq!(rhai_map.get("id").unwrap().as_int().unwrap(), 1);
assert_eq!(rhai_map.get("name").unwrap().clone().into_string().unwrap(), "Test");
assert_eq!(rhai_map.get("is_active").unwrap().as_bool().unwrap(), true);
assert_eq!(rhai_map.get("score").unwrap().as_float().unwrap(), 99.5);
let pos_map = rhai_map.get("position").unwrap().clone().try_cast::<Map>().unwrap();
assert_eq!(pos_map.get("x").unwrap().as_int().unwrap(), 10);
let tags_array = rhai_map.get("tags").unwrap().clone().try_cast::<rhai::Array>().unwrap();
assert_eq!(tags_array.len(), 2);
assert_eq!(tags_array[0].clone().into_string().unwrap(), "alpha");
let history_array = rhai_map.get("history").unwrap().clone().try_cast::<rhai::Array>().unwrap();
assert_eq!(history_array.len(), 2);
let hist_p1_map = history_array[0].clone().try_cast::<Map>().unwrap();
assert_eq!(hist_p1_map.get("x").unwrap().as_int().unwrap(), 1);
}
```
### How it works:
- **Primitive Types**: Fields like `INT`, `i64`, `String`, `FLOAT`, `f64`, and `bool` are cloned and converted into their `rhai::Dynamic` equivalents.
- **Nested Structs**: If a field is another struct (e.g., `position: Point`), that struct must also implement `to_rhai_map()`. The macro will call `self.field_name.to_rhai_map()` for that field.
- **`Vec<T>` Fields**:
- If `T` is a primitive type (e.g., `Vec<String>`), each element is cloned and converted to `rhai::Dynamic`, then collected into a `rhai::Array`.
- If `T` is a custom struct (e.g., `Vec<Point>`), `item.to_rhai_map()` is called for each element, and the resulting `rhai::Map`s are collected into a `rhai::Array`.
## `#[derive(FromRhaiMap)]`
This macro automatically generates an implementation of `from_rhai_map(map: rhai::Map) -> Result<Self, String>` for your struct. This method attempts to construct an instance of your struct from a `rhai::Map`.
### Usage
```rust
use rhai_macros_derive::FromRhaiMap;
use rhai::{INT, FLOAT, Map, Array, Dynamic};
// Assuming Point also derives FromRhaiMap and has a from_rhai_map method
#[derive(Debug, Clone, PartialEq, FromRhaiMap)]
struct Point {
x: INT,
y: INT,
}
impl Point { // Minimal stub for example
fn from_rhai_map(mut map: Map) -> Result<Self, String> {
Ok(Point {
x: map.get("x").and_then(|d| d.as_int().ok()).ok_or("x missing")?,
y: map.get("y").and_then(|d| d.as_int().ok()).ok_or("y missing")?,
})
}
}
#[derive(FromRhaiMap, Debug, PartialEq)] // Added Debug, PartialEq for assert
struct MyStruct {
id: INT,
name: String,
is_active: bool,
score: FLOAT,
position: Point, // Nested struct
tags: Vec<String>, // Vec of primitives
history: Vec<Point>, // Vec of custom structs
}
fn main() {
let mut map = Map::new();
map.insert("id".into(), (1 as INT).into());
map.insert("name".into(), "Test".to_string().into());
map.insert("is_active".into(), true.into());
map.insert("score".into(), (99.5 as FLOAT).into());
let mut pos_map = Map::new();
pos_map.insert("x".into(), (10 as INT).into());
pos_map.insert("y".into(), (20 as INT).into());
map.insert("position".into(), pos_map.into());
let tags_array: Array = vec![Dynamic::from("alpha".to_string()), Dynamic::from("beta".to_string())];
map.insert("tags".into(), tags_array.into());
let mut hist_p1_map = Map::new();
hist_p1_map.insert("x".into(), (1 as INT).into());
hist_p1_map.insert("y".into(), (2 as INT).into());
let mut hist_p2_map = Map::new();
hist_p2_map.insert("x".into(), (3 as INT).into());
hist_p2_map.insert("y".into(), (4 as INT).into());
let history_array: Array = vec![Dynamic::from(hist_p1_map), Dynamic::from(hist_p2_map)];
map.insert("history".into(), history_array.into());
let my_instance = MyStruct::from_rhai_map(map).unwrap();
assert_eq!(my_instance.id, 1);
assert_eq!(my_instance.name, "Test");
assert_eq!(my_instance.position, Point { x: 10, y: 20 });
assert_eq!(my_instance.tags, vec!["alpha".to_string(), "beta".to_string()]);
assert_eq!(my_instance.history, vec![Point { x: 1, y: 2 }, Point { x: 3, y: 4}]);
}
```
### How it works:
- **Primitive Types**: For fields like `INT`, `String`, etc., the macro attempts to retrieve the value from the input `rhai::Map` by its key (field name) and convert it to the expected Rust type (e.g., using `as_int()`, `into_string()`).
- **Nested Structs**: If a field is another struct, it retrieves the corresponding value as a `rhai::Dynamic`, tries to cast it to `rhai::Map`, and then calls `NestedStructType::from_rhai_map()` on that sub-map.
- **`Vec<T>` Fields**:
- Retrieves the value as `rhai::Dynamic`, casts it to `rhai::Array`.
- If `T` is a primitive type, it iterates the array, converting each `rhai::Dynamic` element to `T`.
- If `T` is a custom struct, it iterates the array, casting each `rhai::Dynamic` element to `rhai::Map`, and then calls `T::from_rhai_map()` on each sub-map.
- **Error Handling**: If a field is missing, or if a type conversion fails, `from_rhai_map` will return an `Err(String)` describing the issue.
## Combining with `rhai::CustomType`
For your structs to be fully usable as custom types within Rhai (e.g., to be registered with `engine.build_type::<MyStruct>()`), they should also typically derive `rhai::CustomType`, `Clone`, and `Debug`:
```rust
use rhai_macros_derive::{ToRhaiMap, FromRhaiMap};
use rhai::CustomType;
#[derive(CustomType, ToRhaiMap, FromRhaiMap, Clone, Debug, PartialEq)]
struct MyStruct {
// ... fields
}
```
This setup allows seamless conversion and manipulation of your Rust structs within Rhai scripts.

View File

@ -0,0 +1,429 @@
// rhai_macros_derive/src/lib.rs
// We will add our derive macro implementations here.
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::{quote, format_ident, quote_spanned};
use syn::{parse_macro_input, Type, ItemFn, PathArguments, GenericArgument, DeriveInput, Data, LitStr, FnArg, Pat, ReturnType};
// Old ToRhaiMap and FromRhaiMap definitions will be removed from here.
// The export_fn macro definition starts after this.
// Trait definitions removed from here as proc-crate crates cannot export them.
// They should be defined in a regular library crate (e.g., rhai_wrapper or a new rhai_traits crate).
// Helper functions moved to module level
fn get_option_inner_type(ty: &Type) -> (bool, Option<&Type>) {
if let Type::Path(type_path) = ty {
if type_path.path.segments.len() == 1 && type_path.path.segments.first().unwrap().ident == "Option" {
if let PathArguments::AngleBracketed(params) = &type_path.path.segments.first().unwrap().arguments {
if params.args.len() == 1 {
if let GenericArgument::Type(inner_ty) = params.args.first().unwrap() {
return (true, Some(inner_ty));
}
}
}
}
}
(false, None)
}
fn get_vec_inner_type(ty: &Type) -> (bool, Option<&Type>) {
if let Type::Path(type_path) = ty {
if type_path.path.segments.len() == 1 && type_path.path.segments.first().unwrap().ident == "Vec" {
if let PathArguments::AngleBracketed(params) = &type_path.path.segments.first().unwrap().arguments {
if params.args.len() == 1 {
if let GenericArgument::Type(inner_ty) = params.args.first().unwrap() {
return (true, Some(inner_ty));
}
}
}
}
}
(false, None)
}
fn get_simple_type_str(ty: &Type) -> String {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident.to_string();
}
}
// Fallback, might need refinement for more complex paths like std::string::String
quote!(#ty).to_string().replace(' ', "").replace("::", "_")
}
fn is_primitive_type_str(simple_type_str: &str) -> bool {
["String", "INT", "i64", "FLOAT", "f64", "bool"].contains(&simple_type_str)
}
#[proc_macro_attribute]
pub fn export_fn(_attr: TokenStream, item: TokenStream) -> TokenStream {
let func = parse_macro_input!(item as ItemFn);
let fn_vis = &func.vis;
let fn_name = &func.sig.ident;
let fn_name_str = fn_name.to_string();
let wrapper_fn_name = format_ident!("{}_rhai_wrapper", fn_name);
let mut rhai_arg_names = Vec::new(); // Names for args in the wrapper's signature (arg0, arg1, ...)
let mut rhai_arg_types = Vec::new(); // Types for args in the wrapper's signature (Dynamic)
let mut converted_arg_definitions = Vec::new(); // `let __conv_arg = arg0.try_cast().ok_or_else(...) ?;`
let mut call_arg_names = Vec::new(); // Names of converted args to pass to original func (__conv_arg)
for (i, input) in func.sig.inputs.iter().enumerate() {
if let FnArg::Typed(pat_type) = input {
if let Pat::Ident(pat_ident) = &*pat_type.pat {
let original_arg_name_for_err_msg = &pat_ident.ident; // For cleaner error messages
let rhai_arg_name = format_ident!("arg{}", i);
rhai_arg_names.push(rhai_arg_name.clone());
rhai_arg_types.push(quote! { ::rhai::Dynamic });
let original_arg_ty = &pat_type.ty;
let converted_arg_name = format_ident!("__conv_{}", rhai_arg_name);
converted_arg_definitions.push(quote! {
let #converted_arg_name = #rhai_arg_name.clone().try_cast::<#original_arg_ty>().ok_or_else(|| {
Box::new(::rhai::EvalAltResult::ErrorMismatchDataType(
format!("expected type '{}' for argument '{}' in function '{}'",
stringify!(#original_arg_ty),
stringify!(#original_arg_name_for_err_msg),
#fn_name_str),
#rhai_arg_name.type_name().to_string(),
::rhai::Position::NONE
))
})?;
});
call_arg_names.push(quote! { #converted_arg_name });
} else {
panic!("Unsupported argument pattern in export_fn");
}
} else {
panic!("Unsupported 'self' argument in export_fn");
}
}
let return_type_ast = match &func.sig.output {
ReturnType::Default => quote! { () },
ReturnType::Type(_, ty) => quote! { #ty },
};
let success_return_logic = match &func.sig.output {
ReturnType::Default => quote! { Ok(()) },
ReturnType::Type(_, _) => quote! { Ok(result) },
};
let gen = quote! {
#func // Keep the original function
#fn_vis fn #wrapper_fn_name(#(#rhai_arg_names: #rhai_arg_types),*) -> Result<#return_type_ast, Box<::rhai::EvalAltResult>> {
#(#converted_arg_definitions)*
let result = #fn_name(#(#call_arg_names),*);
#success_return_logic
}
};
// For debugging the generated code
// e.g., panic!(gen.to_string());
TokenStream::from(gen)
}
#[proc_macro_derive(FromRhaiMap, attributes(rhai_map_field))]
pub fn derive_from_rhai_map(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let fields_data = match &input.data {
Data::Struct(syn::DataStruct { fields: syn::Fields::Named(fields), .. }) => &fields.named,
_ => panic!("FromRhaiMapDerive only supports structs with named fields"),
};
let mut field_value_declarations = Vec::new();
let mut struct_field_assignments = Vec::new();
for field in fields_data.iter() {
let field_name_ident = field.ident.as_ref().unwrap();
let field_name_str = field_name_ident.to_string();
let field_name_str_lit = LitStr::new(&field_name_str, field_name_ident.span());
let field_ty = &field.ty;
let field_value_ident = format_ident!("__field_val_{}", field_name_str);
let (is_option, option_inner_ty_opt) = get_option_inner_type(field_ty);
let type_for_vec_check = if is_option { option_inner_ty_opt.unwrap() } else { field_ty };
let (is_vec, vec_inner_ty_opt) = get_vec_inner_type(type_for_vec_check);
let assignment_code = if is_option {
let option_inner_ty = option_inner_ty_opt.expect("Option inner type not found");
let option_inner_ty_str = get_simple_type_str(option_inner_ty);
let (is_vec_in_option, vec_inner_ty_in_option_opt) = get_vec_inner_type(option_inner_ty);
if is_vec_in_option {
let vec_element_ty = vec_inner_ty_in_option_opt.expect("Vec inner type in Option not found");
let vec_element_ty_str = get_simple_type_str(vec_element_ty);
let element_conversion_logic = if is_primitive_type_str(&vec_element_ty_str) {
quote! { // Option<Vec<Primitive>>
let el_for_err_type = el.clone();
match el.try_cast::<#vec_element_ty>() {
Some(val) => Ok(val),
None => Err(format!("Array element expected type {}, but received type {}.",
stringify!(#vec_element_ty), el_for_err_type.type_name()
))
}
}
} else { // Option<Vec<CustomStruct>>
quote! {
let el_for_err_type = el.clone();
el.try_cast::<::rhai::Map>()
.ok_or_else(move || format!("Array element expected a Rhai map for type {}, but received type {}.",
stringify!(#vec_element_ty), el_for_err_type.type_name()))
.and_then(#vec_element_ty::from_rhai_map)
}
};
quote! {
match map.get(#field_name_str_lit).cloned() { // cloned is on Option<Dynamic>, not Dynamic itself
Some(dynamic_val_option_vec) if !dynamic_val_option_vec.is_unit() => {
let dyn_val_array_clone_for_type = dynamic_val_option_vec.clone();
let actual_type_name = dyn_val_array_clone_for_type.type_name();
match dynamic_val_option_vec.try_cast::<::rhai::Array>() {
Some(arr) => {
arr.into_iter().map(|el| { #element_conversion_logic }).collect::<Result<Vec<_>, String>>().map(Some)
},
None => Err(format!(
"Field '{}' (Option<Vec<{}>) expected an array, but received type {}.",
#field_name_str_lit,
#vec_element_ty_str,
actual_type_name
))
}
},
_ => Ok(None) // Field not present or is '()' for Option, so map to None
}?
}
} else if is_primitive_type_str(&option_inner_ty_str) { // Option<Primitive>
quote! {
map.get(#field_name_str_lit).and_then(|val_opt_prim_ref| {
if val_opt_prim_ref.is_unit() { return None; } // Explicitly handle () as None
let val_opt_prim_for_cast = val_opt_prim_ref.clone(); // Clone for try_cast
let val_opt_prim_for_err_type = val_opt_prim_ref.clone(); // Clone for error type_name
match val_opt_prim_for_cast.try_cast::<#option_inner_ty>() {
Some(v) => Some(Ok(v)),
None => Some(Err(format!("Field '{}' expected Option<{}>, but received incompatible type {}.",
#field_name_str_lit, stringify!(#option_inner_ty), val_opt_prim_for_err_type.type_name())))
}
}).transpose()?
}
} else { // Option<CustomStruct>
quote! {
map.get(#field_name_str_lit).and_then(|val_opt_custom_ref| {
if val_opt_custom_ref.is_unit() { return None; } // Explicitly handle () as None
let val_opt_custom_for_cast = val_opt_custom_ref.clone(); // Clone for try_cast Map
let val_opt_custom_for_err_type = val_opt_custom_ref.clone(); // Clone for error message type_name
match val_opt_custom_for_cast.try_cast::<::rhai::Map>() {
Some(inner_map) => Some(#option_inner_ty::from_rhai_map(inner_map)),
None => Some(Err(format!(
"Field '{}' expected a Rhai map for type {}, but received type {}.",
#field_name_str_lit, stringify!(#option_inner_ty), val_opt_custom_for_err_type.type_name()
)))
}
}).transpose()
}
}
} else if is_vec { // Direct Vec<T>
let vec_element_ty = vec_inner_ty_opt.expect("Vec inner T not found");
let vec_element_ty_str = get_simple_type_str(vec_element_ty);
let element_conversion_logic = if is_primitive_type_str(&vec_element_ty_str) {
// Vec<Primitive>
quote! {
let el_for_err_type = el.clone();
match el.try_cast::<#vec_element_ty>() {
Some(val) => Ok(val),
None => Err(format!("Array element expected type {}, but received type {}.",
stringify!(#vec_element_ty), el_for_err_type.type_name()
))
}
}
} else {
// Vec<CustomStruct>
quote! {
let el_for_err_type = el.clone();
el.try_cast::<::rhai::Map>()
.ok_or_else(move || format!("Array element expected a Rhai map for type {}, but received type {}.",
stringify!(#vec_element_ty), el_for_err_type.type_name()))
.and_then(#vec_element_ty::from_rhai_map)
}
};
quote! {
{
let arr_dynamic_ref = map.get(#field_name_str_lit)
.ok_or_else(|| format!("Field '{}' (Vec<{}>) not found in Rhai map.", #field_name_str_lit, #vec_element_ty_str))?;
let arr_dynamic_val_for_cast = arr_dynamic_ref.clone(); // Clone for try_cast
let actual_type_name = arr_dynamic_val_for_cast.type_name();
arr_dynamic_val_for_cast.try_cast::<::rhai::Array>()
.ok_or_else({
let field_name_str_lit_for_err = #field_name_str_lit;
let vec_element_ty_str_for_err = #vec_element_ty_str;
move || format!("Field '{}' (Vec<{}>) expected an array, but received type {}.",
field_name_str_lit_for_err, vec_element_ty_str_for_err, actual_type_name)
})?
.into_iter()
.map(|el| { #element_conversion_logic }).collect::<Result<Vec<_>, String>>()?
}
}
} else if is_primitive_type_str(&get_simple_type_str(field_ty)) { // Direct Primitive
quote! {
{
let dynamic_ref = map.get(#field_name_str_lit)
.ok_or_else(|| format!("Field '{}' (type {}) not found in Rhai map.", #field_name_str_lit, stringify!(#field_ty)))?;
let dynamic_val_for_cast = dynamic_ref.clone(); // Clone for try_cast
let dynamic_val_for_error_msg = dynamic_ref.clone(); // Clone for error message type_name
dynamic_val_for_cast.try_cast::<#field_ty>()
.ok_or_else(move || format!("Field '{}' expected type {}, but received incompatible type {}.",
#field_name_str_lit, stringify!(#field_ty), dynamic_val_for_error_msg.type_name()))?
}
}
} else { // Direct CustomStruct
quote! {
{
let field_str = #field_name_str_lit;
let dynamic_ref = map.get(field_str)
.ok_or_else(|| format!("Field '{}' (type {}) not found in Rhai map.", field_str, stringify!(#field_ty)))?;
let dynamic_val_for_cast = dynamic_ref.clone(); // Clone for try_cast to Map
let actual_type_name_val = dynamic_ref.clone(); // Clone for error message type_name
match dynamic_val_for_cast.try_cast::<::rhai::Map>() {
Some(inner_map) => #field_ty::from_rhai_map(inner_map),
None => Err(format!(
"Field '{}' expected a Rhai map for type {}, but received type {}.",
field_str, stringify!(#field_ty), actual_type_name_val.type_name()
))
}?
}
}
};
field_value_declarations.push(quote! { let #field_value_ident = #assignment_code; });
struct_field_assignments.push(quote_spanned!(field_name_ident.span()=> #field_name_ident: #field_value_ident));
}
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let gen = quote! {
impl #impl_generics FromRhaiMap for #name #ty_generics #where_clause {
fn from_rhai_map(map: ::rhai::Map) -> Result<Self, String> {
#(#field_value_declarations)*
Ok(Self {
#(#struct_field_assignments),*
})
}
}
};
proc_macro::TokenStream::from(gen)
}
#[proc_macro_derive(ToRhaiMap, attributes(rhai_map_field))]
pub fn derive_to_rhai_map(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = syn::parse_macro_input!(input as syn::DeriveInput);
let name = &ast.ident;
let fields_data = match &ast.data {
Data::Struct(syn::DataStruct { fields: syn::Fields::Named(fields), .. }) => &fields.named,
_ => panic!("ToRhaiMapDerive only supports structs with named fields"),
};
let field_insertions = fields_data.iter().map(|field| {
let field_name_ident = field.ident.as_ref().unwrap();
let field_name_str = field_name_ident.to_string();
let field_ty = &field.ty;
let (is_option, option_inner_ty_opt) = get_option_inner_type(field_ty);
if is_option {
let option_inner_ty = option_inner_ty_opt.expect("Option inner type not found");
let option_inner_ty_str = get_simple_type_str(option_inner_ty);
let (is_vec_in_option, vec_inner_ty_in_option_opt) = get_vec_inner_type(option_inner_ty);
if is_vec_in_option {
let vec_element_ty = vec_inner_ty_in_option_opt.expect("Vec inner type in Option not found");
let vec_element_ty_str = get_simple_type_str(vec_element_ty);
if is_primitive_type_str(&vec_element_ty_str) { // Option<Vec<Primitive>>
quote! {
if let Some(ref vec_val) = self.#field_name_ident {
let rhai_array: ::rhai::Array = vec_val.iter().map(|item| item.clone().into()).collect();
map.insert(#field_name_str.into(), ::rhai::Dynamic::from(rhai_array));
} else {
map.insert(#field_name_str.into(), ::rhai::Dynamic::UNIT);
}
}
} else { // Option<Vec<CustomStruct>>
quote! {
if let Some(ref vec_val) = self.#field_name_ident {
let rhai_array: ::rhai::Array = vec_val.iter().map(|item| ::rhai::Dynamic::from(item.to_rhai_map())).collect();
map.insert(#field_name_str.into(), ::rhai::Dynamic::from(rhai_array));
} else {
map.insert(#field_name_str.into(), ::rhai::Dynamic::UNIT);
}
}
}
} else if is_primitive_type_str(&option_inner_ty_str) { // Option<Primitive>
quote! {
if let Some(ref val) = self.#field_name_ident {
map.insert(#field_name_str.into(), val.clone().into());
} else {
map.insert(#field_name_str.into(), ::rhai::Dynamic::UNIT);
}
}
} else { // Option<CustomStruct>
quote! {
if let Some(ref val) = self.#field_name_ident {
map.insert(#field_name_str.into(), ::rhai::Dynamic::from(val.to_rhai_map()));
} else {
map.insert(#field_name_str.into(), ::rhai::Dynamic::UNIT);
}
}
}
} else {
// Not an Option, could be direct Vec<T>, direct CustomStruct, or direct Primitive
let (is_vec, vec_inner_ty_opt) = get_vec_inner_type(field_ty);
if is_vec {
let vec_element_ty = vec_inner_ty_opt.expect("Vec inner type not found");
let vec_element_ty_str = get_simple_type_str(vec_element_ty);
if is_primitive_type_str(&vec_element_ty_str) { // Vec<Primitive>
quote! {
let rhai_array: ::rhai::Array = self.#field_name_ident.iter().map(|item| item.clone().into()).collect();
map.insert(#field_name_str.into(), ::rhai::Dynamic::from(rhai_array));
}
} else { // Vec<CustomStruct>
quote! {
let rhai_array: ::rhai::Array = self.#field_name_ident.iter().map(|item| ::rhai::Dynamic::from(item.to_rhai_map())).collect();
map.insert(#field_name_str.into(), ::rhai::Dynamic::from(rhai_array));
}
}
} else if is_primitive_type_str(&get_simple_type_str(field_ty)) { // Direct Primitive
quote! {
map.insert(#field_name_str.into(), self.#field_name_ident.clone().into());
}
} else { // Direct CustomStruct
quote! {
map.insert(#field_name_str.into(), ::rhai::Dynamic::from(self.#field_name_ident.to_rhai_map()));
}
}
}
});
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let expanded = quote! {
impl #impl_generics ToRhaiMap for #name #ty_generics #where_clause {
fn to_rhai_map(&self) -> ::rhai::Map {
let mut map = ::rhai::Map::new();
#(#field_insertions)*
map
}
}
};
proc_macro::TokenStream::from(expanded)
}

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,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,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 }

1
rhai_worker/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

27
rhai_worker/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "rhai_worker"
version = "0.1.0"
edition = "2021"
[lib]
name = "rhai_worker_lib" # Can be different from package name, or same
path = "src/lib.rs"
[[bin]]
name = "rhai_worker"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
redis = { version = "0.25.0", features = ["tokio-comp"] }
rhai = { version = "1.18.0", features = ["sync", "decimal"] } # Added "decimal" for broader script support
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
log = "0.4"
env_logger = "0.10"
clap = { version = "4.4", features = ["derive"] }
uuid = { version = "1.6", features = ["v4", "serde"] } # Though task_id is string, uuid might be useful
chrono = { version = "0.4", features = ["serde"] }
rhai_client = { path = "../rhai_client" }

View File

@ -0,0 +1,76 @@
use rhai::Engine;
use rhai_client::RhaiClient; // To submit tasks
use rhai_worker_lib::{run_worker_loop, Args as WorkerArgs}; // To run the worker
use std::time::Duration;
use tokio::time::sleep;
// Custom function for Rhai
fn add(a: i64, b: i64) -> i64 {
a + b
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
log::info!("Starting Math Worker Example...");
// 1. Configure and start the Rhai Worker with a custom engine
let mut math_engine = Engine::new();
math_engine.register_fn("add", add);
log::info!("Custom 'add' function registered with Rhai engine for Math Worker.");
let worker_args = WorkerArgs {
redis_url: "redis://127.0.0.1/".to_string(),
circles: vec!["math_circle".to_string()], // Worker listens on a specific queue
};
let worker_args_clone = worker_args.clone(); // Clone for the worker task
tokio::spawn(async move {
log::info!("Math Worker task starting...");
if let Err(e) = run_worker_loop(math_engine, worker_args_clone).await {
log::error!("Math Worker loop failed: {}", e);
}
});
// Give the worker a moment to start and connect
sleep(Duration::from_secs(1)).await;
// 2. Use RhaiClient to submit a script to the "math_circle"
let client = RhaiClient::new("redis://127.0.0.1/")?;
let script_content = r#"
let x = 10;
let y = add(x, 32); // Use the custom registered function
print("Math script: 10 + 32 = " + y);
y // Return the result
"#;
log::info!("Submitting math script to 'math_circle' and awaiting result...");
let timeout_duration = Duration::from_secs(10);
let poll_interval = Duration::from_millis(500);
match client.submit_script_and_await_result(
"math_circle",
script_content.to_string(),
None,
timeout_duration,
poll_interval
).await {
Ok(details) => {
log::info!("Math Worker Example: Task finished. Status: {}, Output: {:?}, Error: {:?}",
details.status, details.output, details.error);
if details.status == "completed" {
assert_eq!(details.output, Some("42".to_string()));
log::info!("Math Worker Example: Assertion for output 42 passed!");
Ok(())
} else {
log::error!("Math Worker Example: Task completed with error: {:?}", details.error);
Err(format!("Task failed with error: {:?}", details.error).into())
}
}
Err(e) => {
log::error!("Math Worker Example: Failed to get task result: {}", e);
Err(e.into())
}
}
}

View File

@ -0,0 +1,76 @@
use rhai::Engine;
use rhai_client::RhaiClient; // To submit tasks
use rhai_worker_lib::{run_worker_loop, Args as WorkerArgs}; // To run the worker
use std::time::Duration;
use tokio::time::sleep;
// Custom function for Rhai
fn reverse_string(s: String) -> String {
s.chars().rev().collect()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
log::info!("Starting String Worker Example...");
// 1. Configure and start the Rhai Worker with a custom engine
let mut string_engine = Engine::new();
string_engine.register_fn("reverse_it", reverse_string);
log::info!("Custom 'reverse_it' function registered with Rhai engine for String Worker.");
let worker_args = WorkerArgs {
redis_url: "redis://127.0.0.1/".to_string(),
circles: vec!["string_circle".to_string()], // Worker listens on a specific queue
};
let worker_args_clone = worker_args.clone();
tokio::spawn(async move {
log::info!("String Worker task starting...");
if let Err(e) = run_worker_loop(string_engine, worker_args_clone).await {
log::error!("String Worker loop failed: {}", e);
}
});
// Give the worker a moment to start and connect
sleep(Duration::from_secs(1)).await;
// 2. Use RhaiClient to submit a script to the "string_circle"
let client = RhaiClient::new("redis://127.0.0.1/")?;
let script_content = r#"
let original = "hello world";
let reversed = reverse_it(original);
print("String script: original = '" + original + "', reversed = '" + reversed + "'");
reversed // Return the result
"#;
log::info!("Submitting string script to 'string_circle' and awaiting result...");
let timeout_duration = Duration::from_secs(10);
let poll_interval = Duration::from_millis(500);
match client.submit_script_and_await_result(
"string_circle",
script_content.to_string(),
None,
timeout_duration,
poll_interval
).await {
Ok(details) => {
log::info!("String Worker Example: Task finished. Status: {}, Output: {:?}, Error: {:?}",
details.status, details.output, details.error);
if details.status == "completed" {
assert_eq!(details.output, Some("\"dlrow olleh\"".to_string())); // Rhai strings include quotes in `debug` format
log::info!("String Worker Example: Assertion for output \"dlrow olleh\" passed!");
Ok(())
} else {
log::error!("String Worker Example: Task completed with error: {:?}", details.error);
Err(format!("Task failed with error: {:?}", details.error).into())
}
}
Err(e) => {
log::error!("String Worker Example: Failed to get task result: {}", e);
Err(e.into())
}
}
}

144
rhai_worker/src/lib.rs Normal file
View File

@ -0,0 +1,144 @@
use chrono::Utc;
use clap::Parser;
use log::{debug, error, info}; // Removed warn as it wasn't used in the loop
use redis::AsyncCommands;
use rhai::{Engine, Scope}; // EvalAltResult is not directly returned by the loop
use std::collections::HashMap; // For hgetall result
// Re-export RhaiTaskDetails from rhai_client if needed by examples,
// or examples can depend on rhai_client directly.
// For now, the worker logic itself just interacts with the hash fields.
const REDIS_TASK_DETAILS_PREFIX: &str = "rhai_task_details:";
const REDIS_QUEUE_PREFIX: &str = "rhai_tasks:";
const BLPOP_TIMEOUT_SECONDS: usize = 5;
#[derive(Parser, Debug, Clone)] // Added Clone for potential use in examples
#[clap(author, version, about, long_about = None)]
pub struct Args {
#[clap(long, value_parser, default_value = "redis://127.0.0.1/")]
pub redis_url: String,
#[clap(short, long, value_parser, required = true, num_args = 1..)]
pub circles: Vec<String>,
}
// This function updates specific fields in the Redis hash.
// It doesn't need to know the full RhaiTaskDetails struct, only the field names.
async fn update_task_status_in_redis(
conn: &mut redis::aio::MultiplexedConnection,
task_id: &str,
status: &str,
output: Option<String>,
error_msg: Option<String>,
) -> redis::RedisResult<()> {
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
let mut updates: Vec<(&str, String)> = vec![
("status", status.to_string()),
("updatedAt", Utc::now().to_rfc3339()), // Ensure this field name matches what rhai_client sets/expects
];
if let Some(out) = output {
updates.push(("output", out)); // Ensure this field name matches
}
if let Some(err) = error_msg {
updates.push(("error", err)); // Ensure this field name matches
}
debug!("Updating task {} in Redis with status: {}, updates: {:?}", task_id, status, updates);
conn.hset_multiple::<_, _, _, ()>(&task_key, &updates).await?;
Ok(())
}
pub async fn run_worker_loop(engine: Engine, args: Args) -> Result<(), Box<dyn std::error::Error>> {
info!("Rhai Worker Loop starting. Connecting to Redis at {}", args.redis_url);
info!("Worker Loop will listen for tasks for circles: {:?}", args.circles);
let redis_client = redis::Client::open(args.redis_url.as_str())?;
let mut redis_conn = redis_client.get_multiplexed_async_connection().await?;
info!("Worker Loop successfully connected to Redis.");
let queue_keys: Vec<String> = args
.circles
.iter()
.map(|name| format!("{}{}", REDIS_QUEUE_PREFIX, name.replace(" ", "_").to_lowercase()))
.collect();
info!("Worker Loop listening on Redis queues: {:?}", queue_keys);
loop {
let response: Option<(String, String)> = redis_conn
.blpop(&queue_keys, BLPOP_TIMEOUT_SECONDS as f64)
.await?;
if let Some((queue_name, task_id)) = response {
info!("Worker Loop received task_id: {} from queue: {}", task_id, queue_name);
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
let task_details_map: Result<HashMap<String, String>, _> =
redis_conn.hgetall(&task_key).await;
match task_details_map {
Ok(details_map) => {
let script_content_opt = details_map.get("script").cloned();
if let Some(script_content) = script_content_opt {
info!("Worker Loop processing task_id: {}. Script: {:.50}...", task_id, script_content);
update_task_status_in_redis(&mut redis_conn, &task_id, "processing", None, None).await?;
let mut scope = Scope::new();
// Examples can show how to pre-populate the scope via the engine or here
match engine.eval_with_scope::<rhai::Dynamic>(&mut scope, &script_content) {
Ok(result) => {
let output_str = format!("{:?}", result);
info!("Worker Loop task {} completed. Output: {}", task_id, output_str);
update_task_status_in_redis(
&mut redis_conn,
&task_id,
"completed",
Some(output_str),
None,
)
.await?;
}
Err(e) => {
let error_str = format!("{:?}", *e); // Dereference EvalAltResult
error!("Worker Loop task {} failed. Error: {}", task_id, error_str);
update_task_status_in_redis(
&mut redis_conn,
&task_id,
"error",
None,
Some(error_str),
)
.await?;
}
}
} else {
error!(
"Worker Loop: Could not find script content for task_id: {} in Redis hash: {}",
task_id, task_key
);
update_task_status_in_redis(
&mut redis_conn,
&task_id,
"error",
None,
Some("Script content not found in Redis hash".to_string()),
)
.await?;
}
}
Err(e) => {
error!(
"Worker Loop: Failed to fetch details for task_id: {} from Redis. Error: {:?}",
task_id, e
);
}
}
} else {
debug!("Worker Loop: BLPOP timed out. No new tasks.");
}
}
// Loop is infinite, Ok(()) is effectively unreachable unless loop breaks
}

18
rhai_worker/src/main.rs Normal file
View File

@ -0,0 +1,18 @@
use rhai::Engine;
use rhai_worker_lib::{run_worker_loop, Args}; // Use the library name defined in Cargo.toml
use clap::Parser; // Required for Args::parse() to be in scope
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let args = Args::parse();
log::info!("Rhai Worker (binary) starting with default engine.");
let engine = Engine::new();
// If specific default configurations are needed for the binary's engine, set them up here.
// For example: engine.set_max_operations(1_000_000);
run_worker_loop(engine, args).await
}

539
rhai_wrapper/Cargo.lock generated Normal file
View File

@ -0,0 +1,539 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"const-random",
"getrandom 0.3.2",
"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 = "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.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
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.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"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.16",
"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 = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
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 = "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 = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[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.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
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 = "rhai"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
dependencies = [
"ahash",
"bitflags",
"instant",
"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_macros_derive"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rhai_wrapper"
version = "0.1.0"
dependencies = [
"chrono",
"rhai",
"rhai_macros_derive",
"serde",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[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 = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
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 = "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 = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[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 = "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 = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

22
rhai_wrapper/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "rhai_wrapper"
version = "0.1.0"
edition = "2021"
description = "A wrapper to make generic Rust functions Rhai-compatible."
[dependencies]
rhai = "1.18.0"
rhai_macros_derive = { path = "../rhai_macros_derive" }
serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
[dev-dependencies]
[[example]]
name = "user_management_example"
path = "examples/user_management_example.rs"
[[example]]
name = "rust_rhai_wrapper_example"
path = "examples/rust_rhai_wrapper_example.rs"

162
rhai_wrapper/README.md Normal file
View File

@ -0,0 +1,162 @@
# Rhai Wrapper Crate
This crate provides utilities to simplify the process of wrapping Rust functions and types for use with the Rhai scripting engine. Its primary component is the `wrap_for_rhai!` macro, which generates the necessary boilerplate to make Rust functions callable from Rhai scripts, including type conversions for arguments and return values.
This crate works in conjunction with the `rhai_macros_derive` crate, which provides `ToRhaiMap` and `FromRhaiMap` derive macros for custom Rust structs.
## Features
- **`wrap_for_rhai!` macro**: Simplifies registering Rust functions with Rhai.
- Support for various function signatures:
- Primitive types (`INT`, `FLOAT`, `bool`, `String`).
- `Option<T>` for optional return values.
- `Vec<PrimitiveType>` (e.g., `Vec<INT>`, `Vec<String>`).
- `Vec<CustomStruct>` where `CustomStruct` implements `FromRhaiMap` (for arguments) and `ToRhaiMap` (for elements in return values).
- Custom structs as direct arguments (if they implement `FromRhaiMap`).
- Custom structs as direct return values (if they implement `ToRhaiMap`).
- Automatic conversion between Rhai's `Dynamic` type and Rust types.
## Dependencies
Ensure you have `rhai` and `rhai_macros_derive` (if using custom structs) in your `Cargo.toml`:
```toml
[dependencies]
rhai = "<version>" # e.g., "1.16.0"
rhai_macros_derive = { path = "../rhai_macros_derive" } # If in the same workspace
# This crate (rhai_wrapper) would also be a local path dependency
# rhai_wrapper = { path = "../rhai_wrapper" }
```
## `wrap_for_rhai!` Macro
The `wrap_for_rhai!` macro is the core of this crate. It takes your Rust function and its type signature (in a specific format) and generates a closure that Rhai can register.
### Basic Usage
```rust
use rhai_wrapper::wrap_for_rhai;
use rhai::{Engine, INT, FLOAT};
// Functions to be wrapped
fn add(a: INT, b: INT) -> INT { a + b }
fn greet(name: String) -> String { format!("Hello, {}!", name) }
fn get_pi() -> FLOAT { 3.14159 }
fn main() {
let mut engine = Engine::new();
// Registering functions using the macro
engine.register_fn("add_rhai", wrap_for_rhai!(add, INT, INT -> INT));
engine.register_fn("greet_rhai", wrap_for_rhai!(greet, String -> String));
engine.register_fn("get_pi_rhai", wrap_for_rhai!(get_pi, -> FLOAT));
let result: INT = engine.eval("add_rhai(10, 32)").unwrap();
assert_eq!(result, 42);
let message: String = engine.eval(r#"greet_rhai("Rhai")"#).unwrap();
assert_eq!(message, "Hello, Rhai!");
}
```
### Supported Signatures & Examples
The macro uses a pattern-matching style to handle different function signatures.
1. **Primitives**: `(INT, INT -> INT)`, `(String -> String)`, `(FLOAT, bool -> String)`
```rust
fn my_func(a: INT, b: String) -> bool { /* ... */ false }
// engine.register_fn("my_func_rhai", wrap_for_rhai!(my_func, INT, String -> bool));
```
2. **No Arguments**: `(-> INT)`
```rust
fn get_answer() -> INT { 42 }
// engine.register_fn("get_answer_rhai", wrap_for_rhai!(get_answer, -> INT));
```
3. **`Option<T>` Return Type**: `(INT -> Option<String>)`
```rust
fn maybe_get_name(id: INT) -> Option<String> {
if id == 1 { Some("Alice".to_string()) } else { None }
}
// engine.register_fn("maybe_get_name_rhai", wrap_for_rhai!(maybe_get_name, INT -> Option<String>));
```
*Rhai will receive `()` (unit/nothing) if the Rust function returns `None`.*
4. **`Vec<Primitive>` Argument**: `(Vec<INT> -> INT)`, `(Vec<String> -> String)`
```rust
fn sum_numbers(numbers: Vec<INT>) -> INT { numbers.iter().sum() }
// engine.register_fn("sum_rhai", wrap_for_rhai!(sum_numbers, Vec<INT> -> INT));
// Rhai script: sum_rhai([1, 2, 3]) -> 6
```
5. **Custom Structs (with `rhai_macros_derive`)**
Assume you have a struct `Point` that derives `ToRhaiMap` and `FromRhaiMap`:
```rust
use rhai_macros_derive::{ToRhaiMap, FromRhaiMap};
use rhai::CustomType;
#[derive(CustomType, ToRhaiMap, FromRhaiMap, Clone, Debug, PartialEq)]
struct Point { x: INT, y: INT }
```
- **Custom Struct Argument**: `(Point -> String)`
```rust
fn print_point(p: Point) -> String { format!("Point(x={}, y={})", p.x, p.y) }
// engine.build_type::<Point>();
// engine.register_fn("print_point_rhai", wrap_for_rhai!(print_point, Point -> String));
// Rhai script: print_point_rhai(#{x:1, y:2})
```
- **Custom Struct Return**: `(INT, INT -> Point)`
```rust
fn make_point(x: INT, y: INT) -> Point { Point { x, y } }
// engine.build_type::<Point>();
// engine.register_fn("make_point_rhai", wrap_for_rhai!(make_point, INT, INT -> Point));
// Rhai script: let p = make_point_rhai(3,4); p.x == 3
```
- **`Vec<CustomStruct>` Argument**: `(Vec<Point> -> INT)`
```rust
fn sum_point_coords(points: Vec<Point>) -> INT { points.iter().map(|p| p.x + p.y).sum() }
// engine.build_type::<Point>();
// engine.register_fn("sum_points_rhai", wrap_for_rhai!(sum_point_coords, Vec<Point> -> INT));
// Rhai script: sum_points_rhai([#{x:1,y:2}, #{x:3,y:4}])
```
- **`Vec<CustomStruct>` Return**: `(INT -> Vec<Point>)`
```rust
fn generate_points(count: INT) -> Vec<Point> {
(0..count).map(|i| Point { x: i, y: i*2 }).collect()
}
// engine.build_type::<Point>();
// engine.register_fn("gen_points_rhai", wrap_for_rhai!(generate_points, INT -> Vec<Point>));
// Rhai script: let arr = gen_points_rhai(2); arr[0].x == 0
```
- **`Vec<CustomStruct>` Argument and `Vec<Primitive>` Return**: `(Vec<Point> -> Vec<INT>)`
```rust
fn get_x_coords(points: Vec<Point>) -> Vec<INT> { points.iter().map(|p| p.x).collect() }
// engine.build_type::<Point>();
// engine.register_fn("get_xs_rhai", wrap_for_rhai!(get_x_coords, Vec<Point> -> Vec<INT>));
```
### How Custom Structs are Handled
- When a custom struct is an **argument** (`MyStructType`): The macro expects the Rhai script to pass an object map. This map is then passed to `MyStructType::from_rhai_map(map)` (provided by `#[derive(FromRhaiMap)]`) to convert it into a Rust struct instance.
- When a custom struct is a **return value**: The Rust function returns an instance of `MyStructType`. The macro calls `instance.to_rhai_map()` (provided by `#[derive(ToRhaiMap)]`) to convert it into a `rhai::Map`, which Rhai receives as an object map.
- For `Vec<CustomStruct>`: Similar logic applies element-wise. Incoming `rhai::Array` elements are converted using `from_rhai_map`; outgoing `Vec` elements are converted using `to_rhai_map` before being collected into a `rhai::Array`.
## Adding New Macro Arms
The `wrap_for_rhai!` macro is defined with several arms, each matching a specific function signature pattern. If you need to support a new, common signature:
1. Open `rhai_wrapper/src/lib.rs`.
2. Add a new macro arm following the existing patterns.
3. Pay attention to argument conversion (from `rhai::Dynamic` or `rhai::Map`) and return value conversion (to `rhai::Dynamic` or `rhai::Map`).
4. For custom types, rely on `YourType::from_rhai_map(...)` and `your_instance.to_rhai_map()`.
5. Consider the order of macro arms if a more specific arm needs to match before a more general one.
This crate aims to reduce boilerplate and make Rhai integration smoother for common Rust patterns.

Some files were not shown because too many files have changed in this diff Show More