Compare commits

..

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
477 changed files with 23187 additions and 1943 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,6 +1,3 @@
# rhaj
```bash
#tests, following one shows how we can dynamically load the functions from a set of rhai scripts
cargo run --bin test_dynamic_loading
```
DSL's for our ecosystem

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

View File

@ -11,10 +11,6 @@ path = "src/lib.rs"
name = "rhai_engine"
path = "src/main.rs"
[[bin]]
name = "test_dynamic_loading"
path = "examples/loadscripts/test_dynamic_loading.rs"
[dependencies]
rhai = "1.12.0"
chrono = "0.4"

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