more efforts to automate rhai bindings

This commit is contained in:
timurgordon 2025-05-13 02:00:35 +03:00
parent 16ad4f5743
commit ec4769a6b0
14 changed files with 3174 additions and 52 deletions

515
_archive/lib.rs Normal file
View File

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

54
rhai_autobind_macros/Cargo.lock generated Normal file
View File

@ -0,0 +1,54 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[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_autobind_macros"
version = "0.1.0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"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 = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"

View File

@ -0,0 +1,13 @@
[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"

View File

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

View File

@ -0,0 +1,90 @@
use rhai::{Engine, EvalAltResult};
use rhai_autobind_macros::rhai_autobind;
use serde::{Deserialize, Serialize};
use std::path::Path;
// Define a simple Calculator struct with the rhai_autobind attribute
#[derive(Debug, Clone, Serialize, Deserialize, rhai::CustomType)]
#[rhai_autobind]
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
Calculator::register_rhai_bindings_for_calculator(&mut engine);
// Register print function for output
engine.register_fn("println", |s: &str| println!("{}", s));
// Create a calculator instance to demonstrate it works
let calc = Calculator::new();
println!("Initial value: {}", calc.value);
// Load and run the Rhai script
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join("rhai_autobind_example")
.join("calculator.rhai");
println!("Loading Rhai script from: {}", script_path.display());
match engine.eval_file::<()>(script_path) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Error executing script: {}\nAt: {:?}", e, e.position()),
}
Ok(())
}

View File

@ -0,0 +1,187 @@
use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::{parse_macro_input, DeriveInput, Ident, Type, Fields, Visibility, ItemStruct, LitStr};
/// A procedural macro to automatically generate Rhai bindings for a struct.
///
/// This macro will generate an `impl` block for the annotated struct containing
/// a static method, typically named `register_rhai_bindings_for_<struct_name_lowercase>`,
/// which registers the struct, its fields (as getters), its methods, and common
/// database operations with a Rhai `Engine`.
///
/// # Example Usage
///
/// ```rust,ignore
/// // Assuming MyDb is your database connection type
/// // and it's wrapped in an Arc for sharing with Rhai.
/// use std::sync::Arc;
/// use rhai::Engine;
/// use rhai_autobind_macros::rhai_model_export;
///
/// // Define your database type (placeholder)
/// struct MyDb;
/// impl MyDb {
/// fn new() -> Self { MyDb }
/// }
///
/// #[rhai_model_export(db_type = "Arc<MyDb>")]
/// pub struct User {
/// pub id: i64,
/// pub name: String,
/// }
///
/// impl User {
/// // A constructor or builder Rhai can use
/// pub fn new(id: i64, name: String) -> Self {
/// Self { id, name }
/// }
///
/// // An example method
/// pub fn greet(&self) -> String {
/// format!("Hello, {}!", self.name)
/// }
/// }
///
/// fn main() {
/// let mut engine = Engine::new();
/// let db_instance = Arc::new(MyDb::new());
///
/// // The generated function is called here
/// User::register_rhai_bindings_for_user(&mut engine, db_instance);
///
/// // Now you can use User in Rhai scripts
/// // let user = User::new(1, "Test User");
/// // print(user.name);
/// // print(user.greet());
/// }
/// ```
///
/// # Macro Attributes
///
/// - `db_type = "path::to::YourDbType"`: **Required**. Specifies the fully qualified
/// Rust type of your database connection/handler that will be passed to the
/// generated registration function and used for database operations.
/// The type should be enclosed in string quotes, e.g., `"Arc<crate::db::MyDatabase>"`.
///
/// - `collection_name = "your_collection_name"`: **Optional**. Specifies the name of the
/// database collection associated with this model. If not provided, it defaults
/// to the struct name converted to snake_case and pluralized (e.g., `User` -> `"users"`).
/// Enclose in string quotes, e.g., `"user_profiles"`.
#[derive(Debug)]
struct MacroArgs {
db_type: syn::Type,
collection_name: Option<String>,
}
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;
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 {
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());
Ok(MacroArgs { db_type, collection_name })
}
}
#[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_lowercase = struct_name_str.to_lowercase();
let db_type = macro_args.db_type;
let collection_name = macro_args.collection_name.unwrap_or_else(|| {
// Basic pluralization, could use `heck` or `inflector` for better results
if struct_name_lowercase.ends_with('y') {
format!("{}ies", &struct_name_lowercase[..struct_name_lowercase.len()-1])
} else if struct_name_lowercase.ends_with('s') {
format!("{}es", struct_name_lowercase)
} else {
format!("{}s", struct_name_lowercase)
}
});
let generated_registration_fn_name = format_ident!("register_rhai_bindings_for_{}", struct_name_lowercase);
// Placeholder for generated getters and method registrations
let mut field_getters = 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 getter_fn = quote! {
engine.register_get(#field_name_str, |s: &mut #struct_name| s.#field_name.clone());
// TODO: Handle non-cloneable types or provide options
};
field_getters.push(getter_fn);
}
}
}
// TODO: Parse methods from the struct's impl blocks (this is complex)
// TODO: Generate wrappers for DB operations using rhai_wrapper macros
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
) {
// Bring rhai_wrapper into scope for generated DB functions later
// Users might need to ensure rhai_wrapper is a dependency of their crate.
// use rhai_wrapper::*;
engine.build_type::<#struct_name>();
// Register getters
#(#field_getters)*
// Placeholder for constructor registration
// Example: engine.register_fn("new_user", User::new);
// Placeholder for method registration
// Example: engine.register_fn("greet", User::greet);
// Placeholder for DB function registrations
// e.g., get_by_id, get_all, save, delete
// let get_by_id_fn_name = format!("get_{}_by_id", #struct_name_lowercase);
// engine.register_fn(&get_by_id_fn_name, ...wrap_option_method_result!(...));
println!("Registered {} with Rhai (collection: '{}') using DB type {}",
#struct_name_str,
#collection_name,
stringify!(#db_type)
);
}
}
};
TokenStream::from(output)
}

242
rhai_wrapper/Cargo.lock generated
View File

