Compare commits
8 Commits
main
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
|
db5b9a0a42 | ||
|
b20140785e | ||
|
061aee6f1d | ||
|
ec4769a6b0 | ||
|
16ad4f5743 | ||
|
22032f329a | ||
|
c23de6871b | ||
|
372b7a2772 |
515
_archive/lib.rs
Normal file
515
_archive/lib.rs
Normal 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
BIN
_archive/listen/.DS_Store
vendored
Normal file
Binary file not shown.
2620
_archive/listen/Cargo.lock
generated
Normal file
2620
_archive/listen/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
_archive/listen/Cargo.toml
Normal file
26
_archive/listen/Cargo.toml
Normal 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.
|
38
_archive/listen/examples/send_rhai_script.rs
Normal file
38
_archive/listen/examples/send_rhai_script.rs
Normal 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(())
|
||||||
|
}
|
3
_archive/listen/scripts/init.rhai
Normal file
3
_archive/listen/scripts/init.rhai
Normal 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
111
_archive/listen/src/main.rs
Normal 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
282
_archive/rhai_autobind_macros/Cargo.lock
generated
Normal 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",
|
||||||
|
]
|
17
_archive/rhai_autobind_macros/Cargo.toml
Normal file
17
_archive/rhai_autobind_macros/Cargo.toml
Normal 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"] }
|
@ -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);
|
@ -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(())
|
||||||
|
}
|
152
_archive/rhai_autobind_macros/src/lib.rs
Normal file
152
_archive/rhai_autobind_macros/src/lib.rs
Normal 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 ¯o_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
BIN
_archive/talk/.DS_Store
vendored
Normal file
Binary file not shown.
2146
_archive/talk/Cargo.lock
generated
Normal file
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
14
_archive/talk/Cargo.toml
Normal 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
331
_archive/talk/src/main.rs
Normal 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
|
||||||
|
}
|
235
_archive/talk/static/index.html
Normal file
235
_archive/talk/static/index.html
Normal 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>
|
131
_archive/talk/static/script.js
Normal file
131
_archive/talk/static/script.js
Normal 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();
|
||||||
|
});
|
49
_archive/talk/static/style.css
Normal file
49
_archive/talk/static/style.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
BIN
_archive/talk/temp_rhai_playground_db/data/lookup/data
Normal file
BIN
_archive/talk/temp_rhai_playground_db/data/lookup/data
Normal file
Binary file not shown.
BIN
_archive/talk/temp_rhai_playground_db/index/0.db
Normal file
BIN
_archive/talk/temp_rhai_playground_db/index/0.db
Normal file
Binary file not shown.
1
_archive/talk/temp_rhai_playground_db/index/lookup/.inc
Normal file
1
_archive/talk/temp_rhai_playground_db/index/lookup/.inc
Normal file
@ -0,0 +1 @@
|
|||||||
|
2
|
BIN
_archive/talk/temp_rhai_playground_db/index/lookup/data
Normal file
BIN
_archive/talk/temp_rhai_playground_db/index/lookup/data
Normal file
Binary file not shown.
794
engine/Cargo.lock
generated
Normal file
794
engine/Cargo.lock
generated
Normal 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
37
engine/Cargo.toml
Normal 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
93
engine/README.md
Normal 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
16
engine/build.rs
Normal 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");
|
||||||
|
}
|
101
engine/examples/calendar/calendar_script.rhai
Normal file
101
engine/examples/calendar/calendar_script.rhai
Normal 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
|
||||||
|
}
|
66
engine/examples/calendar/example.rs
Normal file
66
engine/examples/calendar/example.rs
Normal 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.");
|
||||||
|
}
|
68
engine/examples/finance/example.rs
Normal file
68
engine/examples/finance/example.rs
Normal 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.");
|
||||||
|
}
|
202
engine/examples/finance/finance_script.rhai
Normal file
202
engine/examples/finance/finance_script.rhai
Normal 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()})`);
|
120
engine/examples/flow/example.rs
Normal file
120
engine/examples/flow/example.rs
Normal 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(())
|
||||||
|
}
|
111
engine/examples/flow/flow_script.rhai
Normal file
111
engine/examples/flow/flow_script.rhai
Normal 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
72
engine/src/lib.rs
Normal 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
315
engine/src/mock_db.rs
Normal 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);
|
||||||
|
}
|
82
engine/src/mock_db.rs.part
Normal file
82
engine/src/mock_db.rs.part
Normal 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
1
rhai_client/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
17
rhai_client/Cargo.toml
Normal file
17
rhai_client/Cargo.toml
Normal 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
212
rhai_client/src/lib.rs
Normal 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
290
rhai_macros_derive/Cargo.lock
generated
Normal 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",
|
||||||
|
]
|
15
rhai_macros_derive/Cargo.toml
Normal file
15
rhai_macros_derive/Cargo.toml
Normal 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" }
|
200
rhai_macros_derive/README.md
Normal file
200
rhai_macros_derive/README.md
Normal 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.
|
429
rhai_macros_derive/src/lib.rs
Normal file
429
rhai_macros_derive/src/lib.rs
Normal 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
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
21
rhai_system/Cargo.toml
Normal 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
152
rhai_system/README.md
Normal 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
969
rhai_system/architecture.md
Normal 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| {
|
105
rhai_system/examples/hot_reload/README.md
Normal file
105
rhai_system/examples/hot_reload/README.md
Normal 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.
|
21
rhai_system/examples/hot_reload/initial_script.rhai
Normal file
21
rhai_system/examples/hot_reload/initial_script.rhai
Normal 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
|
||||||
|
}
|
13
rhai_system/examples/hot_reload/initial_utils.rhai
Normal file
13
rhai_system/examples/hot_reload/initial_utils.rhai
Normal 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
|
||||||
|
}
|
152
rhai_system/examples/hot_reload/main.rs
Normal file
152
rhai_system/examples/hot_reload/main.rs
Normal 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(())
|
||||||
|
}
|
22
rhai_system/examples/hot_reload/modified_script.rhai
Normal file
22
rhai_system/examples/hot_reload/modified_script.rhai
Normal 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
|
||||||
|
}
|
18
rhai_system/examples/hot_reload/modified_utils.rhai
Normal file
18
rhai_system/examples/hot_reload/modified_utils.rhai
Normal 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 + " ***"
|
||||||
|
}
|
22
rhai_system/examples/hot_reload/script.rhai
Normal file
22
rhai_system/examples/hot_reload/script.rhai
Normal 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
|
||||||
|
}
|
22
rhai_system/examples/hot_reload/second_modified_script.rhai
Normal file
22
rhai_system/examples/hot_reload/second_modified_script.rhai
Normal 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
|
||||||
|
}
|
18
rhai_system/examples/hot_reload/utils.rhai
Normal file
18
rhai_system/examples/hot_reload/utils.rhai
Normal 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 + " ***"
|
||||||
|
}
|
702
rhai_system/project_structure.md
Normal file
702
rhai_system/project_structure.md
Normal 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
|
69
rhai_system/src/factory.rs
Normal file
69
rhai_system/src/factory.rs
Normal 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())
|
||||||
|
}
|
15
rhai_system/src/hot_reload.rs
Normal file
15
rhai_system/src/hot_reload.rs
Normal 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
41
rhai_system/src/lib.rs
Normal 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
101
rhai_system/src/system.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
547
rhai_system/tera_integration.md
Normal file
547
rhai_system/tera_integration.md
Normal 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.
|
52
rhai_system/tests/common/mod.rs
Normal file
52
rhai_system/tests/common/mod.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
125
rhai_system/tests/hot_reload_callback_tests.rs
Normal file
125
rhai_system/tests/hot_reload_callback_tests.rs
Normal 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.");
|
||||||
|
}
|
9
rhai_system/tests/rhai_scripts/main.rhai
Normal file
9
rhai_system/tests/rhai_scripts/main.rhai
Normal 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
|
9
rhai_system/tests/rhai_scripts/module1.rhai
Normal file
9
rhai_system/tests/rhai_scripts/module1.rhai
Normal 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
|
||||||
|
}
|
5
rhai_system/tests/rhai_scripts/module2.rhai
Normal file
5
rhai_system/tests/rhai_scripts/module2.rhai
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// module2.rhai - A simple module with a multiply function
|
||||||
|
|
||||||
|
fn multiply(x, y) {
|
||||||
|
x * y
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
fn get_value() { 84 ; ; }
|
@ -0,0 +1 @@
|
|||||||
|
fn get_value() { 84 ; ; }
|
@ -0,0 +1,2 @@
|
|||||||
|
import "module" as m;
|
||||||
|
fn calculate() { 21 * m::get_multiplier() }
|
@ -0,0 +1 @@
|
|||||||
|
fn get_multiplier() { 4 }
|
@ -0,0 +1,2 @@
|
|||||||
|
import "module" as m;
|
||||||
|
fn calculate() { 21 * m::get_multiplier() }
|
@ -0,0 +1 @@
|
|||||||
|
fn get_multiplier() { 4 }
|
@ -0,0 +1,2 @@
|
|||||||
|
import "module" as m;
|
||||||
|
fn calculate() { 21 * m::get_multiplier() }
|
@ -0,0 +1 @@
|
|||||||
|
fn get_multiplier() { 4 }
|
@ -0,0 +1,2 @@
|
|||||||
|
import "module" as m;
|
||||||
|
fn calculate() { 21 * m::get_multiplier() }
|
@ -0,0 +1 @@
|
|||||||
|
fn get_multiplier() { 4 }
|
@ -0,0 +1,2 @@
|
|||||||
|
fn get_multiplier() { 4 }
|
||||||
|
fn calculate() { 21 * get_multiplier() }
|
@ -0,0 +1 @@
|
|||||||
|
// This is a secondary file that will be modified
|
@ -0,0 +1,2 @@
|
|||||||
|
fn get_multiplier() { 4 }
|
||||||
|
fn calculate() { 21 * get_multiplier() }
|
@ -0,0 +1 @@
|
|||||||
|
// This is a secondary file that will be modified
|
@ -0,0 +1 @@
|
|||||||
|
fn get_value() { 84 }
|
@ -0,0 +1 @@
|
|||||||
|
fn get_value() { 84 }
|
1
rhai_worker/.gitignore
vendored
Normal file
1
rhai_worker/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
27
rhai_worker/Cargo.toml
Normal file
27
rhai_worker/Cargo.toml
Normal 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" }
|
76
rhai_worker/examples/example_math_worker.rs
Normal file
76
rhai_worker/examples/example_math_worker.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
rhai_worker/examples/example_string_worker.rs
Normal file
76
rhai_worker/examples/example_string_worker.rs
Normal 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
144
rhai_worker/src/lib.rs
Normal 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
18
rhai_worker/src/main.rs
Normal 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
539
rhai_wrapper/Cargo.lock
generated
Normal 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
22
rhai_wrapper/Cargo.toml
Normal 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
162
rhai_wrapper/README.md
Normal 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
Reference in New Issue
Block a user