create macros for generating rhai wrappers and add tests

This commit is contained in:
timurgordon
2025-05-12 02:31:45 +03:00
parent 22032f329a
commit 16ad4f5743
11 changed files with 2735 additions and 0 deletions

View File

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