@ -16,6 +16,21 @@ dependencies = [
"zerocopy", "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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
@ -28,12 +43,42 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "const-random" name = "const-random"
version = "0.1.18" version = "0.1.18"
@ -54,6 +99,12 @@ dependencies = [
"tiny-keccak", "tiny-keccak",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "crunchy" name = "crunchy"
version = "0.2.3" version = "0.2.3"
@ -83,6 +134,30 @@ dependencies = [
"wasi 0.14.2+wasi-0.2.4", "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]] [[package]]
name = "instant" name = "instant"
version = "0.1.13" version = "0.1.13"
@ -92,12 +167,28 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "libc" name = "libc"
version = "0.2.172" version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -187,10 +278,44 @@ dependencies = [
name = "rhai_wrapper" name = "rhai_wrapper"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"rhai", "rhai",
"rhai_macros_derive", "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]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.0" version = "1.15.0"
@ -267,6 +392,123 @@ dependencies = [
"wit-bindgen-rt", "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]] [[package]]
name = "wit-bindgen-rt" name = "wit-bindgen-rt"
version = "0.39.0" version = "0.39.0"

View File

@ -6,7 +6,17 @@ edition = "2021"
description = "A wrapper to make generic Rust functions Rhai-compatible." description = "A wrapper to make generic Rust functions Rhai-compatible."
[dependencies] [dependencies]
rhai = "1.21.0" rhai = "1.18.0"
rhai_macros_derive = { path = "../rhai_macros_derive" } rhai_macros_derive = { path = "../rhai_macros_derive" }
serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
[dev-dependencies] [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"

View File

@ -0,0 +1,176 @@
use rhai::Engine;
use rhai_wrapper::rust_rhai_wrapper;
use std::error::Error;
use std::fmt;
// Define a custom error type for Result examples
#[derive(Debug, Clone)]
struct MyError(String);
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for MyError {}
impl From<String> for MyError {
fn from(s: String) -> Self {
MyError(s)
}
}
fn main() {
// Create a Rhai engine
let mut engine = Engine::new();
// 1. Basic example: Add two numbers
// Define the original Rust function
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Register it with Rhai
engine.register_fn("add_rhai", add);
// Create a wrapper function that calls Rhai which calls the Rust function
rust_rhai_wrapper!(add_via_rhai, "add_rhai", (i32, i32) -> i32);
// Test the full circle
let result = add_via_rhai(&mut engine, 5, 3);
println!("add_via_rhai(5, 3) = {}", result);
// 2. String manipulation example
fn concat(s1: String, s2: String) -> String {
format!("{} {}", s1, s2)
}
engine.register_fn("concat_rhai", concat);
rust_rhai_wrapper!(concat_via_rhai, "concat_rhai", (String, String) -> String);
let result = concat_via_rhai(&mut engine, "Hello".to_string(), "World".to_string());
println!("concat_via_rhai(\"Hello\", \"World\") = {}", result);
// 3. Function with no arguments
fn get_random() -> i32 {
42 // Not so random, but it's just an example
}
engine.register_fn("get_random_rhai", get_random);
rust_rhai_wrapper!(get_random_via_rhai, "get_random_rhai", () -> i32);
let result = get_random_via_rhai(&mut engine);
println!("get_random_via_rhai() = {}", result);
// 4. Function with more arguments
fn calculate(a: i32, b: i32, c: i32, d: i32) -> i32 {
a + b * c - d
}
engine.register_fn("calculate_rhai", calculate);
rust_rhai_wrapper!(calculate_via_rhai, "calculate_rhai", (i32, i32, i32, i32) -> i32);
let result = calculate_via_rhai(&mut engine, 5, 3, 2, 1);
println!("calculate_via_rhai(5, 3, 2, 1) = {}", result);
// 5. Function that handles errors with a custom return type
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
// Register a safe division function that returns an array with success flag and result/error
engine.register_fn("safe_divide", |a: i32, b: i32| -> rhai::Array {
if b == 0 {
// Return [false, error message]
let mut arr = rhai::Array::new();
arr.push(rhai::Dynamic::from(false));
arr.push(rhai::Dynamic::from("Division by zero"));
arr
} else {
// Return [true, result]
let mut arr = rhai::Array::new();
arr.push(rhai::Dynamic::from(true));
arr.push(rhai::Dynamic::from(a / b));
arr
}
});
// Create a wrapper for the safe_divide function
rust_rhai_wrapper!(safe_divide_via_rhai, "safe_divide", (i32, i32) -> rhai::Array);
// Test success case
let result = safe_divide_via_rhai(&mut engine, 10, 2);
println!("safe_divide_via_rhai(10, 2) = {:?}", result);
// Test error case
let result = safe_divide_via_rhai(&mut engine, 10, 0);
println!("safe_divide_via_rhai(10, 0) = {:?}", result);
// Process the result
let success = result[0].as_bool().unwrap();
if success {
println!("Division result: {}", result[1].as_int().unwrap());
} else {
println!("Division error: {}", result[1].clone().into_string().unwrap());
}
// 6. Complex example: Using a custom type with Rhai
#[derive(Debug, Clone)]
struct Person {
name: String,
age: i32,
}
// Register type and methods with Rhai
engine.register_type::<Person>();
engine.register_fn("new_person", |name: String, age: i32| -> Person {
Person { name, age }
});
engine.register_fn("get_name", |p: &mut Person| -> String {
p.name.clone()
});
engine.register_fn("get_age", |p: &mut Person| -> i32 {
p.age
});
engine.register_fn("is_adult", |p: &mut Person| -> bool {
p.age >= 18
});
// Register a function that creates a person and checks if they're an adult
engine.register_fn("create_and_check_person", |name: String, age: i32| -> rhai::Array {
let person = Person { name, age };
let is_adult = person.age >= 18;
// Create an array with the person and the is_adult flag
let mut arr = rhai::Array::new();
// Convert the person to a map for Rhai
let mut map = rhai::Map::new();
map.insert("name".into(), rhai::Dynamic::from(person.name));
map.insert("age".into(), rhai::Dynamic::from(person.age));
arr.push(rhai::Dynamic::from(map));
arr.push(rhai::Dynamic::from(is_adult));
arr
});
// Create a wrapper for the Rhai function
rust_rhai_wrapper!(create_person_via_rhai, "create_and_check_person", (String, i32) -> rhai::Array);
// Test the wrapper
let result = create_person_via_rhai(&mut engine, "Alice".to_string(), 25);
println!("create_person_via_rhai(\"Alice\", 25) = {:?}", result);
// Extract data from the Rhai array
let person_map = result[0].clone().try_cast::<rhai::Map>().expect("Expected a Map");
let is_adult = result[1].clone().as_bool().expect("Expected a boolean");
println!("Person: {:?}, Is Adult: {}", person_map, is_adult);
println!("All examples completed successfully!");
}

View File

@ -0,0 +1,312 @@
use rhai::{Engine, INT, CustomType, TypeBuilder};
use rhai_wrapper::{wrap_option_return, wrap_vec_return};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::path::Path;
use chrono;
// --- Mock heromodels_core ---
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
pub struct BaseModelData {
pub id: u32,
pub created_at: i64, // Using i64 for timestamp simplicity
pub updated_at: i64,
pub comment_ids: Vec<u32>,
}
impl BaseModelData {
pub fn new(id: u32) -> Self {
let now = chrono::Utc::now().timestamp();
Self {
id,
created_at: now,
updated_at: now,
comment_ids: Vec::new(),
}
}
// No &mut self for getters if struct is Clone and passed by value in Rhai, or if Rhai handles it.
// For CustomType, Rhai typically passes &mut T to getters/setters.
pub fn get_id(&mut self) -> u32 { self.id }
pub fn get_created_at(&mut self) -> i64 { self.created_at }
pub fn get_updated_at(&mut self) -> i64 { self.updated_at }
pub fn get_comment_ids(&mut self) -> Vec<u32> { self.comment_ids.clone() }
pub fn add_comment_internal(&mut self, comment_id: u32) { // Renamed to avoid clash if also exposed
self.comment_ids.push(comment_id);
self.updated_at = chrono::Utc::now().timestamp();
}
}
// --- User Struct and Methods (Adapted for Rhai) ---
/// Represents a user in the system
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
pub struct User {
/// Base model data
pub base_data: BaseModelData,
/// User's username
pub username: String,
/// User's email address
pub email: String,
/// User's full name
pub full_name: String,
/// Whether the user is active
pub is_active: bool,
}
impl User {
// This is the "builder" entry point
pub fn user_builder(id: INT) -> Self {
Self {
base_data: BaseModelData::new(id as u32),
username: String::new(),
email: String::new(),
full_name: String::new(),
is_active: true, // Default, can be changed by .is_active(false)
}
}
// Fluent setters returning Self
pub fn username(mut self, username: String) -> Self {
self.username = username;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
pub fn email(mut self, email: String) -> Self {
self.email = email;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
pub fn full_name(mut self, full_name: String) -> Self {
self.full_name = full_name;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
// Naming this 'set_is_active' to distinguish from potential getter 'is_active'
// or the script can use direct field access if setter is also registered for 'is_active'
// For fluent chain .is_active(bool_val)
pub fn is_active(mut self, active_status: bool) -> Self {
self.is_active = active_status;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
// Method to add a comment, distinct from direct field manipulation
pub fn add_comment(mut self, comment_id: INT) -> Self {
self.base_data.add_comment_internal(comment_id as u32);
self
}
// Explicit activate/deactivate methods returning Self for chaining if needed
pub fn activate(mut self) -> Self {
self.is_active = true;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
pub fn deactivate(mut self) -> Self {
self.is_active = false;
self.base_data.updated_at = chrono::Utc::now().timestamp();
self
}
// Getters for direct field access from Rhai: register with .register_get()
// Rhai passes &mut User to these
pub fn get_id_rhai(&mut self) -> INT { self.base_data.id as INT }
pub fn get_username_rhai(&mut self) -> String { self.username.clone() }
pub fn get_email_rhai(&mut self) -> String { self.email.clone() }
pub fn get_full_name_rhai(&mut self) -> String { self.full_name.clone() }
pub fn get_is_active_rhai(&mut self) -> bool { self.is_active }
pub fn get_comment_ids_rhai(&mut self) -> Vec<u32> { self.base_data.comment_ids.clone() }
}
// --- Comment Struct and Methods (Adapted for Rhai) ---
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
pub struct Comment {
pub id: INT,
pub user_id: INT, // Assuming comments are linked to users by ID
pub content: String,
}
impl Comment {
pub fn comment_builder(id: INT) -> Self {
Self {
id,
user_id: 0, // Default
content: String::new(),
}
}
// Fluent setters
pub fn user_id(mut self, user_id: INT) -> Self {
self.user_id = user_id;
self
}
pub fn content(mut self, content: String) -> Self {
self.content = content;
self
}
// Getters for Rhai
pub fn get_id_rhai(&mut self) -> INT { self.id }
pub fn get_user_id_rhai(&mut self) -> INT { self.user_id }
pub fn get_content_rhai(&mut self) -> String { self.content.clone() }
}
// --- Mock Database ---
#[derive(Debug, Clone, Default)]
struct DbState {
users: HashMap<INT, User>,
comments: HashMap<INT, Comment>,
}
type OurDB = Arc<Mutex<DbState>>;
fn set_user(db_arc: OurDB, user: User) {
let mut db = db_arc.lock().unwrap();
db.users.insert(user.base_data.id as INT, user);
}
fn get_user_by_id(db_arc: OurDB, id: INT) -> Option<User> {
let db = db_arc.lock().unwrap();
db.users.get(&id).cloned()
}
fn get_all_users(db_arc: OurDB) -> Vec<User> {
let db = db_arc.lock().unwrap();
db.users.values().cloned().collect()
}
fn delete_user_by_id(db_arc: OurDB, id: INT) {
let mut db = db_arc.lock().unwrap();
db.users.remove(&id);
}
fn set_comment(db_arc: OurDB, comment: Comment) {
let mut db = db_arc.lock().unwrap();
db.comments.insert(comment.id, comment);
}
fn get_comment_by_id(db_arc: OurDB, id: INT) -> Option<Comment> {
let db = db_arc.lock().unwrap();
db.comments.get(&id).cloned()
}
fn get_users_by_activity_status_optional(db_arc: OurDB, is_active_filter: bool) -> Option<Vec<User>> {
let db = db_arc.lock().unwrap();
let users: Vec<User> = db.users.values()
.filter(|u| u.is_active == is_active_filter)
.cloned()
.collect();
if users.is_empty() {
None
} else {
Some(users)
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut engine = Engine::new();
let db_instance: OurDB = Arc::new(Mutex::new(DbState::default()));
// Register User type and its methods/getters/setters
engine
.register_type_with_name::<User>("User")
// Fluent methods - these are registered as functions that take User and return User
.register_fn("user_builder", User::user_builder)
.register_fn("username", User::username)
.register_fn("email", User::email)
.register_fn("full_name", User::full_name)
.register_fn("is_active", User::is_active) // This is the fluent setter
.register_fn("add_comment", User::add_comment)
.register_fn("activate", User::activate)
.register_fn("deactivate", User::deactivate)
// Getters for direct field-like access: user.id, user.username etc.
.register_get("id", User::get_id_rhai)
.register_get("username", User::get_username_rhai)
.register_get("email", User::get_email_rhai)
.register_get("full_name", User::get_full_name_rhai)
.register_get("is_active", User::get_is_active_rhai) // This is the getter for direct field access
.register_get("comment_ids", User::get_comment_ids_rhai);
// Register Comment type and its methods/getters/setters
engine
.register_type_with_name::<Comment>("Comment")
.register_fn("comment_builder", Comment::comment_builder)
.register_fn("user_id", Comment::user_id)
.register_fn("content", Comment::content)
.register_get("id", Comment::get_id_rhai)
.register_get("user_id", Comment::get_user_id_rhai)
.register_get("content", Comment::get_content_rhai);
// DB functions - now directly registered
let db_clone_for_get_db = db_instance.clone();
engine.register_fn("get_db", move || db_clone_for_get_db.clone());
let db_clone_for_set_user = db_instance.clone();
engine.register_fn("set_user", move |user: User| set_user(db_clone_for_set_user.clone(), user));
let db_clone_for_get_user_by_id = db_instance.clone();
engine.register_fn("get_user_by_id", move |id: INT| {
wrap_option_return!(get_user_by_id, OurDB, INT => User)(db_clone_for_get_user_by_id.clone(), id)
});
let db_clone_for_get_all_users = db_instance.clone();
engine.register_fn(
"get_all_users",
move || {
(wrap_vec_return!(get_all_users, OurDB => User))(db_clone_for_get_all_users.clone())
}
);
let db_clone_for_delete_user = db_instance.clone();
engine.register_fn("delete_user_by_id", move |id: INT| delete_user_by_id(db_clone_for_delete_user.clone(), id));
let db_clone_for_set_comment = db_instance.clone();
engine.register_fn("set_comment", move |comment: Comment| set_comment(db_clone_for_set_comment.clone(), comment));
let db_clone_for_get_comment_by_id = db_instance.clone();
engine.register_fn("get_comment_by_id", move |id: INT| {
wrap_option_return!(get_comment_by_id, OurDB, INT => Comment)(db_clone_for_get_comment_by_id.clone(), id)
});
let db_clone_for_optional_vec = db_instance.clone();
engine.register_fn(
"get_users_by_activity_status_optional",
move |is_active_filter: bool| {
(wrap_option_vec_return!(get_users_by_activity_status_optional, OurDB, bool => User))(
db_clone_for_optional_vec.clone(),
is_active_filter
)
}
);
engine.register_fn("println", |s: &str| println!("{}", s));
engine.register_fn("print", |s: &str| print!("{}", s));
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join("user_script.rhai");
println!("Loading Rhai script from: {}", script_path.display());
match engine.eval_file::<()>(script_path) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Error executing script: {}\nAt: {:?}", e, e.position()),
}
Ok(())
}

View File

@ -0,0 +1,175 @@
// Hero Models - Rhai Example Script
println("Hero Models - Rhai Usage Example");
println("================================");
// Get the DB instance
let db = get_db();
// Create a new user using the builder pattern
println("Creating users...");
// Create user 1
let user1 = user_builder(1)
.username("johndoe")
.email("john.doe@example.com")
.full_name("John Doe")
.is_active(false);
set_user(user1);
// Create user 2
let user2 = user_builder(2)
.username("janesmith")
.email("jane.smith@example.com")
.full_name("Jane Smith")
.is_active(true);
set_user(user2);
// Create user 3
let user3 = user_builder(3)
.username("willism")
.email("willis.masters@example.com")
.full_name("Willis Masters")
.is_active(true);
set_user(user3);
// Create user 4
let user4 = user_builder(4)
.username("carrols")
.email("carrol.smith@example.com")
.full_name("Carrol Smith")
.is_active(false);
set_user(user4);
// Get user by ID
println("\nRetrieving user by ID...");
let retrieved_user = get_user_by_id(1);
if retrieved_user != () { // In Rhai, functions returning Option<T> yield () for None
println("Found user: " + retrieved_user.full_name + " (ID: " + retrieved_user.id + ")");
println("Username: " + retrieved_user.username);
println("Email: " + retrieved_user.email);
println("Active: " + retrieved_user.is_active);
} else {
println("User not found");
}
// Get users by active status
println("\nRetrieving active users...");
let all_users_for_active_check = get_all_users();
let active_users = [];
// Filter active users
for user_item in all_users_for_active_check {
if user_item.is_active == true {
active_users.push(user_item);
}
}
println("Found " + active_users.len() + " active users:");
for user_item in active_users {
println("- " + user_item.full_name + " (ID: " + user_item.id + ")");
}
// Delete a user
println("\nDeleting user...");
delete_user_by_id(2);
// Get active users again
println("\nRetrieving active users after deletion...");
let all_users_after_delete = get_all_users();
let active_users_after_delete = [];
// Filter active users
for user_item in all_users_after_delete {
if user_item.is_active == true {
active_users_after_delete.push(user_item);
}
}
println("Found " + active_users_after_delete.len() + " active users:");
for user_item in active_users_after_delete {
println("- " + user_item.full_name + " (ID: " + user_item.id + ")");
}
// Get inactive users
println("\nRetrieving inactive users...");
let all_users_for_inactive_check = get_all_users();
let inactive_users = [];
// Filter inactive users
for user_item in all_users_for_inactive_check {
if user_item.is_active == false {
inactive_users.push(user_item);
}
}
println("Found " + inactive_users.len() + " inactive users:");
for user_item in inactive_users {
println("- " + user_item.full_name + " (ID: " + user_item.id + ")");
}
// Create a comment for user 1
println("\nCreating a comment...");
let comment1 = comment_builder(5)
.user_id(1)
.content("This is a comment on the user");
set_comment(comment1);
// Get the comment
println("\nRetrieving comment...");
let retrieved_comment = get_comment_by_id(5);
if retrieved_comment != () { // In Rhai, functions returning Option<T> yield () for None
println("Found comment: " + retrieved_comment.content);
println("Comment ID: " + retrieved_comment.id);
println("User ID: " + retrieved_comment.user_id);
} else {
println("Comment not found");
}
println("\nRetrieving optional active users (should be Some(Array) or Unit)...");
let optional_active_users = get_users_by_activity_status_optional(true);
if optional_active_users == () { // Check for unit (None)
println("No active users found (returned unit).");
} else {
println("Found optional active users:");
for user in optional_active_users {
println("- " + user.full_name + " (ID: " + user.id + ")");
}
}
println("\nRetrieving optional inactive users (should be Some(Array) or Unit)...");
let optional_inactive_users = get_users_by_activity_status_optional(false);
if optional_inactive_users == () { // Check for unit (None)
println("No inactive users found (returned unit).");
} else {
println("Found optional inactive users:");
for user in optional_inactive_users {
println("- " + user.full_name + " (ID: " + user.id + ")");
}
}
// To specifically test the None case from our Rust logic (empty vec for a status returns None)
println("\nTesting None case for optional vector retrieval...");
println("Deleting all users to ensure empty states...");
let all_users_before_delete_all = get_all_users();
for user_to_delete in all_users_before_delete_all {
delete_user_by_id(user_to_delete.id);
}
let optional_active_after_delete_all = get_users_by_activity_status_optional(true);
if optional_active_after_delete_all == () {
println("Correctly received unit for active users after deleting all.");
} else {
println("ERROR: Expected unit for active users after deleting all, but got an array.");
print("Value received: " + optional_active_after_delete_all);
}
let optional_inactive_after_delete_all = get_users_by_activity_status_optional(false);
if optional_inactive_after_delete_all == () {
println("Correctly received unit for inactive users after deleting all.");
} else {
println("ERROR: Expected unit for inactive users after deleting all, but got an array.");
print("Value received: " + optional_inactive_after_delete_all);
}
println("\nRhai example completed successfully!");

View File

@ -208,3 +208,601 @@ macro_rules! wrap_for_rhai {
$func $func
}; };
} }
/// Macro to wrap a Rust function that returns Option<T> for Rhai.
/// It converts Some(T) to Dynamic::from(T) and None to Dynamic::UNIT.
#[macro_export]
macro_rules! wrap_option_return {
// Matches fn_name(arg1_type, arg2_type) -> Option<ReturnType>
// Example: get_user_by_id(OurDB, INT) -> Option<User>
// Macro call: wrap_option_return!(get_user_by_id, OurDB, INT => User)
// Generated closure: |db: OurDB, id: INT| -> rhai::Dynamic { ... }
($func:ident, $Arg1Type:ty, $Arg2Type:ty => $ReturnType:ident) => {
|arg1: $Arg1Type, arg2: $Arg2Type| -> rhai::Dynamic {
match $func(arg1, arg2) {
Some(value) => {
// Ensure the value is converted into a Dynamic.
// If $ReturnType is already a CustomType, this should work directly.
rhai::Dynamic::from(value)
}
None => rhai::Dynamic::UNIT,
}
}
};
// Add more arms here if functions with different numbers/types of arguments returning Option<T>
// are needed.
// For example, for a function with one argument:
// ($func:ident, $Arg1Type:ty => $ReturnType:ident) => { ... };
}
/// Macro to wrap a Rust function that returns Vec<T> for Rhai.
/// It converts the Vec into a rhai::Array.
#[macro_export]
macro_rules! wrap_vec_return {
// For functions like fn(Arg1) -> Vec<InnerType>
// Example: get_all_users(db: OurDB) -> Vec<User>
// Macro call: wrap_vec_return!(get_all_users, OurDB => User)
// Generated closure: |db: OurDB| -> rhai::Array { ... }
($func:ident, $Arg1Type:ty => $InnerVecType:ty) => {
|arg1_val: $Arg1Type| -> rhai::Array {
let result_vec: std::vec::Vec<$InnerVecType> = $func(arg1_val);
result_vec.into_iter().map(rhai::Dynamic::from).collect::<rhai::Array>()
}
};
// For functions like fn(Arg1, Arg2) -> Vec<InnerType>
($func:ident, $Arg1Type:ty, $Arg2Type:ty => $InnerVecType:ty) => {
|arg1_val: $Arg1Type, arg2_val: $Arg2Type| -> rhai::Array {
let result_vec: std::vec::Vec<$InnerVecType> = $func(arg1_val, arg2_val);
result_vec.into_iter().map(rhai::Dynamic::from).collect::<rhai::Array>()
}
};
// For functions like fn() -> Vec<InnerType>
($func:ident, () => $InnerVecType:ty) => {
|| -> rhai::Array {
let result_vec: std::vec::Vec<$InnerVecType> = $func();
result_vec.into_iter().map(rhai::Dynamic::from).collect::<rhai::Array>()
}
};
}
#[macro_export]
macro_rules! wrap_option_vec_return {
// Case: fn_name(Arg1Type, Arg2Type) -> Option<Vec<InnerType>>
// Generates a closure: |arg1_val: Arg1Type, arg2_val: Arg2Type| -> rhai::Dynamic
($func:ident, $Arg1Type:ty, $Arg2Type:ty => $InnerVecType:ty) => {
move |arg1_val: $Arg1Type, arg2_val: $Arg2Type| -> rhai::Dynamic {
match $func(arg1_val, arg2_val) { // Call the original Rust function
Some(vec_inner) => { // vec_inner is Vec<$InnerVecType>
let rhai_array = vec_inner.into_iter()
.map(rhai::Dynamic::from) // Each $InnerVecType must be convertible to Dynamic
.collect::<rhai::Array>();
rhai::Dynamic::from(rhai_array)
}
None => rhai::Dynamic::UNIT,
}
}
};
// TODO: Add arms for different numbers of arguments if needed, e.g.:
// ($func:ident, $Arg1Type:ty => $InnerVecType:ty) => { ... }
// ($func:ident => $InnerVecType:ty) => { ... }
}
/// Wraps a Rust function that returns `Result<Option<T>, ErrorType>` for Rhai.
/// The generated closure returns `Result<rhai::Dynamic, Box<rhai::EvalAltResult>>`.
/// Assumes `T` implements a `to_rhai_map(&self) -> rhai::Map` method.
/// Assumes `ErrorType` implements `std::fmt::Display`.
#[macro_export]
macro_rules! wrap_option_return_result {
// Case: Function with DB connection and 1 additional argument
(
$func:path, // Path to the actual Rust function
$DbConnType:ty, $Arg1Type:ty => $ReturnType:ty, // DB conn type, Arg types for actual func
$ErrorType:ty // Error type from actual func
) => {
move |db_conn_instance: $DbConnType, arg1_val: $Arg1Type|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match $func(db_conn_instance, arg1_val) { // Call the actual function
Ok(Some(value)) => {
// Assumes ReturnType has a .to_rhai_map() method.
Ok(rhai::Dynamic::from(value.to_rhai_map()))
}
Ok(None) => Ok(rhai::Dynamic::UNIT),
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(), // Requires ErrorType: Display
rhai::Position::NONE,
)))
}
}
}
};
// Case: Function with DB connection and 0 additional arguments
(
$func:path,
$DbConnType:ty => $ReturnType:ty,
$ErrorType:ty
) => {
move |db_conn_instance: $DbConnType|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match $func(db_conn_instance) { // Call the actual function
Ok(Some(value)) => {
Ok(rhai::Dynamic::from(value.to_rhai_map()))
}
Ok(None) => Ok(rhai::Dynamic::UNIT),
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(),
rhai::Position::NONE,
)))
}
}
}
};
// TODO: Add variants for more arguments as needed
}
/// Wraps a Rust function that returns `Result<Vec<T>, ErrorType>` for Rhai.
/// The generated closure returns `Result<rhai::Dynamic, Box<rhai::EvalAltResult>>`.
/// Assumes `T` implements a `to_rhai_map(&self) -> rhai::Map` method.
/// Assumes `ErrorType` implements `std::fmt::Display`.
#[macro_export]
macro_rules! wrap_vec_return_result {
// Case: Function with DB connection and 1 additional argument
(
$func:path, // Path to the actual Rust function
$DbConnType:ty, $Arg1Type:ty => $ReturnType:ty, // DB conn type, Arg types for actual func
$ErrorType:ty // Error type from actual func
) => {
move |db_conn_instance: $DbConnType, arg1_val: $Arg1Type|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match $func(db_conn_instance, arg1_val) { // Call the actual function
Ok(vec_of_values) => {
let rhai_array = vec_of_values
.into_iter()
.map(|value| rhai::Dynamic::from(value.to_rhai_map()))
.collect::<rhai::Array>();
Ok(rhai::Dynamic::from(rhai_array))
}
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(), // Requires ErrorType: Display
rhai::Position::NONE,
)))
}
}
}
};
// Case: Function with DB connection and 0 additional arguments (e.g., get_all)
(
$func:path,
$DbConnType:ty => $ReturnType:ty,
$ErrorType:ty
) => {
move |db_conn_instance: $DbConnType|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match $func(db_conn_instance) { // Call the actual function
Ok(vec_of_values) => {
let rhai_array = vec_of_values
.into_iter()
.map(|value| rhai::Dynamic::from(value.to_rhai_map()))
.collect::<rhai::Array>();
Ok(rhai::Dynamic::from(rhai_array))
}
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(),
rhai::Position::NONE,
)))
}
}
}
};
// TODO: Add variants for more arguments as needed
}
/// Wraps a Rust function that returns `Result<Option<Vec<T>>, ErrorType>` for Rhai.
/// The generated closure returns `Result<rhai::Dynamic, Box<rhai::EvalAltResult>>`.
/// Assumes `T` implements a `to_rhai_map(&self) -> rhai::Map` method.
/// Assumes `ErrorType` implements `std::fmt::Display`.
#[macro_export]
macro_rules! wrap_option_vec_return_result {
// Case: Function with DB connection and 1 additional argument
(
$func:path, // Path to the actual Rust function
$DbConnType:ty, $Arg1Type:ty => $ReturnType:ty, // DB conn type, Arg types for actual func
$ErrorType:ty // Error type from actual func
) => {
move |db_conn_instance: $DbConnType, arg1_val: $Arg1Type|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match $func(db_conn_instance, arg1_val) { // Call the actual function
Ok(Some(vec_of_values)) => {
let rhai_array = vec_of_values
.into_iter()
.map(|value| rhai::Dynamic::from(value.to_rhai_map()))
.collect::<rhai::Array>();
Ok(rhai::Dynamic::from(rhai_array))
}
Ok(None) => Ok(rhai::Dynamic::UNIT),
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(), // Requires ErrorType: Display
rhai::Position::NONE,
)))
}
}
}
};
// Case: Function with DB connection and 0 additional arguments
(
$func:path,
$DbConnType:ty => $ReturnType:ty,
$ErrorType:ty
) => {
move |db_conn_instance: $DbConnType|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match $func(db_conn_instance) { // Call the actual function
Ok(Some(vec_of_values)) => {
let rhai_array = vec_of_values
.into_iter()
.map(|value| rhai::Dynamic::from(value.to_rhai_map()))
.collect::<rhai::Array>();
Ok(rhai::Dynamic::from(rhai_array))
}
Ok(None) => Ok(rhai::Dynamic::UNIT),
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(),
rhai::Position::NONE,
)))
}
}
}
};
// TODO: Add variants for more arguments as needed
}
// --- Macros for methods returning Result<_, _> for fallible operations --- //
/// Wraps a Rust method that returns `Result<Option<T>, ErrorType>` for Rhai.
/// The generated closure returns `Result<rhai::Dynamic, Box<rhai::EvalAltResult>>`.
/// Assumes `T` implements `Clone` and is Rhai `CustomType`.
/// Assumes `ErrorType` implements `std::fmt::Display`.
#[macro_export]
macro_rules! wrap_option_method_result {
// Case: Method with DB connection (self) and 1 additional argument
(
$method_name:ident, // Name of the method on the Collection
$DbConnType:ty, $Arg1Type:ty => $ReturnType:ty, // DB conn type (e.g. &Collection), Arg types
$ErrorType:ty // Error type from method
) => {
move |db_conn_instance: $DbConnType, arg1_val: $Arg1Type|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match db_conn_instance.$method_name(arg1_val) { // Call the method
Ok(Some(value)) => {
Ok(rhai::Dynamic::from(value.clone())) // Assumes ReturnType: Clone
}
Ok(None) => Ok(rhai::Dynamic::UNIT),
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(), // ErrorType: Display
rhai::Position::NONE,
)))
}
}
}
};
// Case: Method with DB connection (self) and 0 additional arguments
(
$method_name:ident,
$DbConnType:ty => $ReturnType:ty,
$ErrorType:ty
) => {
move |db_conn_instance: $DbConnType|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match db_conn_instance.$method_name() { // Call the method
Ok(Some(value)) => {
Ok(rhai::Dynamic::from(value.clone())) // Assumes ReturnType: Clone
}
Ok(None) => Ok(rhai::Dynamic::UNIT),
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(),
rhai::Position::NONE,
)))
}
}
}
};
// TODO: Add variants for more arguments as needed
}
/// Wraps a Rust method that returns `Result<Vec<T>, ErrorType>` for Rhai.
/// The generated closure returns `Result<rhai::Dynamic, Box<rhai::EvalAltResult>>`.
/// Assumes `T` implements `Clone` and is Rhai `CustomType`.
/// Assumes `ErrorType` implements `std::fmt::Display`.
#[macro_export]
macro_rules! wrap_vec_method_result {
// Case: Method with DB connection (self) and 1 additional argument
(
$method_name:ident, // Name of the method on the Collection
$DbConnType:ty, $Arg1Type:ty => $ReturnType:ty, // DB conn type, Arg types
$ErrorType:ty // Error type from method
) => {
move |db_conn_instance: $DbConnType, arg1_val: $Arg1Type|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match db_conn_instance.$method_name(arg1_val) { // Call the method
Ok(vec_of_values) => {
let rhai_array = vec_of_values
.into_iter()
.map(|value| rhai::Dynamic::from(value.clone())) // Assumes ReturnType: Clone
.collect::<rhai::Array>();
Ok(rhai::Dynamic::from(rhai_array))
}
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(), // ErrorType: Display
rhai::Position::NONE,
)))
}
}
}
};
// Case: Method with DB connection (self) and 0 additional arguments (e.g., get_all)
(
$method_name:ident,
$DbConnType:ty => $ReturnType:ty,
$ErrorType:ty
) => {
move |db_conn_instance: $DbConnType|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match db_conn_instance.$method_name() { // Call the method
Ok(vec_of_values) => {
let rhai_array = vec_of_values
.into_iter()
.map(|value| rhai::Dynamic::from(value.clone())) // Assumes ReturnType: Clone
.collect::<rhai::Array>();
Ok(rhai::Dynamic::from(rhai_array))
}
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(),
rhai::Position::NONE,
)))
}
}
}
};
// TODO: Add variants for more arguments as needed
}
/// Wraps a Rust method that returns `Result<Option<Vec<T>>, ErrorType>` for Rhai.
/// The generated closure returns `Result<rhai::Dynamic, Box<rhai::EvalAltResult>>`.
/// Assumes `T` implements `Clone` and is Rhai `CustomType`.
/// Assumes `ErrorType` implements `std::fmt::Display`.
#[macro_export]
macro_rules! wrap_option_vec_method_result {
// Case: Method with DB connection (self) and 1 additional argument
(
$method_name:ident, // Name of the method on the Collection
$DbConnType:ty, $Arg1Type:ty => $ReturnType:ty, // DB conn type, Arg types
$ErrorType:ty // Error type from method
) => {
move |db_conn_instance: $DbConnType, arg1_val: $Arg1Type|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match db_conn_instance.$method_name(arg1_val) { // Call the method
Ok(Some(vec_of_values)) => {
let rhai_array = vec_of_values
.into_iter()
.map(|value| rhai::Dynamic::from(value.clone())) // Assumes ReturnType: Clone
.collect::<rhai::Array>();
Ok(rhai::Dynamic::from(rhai_array))
}
Ok(None) => Ok(rhai::Dynamic::UNIT),
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(), // ErrorType: Display
rhai::Position::NONE,
)))
}
}
}
};
// Case: Method with DB connection (self) and 0 additional arguments
(
$method_name:ident,
$DbConnType:ty => $ReturnType:ty,
$ErrorType:ty
) => {
move |db_conn_instance: $DbConnType|
-> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
match db_conn_instance.$method_name() { // Call the method
Ok(Some(vec_of_values)) => {
let rhai_array = vec_of_values
.into_iter()
.map(|value| rhai::Dynamic::from(value.clone())) // Assumes ReturnType: Clone
.collect::<rhai::Array>();
Ok(rhai::Dynamic::from(rhai_array))
}
Ok(None) => Ok(rhai::Dynamic::UNIT),
Err(err) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("Function Error: {}", err).into(),
rhai::Position::NONE,
)))
}
}
}
};
// TODO: Add variants for more arguments as needed
}
// TODO: Consider merging wrap_option_return, wrap_vec_return, and wrap_option_vec_return
// into a more general wrap_for_rhai! macro if patterns become too numerous or complex.
// For now, separate macros are clear for distinct return type patterns.
/// A macro that creates a Rust function that calls a Rhai engine to execute a Rhai function which wraps an underlying Rust function.
/// This creates a full circle of Rust → Rhai → Rust function calls.
///
/// # Example Usage
/// ```rust,ignore
/// // Define a Rust function
/// fn add(a: i32, b: i32) -> i32 {
/// a + b
/// }
///
/// // Register it with Rhai (assuming engine is already created)
/// engine.register_fn("add_rhai", add);
///
/// // Create a wrapper function that takes an engine reference and calls the Rhai function
/// rust_rhai_wrapper!(add_via_rhai, "add_rhai", (i32, i32) -> i32);
///
/// // Now you can call add_via_rhai which will call the Rhai function add_rhai which calls the Rust function add
/// let result = add_via_rhai(&mut engine, 5, 3); // result = 8
/// ```
#[macro_export]
macro_rules! rust_rhai_wrapper {
// Basic case: function with no arguments
($func_name:ident, $rhai_func_name:expr, () -> $return_type:ty) => {
pub fn $func_name(engine: &mut rhai::Engine) -> $return_type {
let result = engine.eval::<$return_type>(
&format!("{}()", $rhai_func_name)
).expect(&format!("Failed to call Rhai function {}", $rhai_func_name));
result
}
};
// Function with one argument
($func_name:ident, $rhai_func_name:expr, ($arg1_type:ty) -> $return_type:ty) => {
pub fn $func_name(engine: &mut rhai::Engine, arg1: $arg1_type) -> $return_type {
// Create a scope to pass arguments
let mut scope = rhai::Scope::new();
scope.push("arg1", arg1);
let result = engine.eval_with_scope::<$return_type>(
&mut scope,
&format!("{}(arg1)", $rhai_func_name)
).expect(&format!("Failed to call Rhai function {}", $rhai_func_name));
result
}
};
// Function with two arguments
($func_name:ident, $rhai_func_name:expr, ($arg1_type:ty, $arg2_type:ty) -> $return_type:ty) => {
pub fn $func_name(engine: &mut rhai::Engine, arg1: $arg1_type, arg2: $arg2_type) -> $return_type {
// Create a scope to pass arguments
let mut scope = rhai::Scope::new();
scope.push("arg1", arg1);
scope.push("arg2", arg2);
let result = engine.eval_with_scope::<$return_type>(
&mut scope,
&format!("{}(arg1, arg2)", $rhai_func_name)
).expect(&format!("Failed to call Rhai function {}", $rhai_func_name));
result
}
};
// Function with three arguments
($func_name:ident, $rhai_func_name:expr, ($arg1_type:ty, $arg2_type:ty, $arg3_type:ty) -> $return_type:ty) => {
pub fn $func_name(engine: &mut rhai::Engine, arg1: $arg1_type, arg2: $arg2_type, arg3: $arg3_type) -> $return_type {
// Create a scope to pass arguments
let mut scope = rhai::Scope::new();
scope.push("arg1", arg1);
scope.push("arg2", arg2);
scope.push("arg3", arg3);
let result = engine.eval_with_scope::<$return_type>(
&mut scope,
&format!("{}(arg1, arg2, arg3)", $rhai_func_name)
).expect(&format!("Failed to call Rhai function {}", $rhai_func_name));
result
}
};
// Function with four arguments
($func_name:ident, $rhai_func_name:expr, ($arg1_type:ty, $arg2_type:ty, $arg3_type:ty, $arg4_type:ty) -> $return_type:ty) => {
pub fn $func_name(engine: &mut rhai::Engine, arg1: $arg1_type, arg2: $arg2_type, arg3: $arg3_type, arg4: $arg4_type) -> $return_type {
// Create a scope to pass arguments
let mut scope = rhai::Scope::new();
scope.push("arg1", arg1);
scope.push("arg2", arg2);
scope.push("arg3", arg3);
scope.push("arg4", arg4);
let result = engine.eval_with_scope::<$return_type>(
&mut scope,
&format!("{}(arg1, arg2, arg3, arg4)", $rhai_func_name)
).expect(&format!("Failed to call Rhai function {}", $rhai_func_name));
result
}
};
// Function with five arguments
($func_name:ident, $rhai_func_name:expr, ($arg1_type:ty, $arg2_type:ty, $arg3_type:ty, $arg4_type:ty, $arg5_type:ty) -> $return_type:ty) => {
pub fn $func_name(engine: &mut rhai::Engine, arg1: $arg1_type, arg2: $arg2_type, arg3: $arg3_type, arg4: $arg4_type, arg5: $arg5_type) -> $return_type {
// Create a scope to pass arguments
let mut scope = rhai::Scope::new();
scope.push("arg1", arg1);
scope.push("arg2", arg2);
scope.push("arg3", arg3);
scope.push("arg4", arg4);
scope.push("arg5", arg5);
let result = engine.eval_with_scope::<$return_type>(
&mut scope,
&format!("{}(arg1, arg2, arg3, arg4, arg5)", $rhai_func_name)
).expect(&format!("Failed to call Rhai function {}", $rhai_func_name));
result
}
};
// Function with a Result return type and no arguments
($func_name:ident, $rhai_func_name:expr, () -> Result<$ok_type:ty, $err_type:ty>) => {
pub fn $func_name(engine: &mut rhai::Engine) -> Result<$ok_type, $err_type> {
match engine.eval::<$ok_type>(&format!("{}()", $rhai_func_name)) {
Ok(result) => Ok(result),
Err(err) => Err($err_type::from(format!("Rhai error: {}", err))),
}
}
};
// Function with a Result return type and one argument
($func_name:ident, $rhai_func_name:expr, ($arg1_type:ty) -> Result<$ok_type:ty, $err_type:ty>) => {
pub fn $func_name(engine: &mut rhai::Engine, arg1: $arg1_type) -> Result<$ok_type, $err_type> {
// Create a scope to pass arguments
let mut scope = rhai::Scope::new();
scope.push("arg1", arg1);
match engine.eval_with_scope::<$ok_type>(&mut scope, &format!("{}(arg1)", $rhai_func_name)) {
Ok(result) => Ok(result),
Err(err) => Err($err_type::from(format!("Rhai error: {}", err))),
}
}
};
// Function with a Result return type and two arguments
($func_name:ident, $rhai_func_name:expr, ($arg1_type:ty, $arg2_type:ty) -> Result<$ok_type:ty, $err_type:ty>) => {
pub fn $func_name(engine: &mut rhai::Engine, arg1: $arg1_type, arg2: $arg2_type) -> Result<$ok_type, $err_type> {
// Create a scope to pass arguments
let mut scope = rhai::Scope::new();
scope.push("arg1", arg1);
scope.push("arg2", arg2);
match engine.eval_with_scope::<$ok_type>(&mut scope, &format!("{}(arg1, arg2)", $rhai_func_name)) {
Ok(result) => Ok(result),
Err(err) => Err($err_type::from(format!("Rhai error: {}", err))),
}
}
};
}
// into a more general wrap_for_rhai! macro if patterns become too numerous or complex.
// For now, separate macros are clear for distinct return type patterns.

