diff --git a/adapter_macros/Cargo.toml b/adapter_macros/Cargo.toml new file mode 100644 index 0000000..27b2318 --- /dev/null +++ b/adapter_macros/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "adapter_macros" +version = "0.1.0" +edition = "2021" + +[dependencies] +rhai = "1.21.0" +chrono = "0.4" diff --git a/adapter_macros/README.md b/adapter_macros/README.md new file mode 100644 index 0000000..015af5d --- /dev/null +++ b/adapter_macros/README.md @@ -0,0 +1,77 @@ +# Rhai Adapter Macros (`adapter_macros`) + +This crate provides utility macros to facilitate the integration of Rust code with the Rhai scripting engine, particularly for adapting function and method signatures. + +## Purpose + +Rhai often uses `i64` as its default integer type. When exposing Rust functions or methods that use other integer types (e.g., `u32`, `usize`), direct registration can lead to type mismatches or require manual conversion boilerplate in each registered function. + +These macros help bridge this gap by wrapping your Rust functions/methods, automatically handling the conversion from Rhai's `i64` to the Rust-native integer type and providing more informative error messages if the conversion fails (e.g., due to overflow). + +## Macros + +### 1. `adapt_rhai_i64_input_fn!(rust_fn:path, rust_int_ty:ty)` + +Adapts a standalone Rust function that takes a single argument of `rust_int_ty` and returns `Result<_, Box>`. + +- `rust_fn`: The path to your Rust function (e.g., `my_module::my_function`). +- `rust_int_ty`: The integer type your Rust function expects (e.g., `u32`). + +**Example Usage in Rust (when registering with Rhai Engine):** + +```rust +// In your Rust code where you set up the Rhai engine: +// Assuming your_function(val: u32) -> Result exists +// and adapter_macros is a dependency. + +engine.register_fn("my_rhai_func", adapter_macros::adapt_rhai_i64_input_fn!(my_module::your_function, u32)); + +// In Rhai script: +// my_rhai_func(10); // 10 (i64) will be converted to u32 for your_function +``` + +### 2. `adapt_rhai_i64_input_method!(struct_ty:ty, rust_method_name:ident, rust_int_ty:ty)` + +Adapts a Rust instance method that takes `self` by value, a single integer argument of `rust_int_ty`, and returns `Self`. This is useful for builder-like patterns or methods that modify and return the instance. + +- `struct_ty`: The type of the struct on which the method is defined (e.g., `MyStruct`). +- `rust_method_name`: The identifier of the Rust method (e.g., `with_value`). +- `rust_int_ty`: The integer type your Rust method's argument expects (e.g., `u16`). + +**Example Usage in Rust (when registering with Rhai Engine):** + +```rust +// In your Rust code: +// Assuming MyStruct has a method: fn with_value(self, val: u16) -> Self +// and adapter_macros is a dependency. + +engine.register_fn("with_value", adapter_macros::adapt_rhai_i64_input_method!(MyStruct, with_value, u16)); + +// In Rhai script: +// let my_obj = MyStruct::new(); +// my_obj.with_value(5); // 5 (i64) will be converted to u16 for MyStruct::with_value +``` + +## Error Handling + +If the `i64` value from Rhai cannot be converted to `rust_int_ty` (e.g., an `i64` value of -1 is passed when `u32` is expected, or a value too large for `u16`), the macros will generate a `rhai::EvalAltResult::ErrorArithmetic` with a descriptive message and the script position. + +## How It Works + +The macros generate a closure that: +1. Takes a `rhai::NativeCallContext` and an `i64` from the Rhai engine. +2. Attempts to convert the `i64` to the specified `rust_int_ty` using `try_into()`. +3. If conversion fails, it returns an `ErrorArithmetic` detailing the function/method name and the type conversion that failed. +4. If conversion succeeds, it calls the original Rust function/method with the converted value. + +## Adding to Your Project + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +adapter_macros = { path = "../adapter_macros" } # Or version = "0.1.0" if published +rhai = "x.y.z" # Your rhai version +``` + +Ensure the path to `adapter_macros` is correct if used locally. diff --git a/adapter_macros/src/lib.rs b/adapter_macros/src/lib.rs new file mode 100644 index 0000000..68676e4 --- /dev/null +++ b/adapter_macros/src/lib.rs @@ -0,0 +1,184 @@ +/// Macro to adapt a Rust function taking a specific integer type (e.g., u32) +/// to a Rhai function that expects an i64 argument. +#[macro_export] +macro_rules! adapt_rhai_i64_input_fn { + ($rust_fn:path, $rust_int_ty:ty) => { + move |context: ::rhai::NativeCallContext, rhai_val: i64| -> Result<_, Box<::rhai::EvalAltResult>> { + let rust_val: $rust_int_ty = rhai_val.try_into().map_err(|_e| { + Box::new(::rhai::EvalAltResult::ErrorArithmetic( + format!("Conversion error for arg in '{}' from i64 to {}", stringify!($rust_fn), stringify!($rust_int_ty)), + context.position(), + )) + })?; + Ok($rust_fn(rust_val)) + } + }; +} + +/// Macro to adapt a Rust instance method (taking self by value, one int arg, returns Self) +/// to a Rhai function that expects an i64 for that integer argument. +#[macro_export] +macro_rules! adapt_rhai_i64_input_method { + ($struct_ty:ty, $rust_method_name:ident, $rust_int_ty:ty) => { + move |context: ::rhai::NativeCallContext, instance: $struct_ty, rhai_val: i64| -> Result<$struct_ty, Box<::rhai::EvalAltResult>> { + let rust_val: $rust_int_ty = rhai_val.try_into().map_err(|_e| { + Box::new(::rhai::EvalAltResult::ErrorArithmetic( + format!("Conversion error for arg in '{}::{}' from i64 to {}", stringify!($struct_ty), stringify!($rust_method_name), stringify!($rust_int_ty)), + context.position(), + )) + })?; + Ok(instance.$rust_method_name(rust_val)) + } + }; +} + +// --- Rhai Timestamp Helper Functions --- +pub mod rhai_timestamp_helpers { + use rhai::{INT, EvalAltResult, Position}; + use chrono::{DateTime, Utc, TimeZone}; + + pub fn datetime_to_rhai_timestamp(dt: &DateTime) -> INT { + dt.timestamp() + } + + pub fn option_datetime_to_rhai_timestamp(dt_opt: &Option>) -> Option { + dt_opt.as_ref().map(datetime_to_rhai_timestamp) + } + + pub fn rhai_timestamp_to_datetime(ts: INT) -> Result, Box> { + Utc.timestamp_opt(ts, 0).single() + .ok_or_else(|| Box::new(EvalAltResult::ErrorArithmetic(format!("Invalid Unix timestamp: {}", ts), Position::NONE))) + } + + pub fn option_rhai_timestamp_to_datetime(ts_opt: Option) -> Result>, Box> { + match ts_opt { + Some(ts) => rhai_timestamp_to_datetime(ts).map(Some), + None => Ok(None), + } + } +} + +// --- Macro for Enum Accessors (String Conversion) --- +#[macro_export] +macro_rules! register_rhai_enum_accessors { + ($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr, $to_string_fn:path, $from_string_fn:path) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> rhai::ImmutableString { $to_string_fn(&obj.$field_name) }, + move |obj: &mut $struct_type, val: rhai::ImmutableString| -> Result<(), Box> { + obj.$field_name = $from_string_fn(val.as_str())?; + Ok(()) + } + ); + }; +} + +// --- Macro for DateTime Accessors (Unix Timestamp INT) --- +#[macro_export] +macro_rules! register_rhai_datetime_accessors { + ($engine:expr, $struct_type:ty, $field_path:ident, $rhai_name:expr, _required) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> rhai::INT { + let field_value = &obj.$field_path; + $crate::rhai_timestamp_helpers::datetime_to_rhai_timestamp(field_value) + }, + move |obj: &mut $struct_type, val: rhai::INT| -> Result<(), Box> { + obj.$field_path = $crate::rhai_timestamp_helpers::rhai_timestamp_to_datetime(val)?; + Ok(()) + } + ); + }; + ($engine:expr, $struct_type:ty, base_data.$field_name:ident, $rhai_name:expr, _required) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> rhai::INT { + let field_value = &obj.base_data.$field_name; + $crate::rhai_timestamp_helpers::datetime_to_rhai_timestamp(field_value) + }, + move |obj: &mut $struct_type, val: rhai::INT| -> Result<(), Box> { + obj.base_data.$field_name = $crate::rhai_timestamp_helpers::rhai_timestamp_to_datetime(val)?; + Ok(()) + } + ); + }; + ($engine:expr, $struct_type:ty, $field_path:ident, $rhai_name:expr) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> Option { + let field_value = &obj.$field_path; + $crate::rhai_timestamp_helpers::option_datetime_to_rhai_timestamp(field_value) + }, + move |obj: &mut $struct_type, val_opt: Option| -> Result<(), Box> { + obj.$field_path = $crate::rhai_timestamp_helpers::option_rhai_timestamp_to_datetime(val_opt)?; + Ok(()) + } + ); + }; + ($engine:expr, $struct_type:ty, base_data.$field_name:ident, $rhai_name:expr) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> Option { + let field_value = &obj.base_data.$field_name; + $crate::rhai_timestamp_helpers::option_datetime_to_rhai_timestamp(field_value) + }, + move |obj: &mut $struct_type, val_opt: Option| -> Result<(), Box> { + obj.base_data.$field_name = $crate::rhai_timestamp_helpers::option_rhai_timestamp_to_datetime(val_opt)?; + Ok(()) + } + ); + }; +} + +// --- Macro for Vec Accessors --- +#[macro_export] +macro_rules! register_rhai_vec_string_accessors { + ($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| -> rhai::Array { + obj.$field_name.iter().map(|s| rhai::Dynamic::from(rhai::ImmutableString::from(s.as_str()))).collect() + }, + move |obj: &mut $struct_type, val: rhai::Array| { + obj.$field_name = val.into_iter().map(|d| d.into_string().unwrap_or_default()).collect(); + } + ); + }; +} + +// --- Macro for Generic Field Accessors (Example: ImmutableString) --- +#[macro_export] +macro_rules! register_rhai_field_accessors { + ($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| obj.$field_name.clone(), // Assuming cloneable and directly Rhai compatible + move |obj: &mut $struct_type, val: rhai::Dynamic| { // Or specific type like ImmutableString + // This part would need more specific handling based on expected type + // For example, if it's always ImmutableString: + // if let Ok(s) = val.into_immutable_string() { obj.$field_name = s.into_owned(); } + // For now, let's assume it's a type that can be directly assigned from Dynamic if Dynamic holds the right type + // This is a simplification; real use might need obj.$field_name = val.try_cast().unwrap_or_default(); + // However, register_get_set usually infers setter type from getter type. + // If getter is T, setter is fn(&mut S, T) + // So if getter is |obj| obj.field.clone() -> String, setter should be |obj, val: String| + // Let's assume string for now if using ImmutableString for Rhai + if let Ok(s_val) = val.into_immutable_string() { + obj.$field_name = s_val.into(); // Assumes field_name is String + } else { + // Handle error or default + eprintln!("Failed to cast for field {}", $rhai_name); + } + } + ); + }; + ($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr, $rhai_type:ty) => { + $engine.register_get_set( + $rhai_name, + move |obj: &mut $struct_type| obj.$field_name.clone(), + move |obj: &mut $struct_type, val: $rhai_type| { + obj.$field_name = val; + } + ); + }; +}