diff --git a/_archive/lib.rs b/_archive/lib.rs new file mode 100644 index 0000000..d88bb9e --- /dev/null +++ b/_archive/lib.rs @@ -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 = Vec::new(); // For let converted_arg_0 = ... + let mut original_fn_call_args: Vec = Vec::new(); // For fn_name(converted_arg_0, ...) + + let mut wrapper_args_dynamic_defs: Vec = 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::() + .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 = #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 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 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> + let vec_element_ty = vec_inner_ty_opt.expect("Option> 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 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 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::, String>>() + }).transpose()? + } + } else { // Option + 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 + 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::, 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 { + #(#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 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 diff --git a/rhai_autobind_macros/Cargo.lock b/rhai_autobind_macros/Cargo.lock new file mode 100644 index 0000000..d4c5843 --- /dev/null +++ b/rhai_autobind_macros/Cargo.lock @@ -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" diff --git a/rhai_autobind_macros/Cargo.toml b/rhai_autobind_macros/Cargo.toml new file mode 100644 index 0000000..f0b32ac --- /dev/null +++ b/rhai_autobind_macros/Cargo.toml @@ -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" diff --git a/rhai_autobind_macros/examples/rhai_autobind_example/calculator.rhai b/rhai_autobind_macros/examples/rhai_autobind_example/calculator.rhai new file mode 100644 index 0000000..13ea8f3 --- /dev/null +++ b/rhai_autobind_macros/examples/rhai_autobind_example/calculator.rhai @@ -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); diff --git a/rhai_autobind_macros/examples/rhai_autobind_example/main.rs b/rhai_autobind_macros/examples/rhai_autobind_example/main.rs new file mode 100644 index 0000000..d1c2423 --- /dev/null +++ b/rhai_autobind_macros/examples/rhai_autobind_example/main.rs @@ -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> { + 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(()) +} diff --git a/rhai_autobind_macros/src/lib.rs b/rhai_autobind_macros/src/lib.rs new file mode 100644 index 0000000..e0ac757 --- /dev/null +++ b/rhai_autobind_macros/src/lib.rs @@ -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_`, +/// 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")] +/// 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"`. +/// +/// - `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, +} + +impl syn::parse::Parse for MacroArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut db_type_str: Option = None; + let mut collection_name_str: Option = 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) +} diff --git a/rhai_wrapper/Cargo.lock b/rhai_wrapper/Cargo.lock index 32a73c8..08cd517 100644 --- a/rhai_wrapper/Cargo.lock +++ b/rhai_wrapper/Cargo.lock @@ -16,6 +16,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -28,12 +43,42 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cc" +version = "1.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "const-random" version = "0.1.18" @@ -54,6 +99,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crunchy" version = "0.2.3" @@ -83,6 +134,30 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "instant" version = "0.1.13" @@ -92,12 +167,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "num-traits" version = "0.2.19" @@ -187,10 +278,44 @@ dependencies = [ name = "rhai_wrapper" version = "0.1.0" dependencies = [ + "chrono", "rhai", "rhai_macros_derive", + "serde", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "smallvec" version = "1.15.0" @@ -267,6 +392,123 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/rhai_wrapper/Cargo.toml b/rhai_wrapper/Cargo.toml index c12553e..bed394d 100644 --- a/rhai_wrapper/Cargo.toml +++ b/rhai_wrapper/Cargo.toml @@ -6,7 +6,17 @@ edition = "2021" description = "A wrapper to make generic Rust functions Rhai-compatible." [dependencies] -rhai = "1.21.0" +rhai = "1.18.0" rhai_macros_derive = { path = "../rhai_macros_derive" } +serde = { version = "1.0", features = ["derive"] } +chrono = { version = "0.4", features = ["serde"] } [dev-dependencies] + +[[example]] +name = "user_management_example" +path = "examples/user_management_example.rs" + +[[example]] +name = "rust_rhai_wrapper_example" +path = "examples/rust_rhai_wrapper_example.rs" diff --git a/rhai_wrapper/examples/rust_rhai_wrapper_example.rs b/rhai_wrapper/examples/rust_rhai_wrapper_example.rs new file mode 100644 index 0000000..4e96e95 --- /dev/null +++ b/rhai_wrapper/examples/rust_rhai_wrapper_example.rs @@ -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 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 { + 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::(); + 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::().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!"); +} diff --git a/rhai_wrapper/examples/user_management_example.rs b/rhai_wrapper/examples/user_management_example.rs new file mode 100644 index 0000000..fdfc62d --- /dev/null +++ b/rhai_wrapper/examples/user_management_example.rs @@ -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, +} + +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 { 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 { 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, + comments: HashMap, +} + +type OurDB = Arc>; + +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 { + let db = db_arc.lock().unwrap(); + db.users.get(&id).cloned() +} + +fn get_all_users(db_arc: OurDB) -> Vec { + 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 { + 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> { + let db = db_arc.lock().unwrap(); + let users: Vec = 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> { + 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") + // 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") + .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(()) +} diff --git a/rhai_wrapper/examples/user_script.rhai b/rhai_wrapper/examples/user_script.rhai new file mode 100644 index 0000000..95f3fb2 --- /dev/null +++ b/rhai_wrapper/examples/user_script.rhai @@ -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 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 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!"); diff --git a/rhai_wrapper/src/lib.rs b/rhai_wrapper/src/lib.rs index 0520514..ea9c086 100644 --- a/rhai_wrapper/src/lib.rs +++ b/rhai_wrapper/src/lib.rs @@ -208,3 +208,601 @@ macro_rules! wrap_for_rhai { $func }; } + +/// Macro to wrap a Rust function that returns Option 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 + // Example: get_user_by_id(OurDB, INT) -> Option + // 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 + // 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 for Rhai. +/// It converts the Vec into a rhai::Array. +#[macro_export] +macro_rules! wrap_vec_return { + // For functions like fn(Arg1) -> Vec + // Example: get_all_users(db: OurDB) -> Vec + // 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::() + } + }; + + // For functions like fn(Arg1, Arg2) -> Vec + ($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::() + } + }; + + // For functions like fn() -> Vec + ($func:ident, () => $InnerVecType:ty) => { + || -> rhai::Array { + let result_vec: std::vec::Vec<$InnerVecType> = $func(); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; +} + +#[macro_export] +macro_rules! wrap_option_vec_return { + // Case: fn_name(Arg1Type, Arg2Type) -> Option> + // 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::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, ErrorType>` for Rhai. +/// The generated closure returns `Result>`. +/// 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> { + 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> { + 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, ErrorType>` for Rhai. +/// The generated closure returns `Result>`. +/// 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> { + 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::(); + 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> { + 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::(); + 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>, ErrorType>` for Rhai. +/// The generated closure returns `Result>`. +/// 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> { + 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::(); + 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> { + 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::(); + 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, ErrorType>` for Rhai. +/// The generated closure returns `Result>`. +/// 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> { + 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> { + 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, ErrorType>` for Rhai. +/// The generated closure returns `Result>`. +/// 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> { + 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::(); + 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> { + 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::(); + 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>, ErrorType>` for Rhai. +/// The generated closure returns `Result>`. +/// 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> { + 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::(); + 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> { + 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::(); + 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. diff --git a/rhai_wrapper/tests/integration.rs b/rhai_wrapper/tests/integration.rs index c438466..cb85d07 100644 --- a/rhai_wrapper/tests/integration.rs +++ b/rhai_wrapper/tests/integration.rs @@ -1,15 +1,23 @@ use rhai_wrapper::wrap_for_rhai; use rhai_wrapper::{ToRhaiMap, FromRhaiMap}; 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 } +#[export_fn(rhai_name = "mul_rhai")] fn mul(a: INT, b: INT) -> INT { a * b } +#[export_fn(rhai_name = "greet_rhai")] fn greet(name: String) -> String { format!("Hello, {name}!") } +#[export_fn(rhai_name = "get_forty_two_rhai")] fn get_forty_two() -> INT { 42 } +#[export_fn(rhai_name = "shout_rhai")] fn shout() -> String { "HEY!".to_string() } +#[export_fn(rhai_name = "add_float_rhai")] 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 } +#[export_fn(rhai_name = "maybe_add_rhai")] fn maybe_add(a: INT, b: INT, do_add: bool) -> Option { if do_add { Some(a + b) } else { None } } // Renamed from sum_vec, takes rhai::Array @@ -110,69 +118,83 @@ fn get_polygon_id_and_num_vertices(poly: Polygon) -> String { #[test] fn test_add() { let mut engine = Engine::new(); - engine.register_fn("add", wrap_for_rhai!(add)); - let result = engine.eval::("add(2, 3)").unwrap(); + engine.register_fn("add_rhai", add_rhai_wrapper); + let result = engine.eval::("add_rhai(2, 3)").unwrap(); assert_eq!(result, 5); } #[test] fn test_mul() { let mut engine = Engine::new(); - engine.register_fn("mul", wrap_for_rhai!(mul)); - let result = engine.eval::("mul(4, 5)").unwrap(); + engine.register_fn("mul_rhai", mul_rhai_wrapper); + let result = engine.eval::("mul_rhai(4, 5)").unwrap(); assert_eq!(result, 20); } #[test] fn test_greet() { let mut engine = Engine::new(); - engine.register_fn("greet", wrap_for_rhai!(greet)); - let result = engine.eval::(r#"greet("Alice")"#).unwrap(); + engine.register_fn("greet_rhai", greet_rhai_wrapper); + let result = engine.eval::(r#"greet_rhai("Alice")"#).unwrap(); assert_eq!(result, "Hello, Alice!"); } #[test] fn test_get_forty_two() { let mut engine = Engine::new(); - engine.register_fn("get_forty_two", wrap_for_rhai!(get_forty_two)); - let result = engine.eval::("get_forty_two()").unwrap(); + engine.register_fn("get_forty_two_rhai", get_forty_two_rhai_wrapper); + let result = engine.eval::("get_forty_two_rhai()").unwrap(); assert_eq!(result, 42); } #[test] fn test_shout() { let mut engine = Engine::new(); - engine.register_fn("shout", wrap_for_rhai!(shout)); - let result = engine.eval::("shout()").unwrap(); + engine.register_fn("shout_rhai", shout_rhai_wrapper); + let result = engine.eval::("shout_rhai()").unwrap(); assert_eq!(result, "HEY!"); } #[test] fn test_add_float() { let mut engine = Engine::new(); - engine.register_fn("add_float", wrap_for_rhai!(add_float)); - let result = engine.eval::("add_float(1.5, 2.25)").unwrap(); - assert!((result - 3.75).abs() < 1e-8); + engine.register_fn("add_float_rhai", add_float_rhai_wrapper); + let result = engine.eval::("add_float_rhai(2.5, 3.5)").unwrap(); + assert_eq!(result, 6.0); } #[test] fn test_is_even() { let mut engine = Engine::new(); - engine.register_fn("is_even", wrap_for_rhai!(is_even)); - let result = engine.eval::("is_even(4)").unwrap(); - assert!(result); - let result = engine.eval::("is_even(5)").unwrap(); - assert!(!result); + engine.register_fn("is_even_rhai", is_even_rhai_wrapper); + let result_true = engine.eval::("is_even_rhai(4)").unwrap(); + assert_eq!(result_true, true); + let result_false = engine.eval::("is_even_rhai(3)").unwrap(); + assert_eq!(result_false, false); } #[test] fn test_maybe_add() { let mut engine = Engine::new(); - engine.register_fn("maybe_add", wrap_for_rhai!(maybe_add)); - let result = engine.eval::>("maybe_add(2, 3, true)").unwrap(); - assert_eq!(result, Some(5)); - let result = engine.eval::>("maybe_add(2, 3, false)").unwrap(); - assert_eq!(result, None); + engine.register_fn("maybe_add_rhai", maybe_add_rhai_wrapper); + + // Test case where None is returned (expecting an error or specific handling in Rhai) + // Rhai treats Option::None as an empty Dynamic, which can lead to type mismatch if not handled. + // 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::("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::("maybe_add_rhai(2, 3, true)").unwrap(); + assert_eq!(result_some, 5); } #[test] @@ -502,21 +524,20 @@ mod new_export_fn_tests { assert_eq!(result, 15); } - // #[test] - // fn test_export_fn_custom_type_arg_return() { // This test was commented out, keeping as is for now - // let mut engine = Engine::new(); - // engine.build_type::(); - // // engine.register_fn("offset_simple_point", offset_simple_point_rhai_wrapper); + #[test] + fn test_export_fn_custom_type_arg_return_new() { + let mut engine = Engine::new(); + engine.build_type::(); + engine.register_fn("offset_simple_point", offset_simple_point_rhai_wrapper); - // // let script = r#" - // // let p = #{ x: 10, y: 20 }; - // // let p_offset = offset_simple_point(p, 5); - // // p_offset.x - // // "#; - // // let result = engine.eval::(script).unwrap(); - // // assert_eq!(result, 15); - - // } + let script = r#" + let p = #{ x: 10, y: 20 }; + let p_offset = offset_simple_point(p, 5); + p_offset.x + "#; + let result = engine.eval::(script).unwrap(); + assert_eq!(result, 15); + } #[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()[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::(); - engine.register_fn("offset_simple_point", offset_simple_point_rhai_wrapper); - - let script = r#" - 42 - "#; - let result = engine.eval::(script).unwrap(); - assert_eq!(result, 42); - } } diff --git a/rhai_wrapper/tests/wrapper_macros_test.rs b/rhai_wrapper/tests/wrapper_macros_test.rs new file mode 100644 index 0000000..124835e --- /dev/null +++ b/rhai_wrapper/tests/wrapper_macros_test.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + vec![1, 2, 3] +} + +// Test functions for wrap_option_vec_return +fn find_users_by_name(_db: &OurDB, name_part: String) -> Option> { + 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, 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, 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>, 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, + 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, 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, 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>, 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::(); + + // Test with existing user + let script1 = r#" + let user = get_user_by_id_rhai(db, 1); + user.id + "#; + + let result1 = engine.call_fn::( + &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::( + &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::(); + + // Test get_all_users + let script1 = r#" + let users = get_all_users_rhai(db); + users.len() + "#; + + let result1 = engine.call_fn::( + &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::( + &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::( + &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::(); + + // Test with found users + let script1 = r#" + let users = find_users_by_name_rhai(db, "User"); + users.len() + "#; + + let result1 = engine.call_fn::( + &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::( + &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::(); + + // Test with existing user + let script1 = r#" + let user = get_user_by_id_result_rhai(db, 1); + user.id + "#; + + let result1 = engine.call_fn::( + &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::( + &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::( + &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::(); + + // Test with successful result + let script1 = r#" + let users = get_all_users_result_rhai(db); + users.len() + "#; + + let result1 = engine.call_fn::( + &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::( + &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::(); + + // Test with found users + let script1 = r#" + let users = find_users_by_name_result_rhai(db, "User"); + users.len() + "#; + + let result1 = engine.call_fn::( + &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::( + &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::( + &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::(); + + // Test with existing user + let script1 = r#" + let user = get_by_id_rhai(collection, 1); + user.id + "#; + + let result1 = engine.call_fn::( + &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::( + &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::( + &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::(); + + // Test with successful result + let script1 = r#" + let users = get_all_rhai(collection); + users.len() + "#; + + let result1 = engine.call_fn::( + &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::( + &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::(); + + // Test with found users + let script1 = r#" + let users = find_by_name_rhai(collection, "User"); + users.len() + "#; + + let result1 = engine.call_fn::( + &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::( + &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::( + &mut rhai::Scope::new(), + script3, + "try_catch_block", + (collection_err, ) + ).unwrap(); + + assert_eq!(result3, 99); +}