View File

@ -1,15 +1,23 @@
use rhai_wrapper::wrap_for_rhai; use rhai_wrapper::wrap_for_rhai;
use rhai_wrapper::{ToRhaiMap, FromRhaiMap}; use rhai_wrapper::{ToRhaiMap, FromRhaiMap};
use rhai::{CustomType, TypeBuilder, Engine, INT, FLOAT, Array}; use rhai::{CustomType, TypeBuilder, Engine, INT, FLOAT, Array};
use rhai_macros_derive::{ToRhaiMap as ToRhaiMapDerive, FromRhaiMap as FromRhaiMapDerive}; use rhai_macros_derive::{ToRhaiMap as ToRhaiMapDerive, FromRhaiMap as FromRhaiMapDerive, export_fn};
#[export_fn(rhai_name = "add_rhai")]
fn add(a: INT, b: INT) -> INT { a + b } fn add(a: INT, b: INT) -> INT { a + b }
#[export_fn(rhai_name = "mul_rhai")]
fn mul(a: INT, b: INT) -> INT { a * b } fn mul(a: INT, b: INT) -> INT { a * b }
#[export_fn(rhai_name = "greet_rhai")]
fn greet(name: String) -> String { format!("Hello, {name}!") } fn greet(name: String) -> String { format!("Hello, {name}!") }
#[export_fn(rhai_name = "get_forty_two_rhai")]
fn get_forty_two() -> INT { 42 } fn get_forty_two() -> INT { 42 }
#[export_fn(rhai_name = "shout_rhai")]
fn shout() -> String { "HEY!".to_string() } fn shout() -> String { "HEY!".to_string() }
#[export_fn(rhai_name = "add_float_rhai")]
fn add_float(a: FLOAT, b: FLOAT) -> FLOAT { a + b } fn add_float(a: FLOAT, b: FLOAT) -> FLOAT { a + b }
#[export_fn(rhai_name = "is_even_rhai")]
fn is_even(n: INT) -> bool { n % 2 == 0 } fn is_even(n: INT) -> bool { n % 2 == 0 }
#[export_fn(rhai_name = "maybe_add_rhai")]
fn maybe_add(a: INT, b: INT, do_add: bool) -> Option<INT> { if do_add { Some(a + b) } else { None } } fn maybe_add(a: INT, b: INT, do_add: bool) -> Option<INT> { if do_add { Some(a + b) } else { None } }
// Renamed from sum_vec, takes rhai::Array // Renamed from sum_vec, takes rhai::Array
@ -110,69 +118,83 @@ fn get_polygon_id_and_num_vertices(poly: Polygon) -> String {
#[test] #[test]
fn test_add() { fn test_add() {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_fn("add", wrap_for_rhai!(add)); engine.register_fn("add_rhai", add_rhai_wrapper);
let result = engine.eval::<INT>("add(2, 3)").unwrap(); let result = engine.eval::<INT>("add_rhai(2, 3)").unwrap();
assert_eq!(result, 5); assert_eq!(result, 5);
} }
#[test] #[test]
fn test_mul() { fn test_mul() {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_fn("mul", wrap_for_rhai!(mul)); engine.register_fn("mul_rhai", mul_rhai_wrapper);
let result = engine.eval::<INT>("mul(4, 5)").unwrap(); let result = engine.eval::<INT>("mul_rhai(4, 5)").unwrap();
assert_eq!(result, 20); assert_eq!(result, 20);
} }
#[test] #[test]
fn test_greet() { fn test_greet() {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_fn("greet", wrap_for_rhai!(greet)); engine.register_fn("greet_rhai", greet_rhai_wrapper);
let result = engine.eval::<String>(r#"greet("Alice")"#).unwrap(); let result = engine.eval::<String>(r#"greet_rhai("Alice")"#).unwrap();
assert_eq!(result, "Hello, Alice!"); assert_eq!(result, "Hello, Alice!");
} }
#[test] #[test]
fn test_get_forty_two() { fn test_get_forty_two() {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_fn("get_forty_two", wrap_for_rhai!(get_forty_two)); engine.register_fn("get_forty_two_rhai", get_forty_two_rhai_wrapper);
let result = engine.eval::<INT>("get_forty_two()").unwrap(); let result = engine.eval::<INT>("get_forty_two_rhai()").unwrap();
assert_eq!(result, 42); assert_eq!(result, 42);
} }
#[test] #[test]
fn test_shout() { fn test_shout() {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_fn("shout", wrap_for_rhai!(shout)); engine.register_fn("shout_rhai", shout_rhai_wrapper);
let result = engine.eval::<String>("shout()").unwrap(); let result = engine.eval::<String>("shout_rhai()").unwrap();
assert_eq!(result, "HEY!"); assert_eq!(result, "HEY!");
} }
#[test] #[test]
fn test_add_float() { fn test_add_float() {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_fn("add_float", wrap_for_rhai!(add_float)); engine.register_fn("add_float_rhai", add_float_rhai_wrapper);
let result = engine.eval::<FLOAT>("add_float(1.5, 2.25)").unwrap(); let result = engine.eval::<FLOAT>("add_float_rhai(2.5, 3.5)").unwrap();
assert!((result - 3.75).abs() < 1e-8); assert_eq!(result, 6.0);
} }
#[test] #[test]
fn test_is_even() { fn test_is_even() {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_fn("is_even", wrap_for_rhai!(is_even)); engine.register_fn("is_even_rhai", is_even_rhai_wrapper);
let result = engine.eval::<bool>("is_even(4)").unwrap(); let result_true = engine.eval::<bool>("is_even_rhai(4)").unwrap();
assert!(result); assert_eq!(result_true, true);
let result = engine.eval::<bool>("is_even(5)").unwrap(); let result_false = engine.eval::<bool>("is_even_rhai(3)").unwrap();
assert!(!result); assert_eq!(result_false, false);
} }
#[test] #[test]
fn test_maybe_add() { fn test_maybe_add() {
let mut engine = Engine::new(); let mut engine = Engine::new();
engine.register_fn("maybe_add", wrap_for_rhai!(maybe_add)); engine.register_fn("maybe_add_rhai", maybe_add_rhai_wrapper);
let result = engine.eval::<Option<INT>>("maybe_add(2, 3, true)").unwrap();
assert_eq!(result, Some(5)); // Test case where None is returned (expecting an error or specific handling in Rhai)
let result = engine.eval::<Option<INT>>("maybe_add(2, 3, false)").unwrap(); // Rhai treats Option::None as an empty Dynamic, which can lead to type mismatch if not handled.
assert_eq!(result, None); // For now, let's check if the script produces a specific type or if it can be evaluated to Dynamic.
// If the function returns None, eval might return an error if trying to cast to INT.
// Let's eval to Dynamic and check if it's empty (Rhai's representation of None).
let result_none = engine.eval::<rhai::Dynamic>("maybe_add_rhai(2, 3, false)").unwrap();
// Debug prints
println!("Debug [test_maybe_add]: result_none = {:?}", result_none);
println!("Debug [test_maybe_add]: result_none.type_name() = {}", result_none.type_name());
println!("Debug [test_maybe_add]: result_none.is::<()>() = {}", result_none.is::<()>());
assert!(result_none.is_unit(), "Expected Rhai None (unit Dynamic)");
// Test case where Some is returned
let result_some = engine.eval::<INT>("maybe_add_rhai(2, 3, true)").unwrap();
assert_eq!(result_some, 5);
} }
#[test] #[test]
@ -502,21 +524,20 @@ mod new_export_fn_tests {
assert_eq!(result, 15); assert_eq!(result, 15);
} }
// #[test] #[test]
// fn test_export_fn_custom_type_arg_return() { // This test was commented out, keeping as is for now fn test_export_fn_custom_type_arg_return_new() {
// let mut engine = Engine::new(); let mut engine = Engine::new();
// engine.build_type::<Point>(); engine.build_type::<Point>();
// // engine.register_fn("offset_simple_point", offset_simple_point_rhai_wrapper); engine.register_fn("offset_simple_point", offset_simple_point_rhai_wrapper);
// // let script = r#" let script = r#"
// // let p = #{ x: 10, y: 20 }; let p = #{ x: 10, y: 20 };
// // let p_offset = offset_simple_point(p, 5); let p_offset = offset_simple_point(p, 5);
// // p_offset.x p_offset.x
// // "#; "#;
// // let result = engine.eval::<INT>(script).unwrap(); let result = engine.eval::<INT>(script).unwrap();
// // assert_eq!(result, 15); assert_eq!(result, 15);
}
// }
#[derive(Debug, Clone, PartialEq, FromRhaiMapDerive, ToRhaiMapDerive, CustomType)] #[derive(Debug, Clone, PartialEq, FromRhaiMapDerive, ToRhaiMapDerive, CustomType)]
@ -556,17 +577,4 @@ mod new_export_fn_tests {
assert_eq!(result_struct.optional_nested_vec.as_ref().unwrap().len(), 1); assert_eq!(result_struct.optional_nested_vec.as_ref().unwrap().len(), 1);
assert_eq!(result_struct.optional_nested_vec.as_ref().unwrap()[0], SampleStruct { value: 3, name: "n3".to_string() }); assert_eq!(result_struct.optional_nested_vec.as_ref().unwrap()[0], SampleStruct { value: 3, name: "n3".to_string() });
} }
#[test]
fn test_export_fn_custom_type_arg_return_new() {
let mut engine = Engine::new();
engine.build_type::<Point>();
engine.register_fn("offset_simple_point", offset_simple_point_rhai_wrapper);
let script = r#"
42
"#;
let result = engine.eval::<INT>(script).unwrap();
assert_eq!(result, 42);
}
} }

