more efforts to automate rhai bindings

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

View File

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