View File

@ -0,0 +1,709 @@
use rhai::{Engine, INT, FLOAT, Dynamic, Map, Array, EvalAltResult, CustomType, TypeBuilder};
use rhai_wrapper::{
wrap_option_return, wrap_vec_return, wrap_option_vec_return,
wrap_option_return_result, wrap_vec_return_result, wrap_option_vec_return_result,
wrap_option_method_result, wrap_vec_method_result, wrap_option_vec_method_result,
ToRhaiMap, FromRhaiMap
};
use rhai_macros_derive::{ToRhaiMap as ToRhaiMapDerive, FromRhaiMap as FromRhaiMapDerive};
use std::fmt;
// Test structs
#[derive(Debug, Clone, PartialEq, CustomType, ToRhaiMapDerive, FromRhaiMapDerive)]
struct User {
id: INT,
name: String,
age: INT,
}
impl User {
fn to_rhai_map(&self) -> Map {
let mut map = Map::new();
map.insert("id".into(), self.id.into());
map.insert("name".into(), self.name.clone().into());
map.insert("age".into(), self.age.into());
map
}
fn from_rhai_map(map: Map) -> Result<Self, String> {
let id = map.get("id")
.and_then(|d| d.as_int().ok())
.ok_or_else(|| "Field 'id' not found or not an INT".to_string())?;
let name = map.get("name")
.and_then(|d| d.clone().into_string().ok())
.ok_or_else(|| "Field 'name' not found or not a String".to_string())?;
let age = map.get("age")
.and_then(|d| d.as_int().ok())
.ok_or_else(|| "Field 'age' not found or not an INT".to_string())?;
Ok(User { id, name, age })
}
}
// Mock DB connection type
struct OurDB {
// Some mock state
error_mode: bool,
}
impl OurDB {
fn new(error_mode: bool) -> Self {
OurDB { error_mode }
}
}
// Custom error type for testing
#[derive(Debug)]
struct DBError {
message: String,
}
impl fmt::Display for DBError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DB Error: {}", self.message)
}
}
// Test functions for wrap_option_return
fn get_user_by_id(_db: &OurDB, id: INT) -> Option<User> {
if id > 0 {
Some(User { id, name: format!("User {}", id), age: 30 })
} else {
None
}
}
// Test functions for wrap_vec_return
fn get_all_users(_db: &OurDB) -> Vec<User> {
vec![
User { id: 1, name: "User 1".to_string(), age: 30 },
User { id: 2, name: "User 2".to_string(), age: 25 },
]
}
fn get_users_by_age(_db: &OurDB, min_age: INT) -> Vec<User> {
vec![
User { id: 1, name: "User 1".to_string(), age: 30 },
User { id: 2, name: "User 2".to_string(), age: 25 },
].into_iter().filter(|u| u.age >= min_age).collect()
}
fn get_all_user_ids() -> Vec<INT> {
vec![1, 2, 3]
}
// Test functions for wrap_option_vec_return
fn find_users_by_name(_db: &OurDB, name_part: String) -> Option<Vec<User>> {
if name_part.is_empty() {
None
} else {
Some(vec![
User { id: 1, name: format!("{} One", name_part), age: 30 },
User { id: 2, name: format!("{} Two", name_part), age: 25 },
])
}
}
// Test functions for result-returning wrappers
fn get_user_by_id_result(db: &OurDB, id: INT) -> Result<Option<User>, DBError> {
if db.error_mode {
Err(DBError { message: "DB connection error".to_string() })
} else if id > 0 {
Ok(Some(User { id, name: format!("User {}", id), age: 30 }))
} else {
Ok(None)
}
}
fn get_all_users_result(db: &OurDB) -> Result<Vec<User>, DBError> {
if db.error_mode {
Err(DBError { message: "DB connection error".to_string() })
} else {
Ok(vec![
User { id: 1, name: "User 1".to_string(), age: 30 },
User { id: 2, name: "User 2".to_string(), age: 25 },
])
}
}
fn find_users_by_name_result(db: &OurDB, name_part: String) -> Result<Option<Vec<User>>, DBError> {
if db.error_mode {
Err(DBError { message: "DB connection error".to_string() })
} else if name_part.is_empty() {
Ok(None)
} else {
Ok(Some(vec![
User { id: 1, name: format!("{} One", name_part), age: 30 },
User { id: 2, name: format!("{} Two", name_part), age: 25 },
]))
}
}
// Test methods for method wrappers
struct UserCollection {
users: Vec<User>,
error_mode: bool,
}
impl UserCollection {
fn new(error_mode: bool) -> Self {
UserCollection {
users: vec![
User { id: 1, name: "User 1".to_string(), age: 30 },
User { id: 2, name: "User 2".to_string(), age: 25 },
],
error_mode,
}
}
fn get_by_id(&self, id: INT) -> Result<Option<User>, DBError> {
if self.error_mode {
Err(DBError { message: "Collection error".to_string() })
} else {
Ok(self.users.iter().find(|u| u.id == id).cloned())
}
}
fn get_all(&self) -> Result<Vec<User>, DBError> {
if self.error_mode {
Err(DBError { message: "Collection error".to_string() })
} else {
Ok(self.users.clone())
}
}
fn find_by_name(&self, name_part: String) -> Result<Option<Vec<User>>, DBError> {
if self.error_mode {
Err(DBError { message: "Collection error".to_string() })
} else if name_part.is_empty() {
Ok(None)
} else {
Ok(Some(self.users.iter()
.filter(|u| u.name.contains(&name_part))
.cloned()
.collect()))
}
}
}
#[test]
fn test_wrap_option_return() {
let mut engine = Engine::new();
let db = OurDB::new(false);
// Register the wrapped function
engine.register_fn(
"get_user_by_id_rhai",
wrap_option_return!(get_user_by_id, &OurDB, INT => User)
);
// Register the User type
engine.build_type::<User>();
// Test with existing user
let script1 = r#"
let user = get_user_by_id_rhai(db, 1);
user.id
"#;
let result1 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script1,
"user.id",
(db.clone(), )
).unwrap();
assert_eq!(result1, 1);
// Test with non-existing user
let script2 = r#"
let user = get_user_by_id_rhai(db, 0);
if user == () { 42 } else { 0 }
"#;
let result2 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script2,
"if user == () { 42 } else { 0 }",
(db, )
).unwrap();
assert_eq!(result2, 42);
}
#[test]
fn test_wrap_vec_return() {
let mut engine = Engine::new();
let db = OurDB::new(false);
// Register the wrapped functions
engine.register_fn(
"get_all_users_rhai",
wrap_vec_return!(get_all_users, &OurDB => User)
);
engine.register_fn(
"get_users_by_age_rhai",
wrap_vec_return!(get_users_by_age, &OurDB, INT => User)
);
engine.register_fn(
"get_all_user_ids_rhai",
wrap_vec_return!(get_all_user_ids, () => INT)
);
// Register the User type
engine.build_type::<User>();
// Test get_all_users
let script1 = r#"
let users = get_all_users_rhai(db);
users.len()
"#;
let result1 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script1,
"users.len()",
(db.clone(), )
).unwrap();
assert_eq!(result1, 2);
// Test get_users_by_age
let script2 = r#"
let users = get_users_by_age_rhai(db, 30);
users.len()
"#;
let result2 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script2,
"users.len()",
(db, )
).unwrap();
assert_eq!(result2, 1);
// Test get_all_user_ids
let script3 = r#"
let ids = get_all_user_ids_rhai();
ids.len()
"#;
let result3 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script3,
"ids.len()",
()
).unwrap();
assert_eq!(result3, 3);
}
#[test]
fn test_wrap_option_vec_return() {
let mut engine = Engine::new();
let db = OurDB::new(false);
// Register the wrapped function
engine.register_fn(
"find_users_by_name_rhai",
wrap_option_vec_return!(find_users_by_name, &OurDB, String => User)
);
// Register the User type
engine.build_type::<User>();
// Test with found users
let script1 = r#"
let users = find_users_by_name_rhai(db, "User");
users.len()
"#;
let result1 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script1,
"users.len()",
(db.clone(), )
).unwrap();
assert_eq!(result1, 2);
// Test with no users found
let script2 = r#"
let users = find_users_by_name_rhai(db, "");
if users == () { 42 } else { 0 }
"#;
let result2 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script2,
"if users == () { 42 } else { 0 }",
(db, )
).unwrap();
assert_eq!(result2, 42);
}
#[test]
fn test_wrap_option_return_result() {
let mut engine = Engine::new();
let db_ok = OurDB::new(false);
let db_err = OurDB::new(true);
// Register the wrapped function
engine.register_result_fn(
"get_user_by_id_result_rhai",
wrap_option_return_result!(get_user_by_id_result, &OurDB, INT => User, DBError)
);
// Register the User type
engine.build_type::<User>();
// Test with existing user
let script1 = r#"
let user = get_user_by_id_result_rhai(db, 1);
user.id
"#;
let result1 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script1,
"user.id",
(db_ok.clone(), )
).unwrap();
assert_eq!(result1, 1);
// Test with non-existing user
let script2 = r#"
let user = get_user_by_id_result_rhai(db, 0);
if user == () { 42 } else { 0 }
"#;
let result2 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script2,
"if user == () { 42 } else { 0 }",
(db_ok.clone(), )
).unwrap();
assert_eq!(result2, 42);
// Test with error
let script3 = r#"
try {
let user = get_user_by_id_result_rhai(db, 1);
0
} catch(err) {
if err.contains("DB connection error") { 99 } else { 0 }
}
"#;
let result3 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script3,
"try_catch_block",
(db_err, )
).unwrap();
assert_eq!(result3, 99);
}
#[test]
fn test_wrap_vec_return_result() {
let mut engine = Engine::new();
let db_ok = OurDB::new(false);
let db_err = OurDB::new(true);
// Register the wrapped function
engine.register_result_fn(
"get_all_users_result_rhai",
wrap_vec_return_result!(get_all_users_result, &OurDB => User, DBError)
);
// Register the User type
engine.build_type::<User>();
// Test with successful result
let script1 = r#"
let users = get_all_users_result_rhai(db);
users.len()
"#;
let result1 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script1,
"users.len()",
(db_ok.clone(), )
).unwrap();
assert_eq!(result1, 2);
// Test with error
let script2 = r#"
try {
let users = get_all_users_result_rhai(db);
0
} catch(err) {
if err.contains("DB connection error") { 99 } else { 0 }
}
"#;
let result2 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script2,
"try_catch_block",
(db_err, )
).unwrap();
assert_eq!(result2, 99);
}
#[test]
fn test_wrap_option_vec_return_result() {
let mut engine = Engine::new();
let db_ok = OurDB::new(false);
let db_err = OurDB::new(true);
// Register the wrapped function
engine.register_result_fn(
"find_users_by_name_result_rhai",
wrap_option_vec_return_result!(find_users_by_name_result, &OurDB, String => User, DBError)
);
// Register the User type
engine.build_type::<User>();
// Test with found users
let script1 = r#"
let users = find_users_by_name_result_rhai(db, "User");
users.len()
"#;
let result1 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script1,
"users.len()",
(db_ok.clone(), )
).unwrap();
assert_eq!(result1, 2);
// Test with no users found
let script2 = r#"
let users = find_users_by_name_result_rhai(db, "");
if users == () { 42 } else { 0 }
"#;
let result2 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script2,
"if users == () { 42 } else { 0 }",
(db_ok.clone(), )
).unwrap();
assert_eq!(result2, 42);
// Test with error
let script3 = r#"
try {
let users = find_users_by_name_result_rhai(db, "User");
0
} catch(err) {
if err.contains("DB connection error") { 99 } else { 0 }
}
"#;
let result3 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script3,
"try_catch_block",
(db_err, )
).unwrap();
assert_eq!(result3, 99);
}
#[test]
fn test_wrap_option_method_result() {
let mut engine = Engine::new();
let collection_ok = UserCollection::new(false);
let collection_err = UserCollection::new(true);
// Register the wrapped method
engine.register_result_fn(
"get_by_id_rhai",
wrap_option_method_result!(get_by_id, &UserCollection, INT => User, DBError)
);
// Register the User type
engine.build_type::<User>();
// Test with existing user
let script1 = r#"
let user = get_by_id_rhai(collection, 1);
user.id
"#;
let result1 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script1,
"user.id",
(collection_ok.clone(), )
).unwrap();
assert_eq!(result1, 1);
// Test with non-existing user
let script2 = r#"
let user = get_by_id_rhai(collection, 999);
if user == () { 42 } else { 0 }
"#;
let result2 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script2,
"if user == () { 42 } else { 0 }",
(collection_ok.clone(), )
).unwrap();
assert_eq!(result2, 42);
// Test with error
let script3 = r#"
try {
let user = get_by_id_rhai(collection, 1);
0
} catch(err) {
if err.contains("Collection error") { 99 } else { 0 }
}
"#;
let result3 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script3,
"try_catch_block",
(collection_err, )
).unwrap();
assert_eq!(result3, 99);
}
#[test]
fn test_wrap_vec_method_result() {
let mut engine = Engine::new();
let collection_ok = UserCollection::new(false);
let collection_err = UserCollection::new(true);
// Register the wrapped method
engine.register_result_fn(
"get_all_rhai",
wrap_vec_method_result!(get_all, &UserCollection => User, DBError)
);
// Register the User type
engine.build_type::<User>();
// Test with successful result
let script1 = r#"
let users = get_all_rhai(collection);
users.len()
"#;
let result1 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script1,
"users.len()",
(collection_ok.clone(), )
).unwrap();
assert_eq!(result1, 2);
// Test with error
let script2 = r#"
try {
let users = get_all_rhai(collection);
0
} catch(err) {
if err.contains("Collection error") { 99 } else { 0 }
}
"#;
let result2 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script2,
"try_catch_block",
(collection_err, )
).unwrap();
assert_eq!(result2, 99);
}
#[test]
fn test_wrap_option_vec_method_result() {
let mut engine = Engine::new();
let collection_ok = UserCollection::new(false);
let collection_err = UserCollection::new(true);
// Register the wrapped method
engine.register_result_fn(
"find_by_name_rhai",
wrap_option_vec_method_result!(find_by_name, &UserCollection, String => User, DBError)
);
// Register the User type
engine.build_type::<User>();
// Test with found users
let script1 = r#"
let users = find_by_name_rhai(collection, "User");
users.len()
"#;
let result1 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script1,
"users.len()",
(collection_ok.clone(), )
).unwrap();
assert_eq!(result1, 2);
// Test with no users found
let script2 = r#"
let users = find_by_name_rhai(collection, "");
if users == () { 42 } else { 0 }
"#;
let result2 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script2,
"if users == () { 42 } else { 0 }",
(collection_ok.clone(), )
).unwrap();
assert_eq!(result2, 42);
// Test with error
let script3 = r#"
try {
let users = find_by_name_rhai(collection, "User");
0
} catch(err) {
if err.contains("Collection error") { 99 } else { 0 }
}
"#;
let result3 = engine.call_fn::<INT>(
&mut rhai::Scope::new(),
script3,
"try_catch_block",
(collection_err, )
).unwrap();
assert_eq!(result3, 99);
}