From 16ad4f5743883ba81ed7918d03b63099d51e15ee Mon Sep 17 00:00:00 2001 From: timurgordon Date: Mon, 12 May 2025 02:31:45 +0300 Subject: [PATCH] create macros for generating rhai wrappers and add tests --- rhai_macros_derive/Cargo.lock | 290 +++++++++++++++ rhai_macros_derive/Cargo.toml | 15 + rhai_macros_derive/README.md | 200 +++++++++++ rhai_macros_derive/src/lib.rs | 429 ++++++++++++++++++++++ rhai_wrapper/Cargo.lock | 297 ++++++++++++++++ rhai_wrapper/Cargo.toml | 12 + rhai_wrapper/README.md | 162 +++++++++ rhai_wrapper/src/lib.rs | 210 +++++++++++ rhai_wrapper/src/lib.rs.bak | 193 ++++++++++ rhai_wrapper/src/lib.rs.bak2 | 355 ++++++++++++++++++ rhai_wrapper/tests/integration.rs | 572 ++++++++++++++++++++++++++++++ 11 files changed, 2735 insertions(+) create mode 100644 rhai_macros_derive/Cargo.lock create mode 100644 rhai_macros_derive/Cargo.toml create mode 100644 rhai_macros_derive/README.md create mode 100644 rhai_macros_derive/src/lib.rs create mode 100644 rhai_wrapper/Cargo.lock create mode 100644 rhai_wrapper/Cargo.toml create mode 100644 rhai_wrapper/README.md create mode 100644 rhai_wrapper/src/lib.rs create mode 100644 rhai_wrapper/src/lib.rs.bak create mode 100644 rhai_wrapper/src/lib.rs.bak2 create mode 100644 rhai_wrapper/tests/integration.rs diff --git a/rhai_macros_derive/Cargo.lock b/rhai_macros_derive/Cargo.lock new file mode 100644 index 0000000..e826edc --- /dev/null +++ b/rhai_macros_derive/Cargo.lock @@ -0,0 +1,290 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[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 = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rhai" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" +dependencies = [ + "ahash", + "bitflags", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rhai_macros_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "rhai", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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 = "thin-vec" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rhai_macros_derive/Cargo.toml b/rhai_macros_derive/Cargo.toml new file mode 100644 index 0000000..5add56e --- /dev/null +++ b/rhai_macros_derive/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rhai_macros_derive" +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" + +# We might need rhai types for some advanced scenarios, but start without it +# rhai = { version = "1.21.0" } diff --git a/rhai_macros_derive/README.md b/rhai_macros_derive/README.md new file mode 100644 index 0000000..b7f20d6 --- /dev/null +++ b/rhai_macros_derive/README.md @@ -0,0 +1,200 @@ +# Rhai Macros Derive Crate + +This crate provides procedural macros to simplify the integration of custom Rust structs with the Rhai scripting engine, specifically for converting structs to and from `rhai::Map` objects. It is intended to be used alongside the `rhai_wrapper` crate. + +## Provided Macros + +- `#[derive(ToRhaiMap)]` +- `#[derive(FromRhaiMap)]` + +## Dependencies + +Make sure this crate is included in your `Cargo.toml` dependencies, typically as a local path dependency if working within the `rhaj` project: + +```toml +[dependencies] +rhai_macros_derive = { path = "../rhai_macros_derive" } +# ... other dependencies +``` + +And `rhai` itself: +```toml +[dependencies] +rhai = "" # e.g., "1.16.0" +``` + +## `#[derive(ToRhaiMap)]` + +This macro automatically generates an implementation of a `to_rhai_map(&self) -> rhai::Map` method for your struct. This method converts an instance of your struct into a `rhai::Map`, which can then be easily used within Rhai scripts as an object map. + +### Usage + +```rust +use rhai_macros_derive::ToRhaiMap; +use rhai::{INT, FLOAT, Map}; + +// Forward declaration for Point if used in Vec +// Typically Point would also derive ToRhaiMap and FromRhaiMap +#[derive(Debug, Clone, PartialEq, ToRhaiMap)] // Assuming Point also derives ToRhaiMap +struct Point { + x: INT, + y: INT, +} + +impl Point { // Minimal stub for example if not fully defined elsewhere + fn to_rhai_map(&self) -> Map { + let mut map = Map::new(); + map.insert("x".into(), self.x.into()); + map.insert("y".into(), self.y.into()); + map + } +} + +#[derive(ToRhaiMap)] +struct MyStruct { + id: INT, + name: String, + is_active: bool, + score: FLOAT, + position: Point, // Nested struct + tags: Vec, // Vec of primitives + history: Vec, // Vec of custom structs +} + +fn main() { + let p = Point { x: 10, y: 20 }; + let my_instance = MyStruct { + id: 1, + name: "Test".to_string(), + is_active: true, + score: 99.5, + position: p.clone(), + tags: vec!["alpha".to_string(), "beta".to_string()], + history: vec![Point { x: 1, y: 2 }, Point { x: 3, y: 4}], + }; + + let rhai_map = my_instance.to_rhai_map(); + + assert_eq!(rhai_map.get("id").unwrap().as_int().unwrap(), 1); + assert_eq!(rhai_map.get("name").unwrap().clone().into_string().unwrap(), "Test"); + assert_eq!(rhai_map.get("is_active").unwrap().as_bool().unwrap(), true); + assert_eq!(rhai_map.get("score").unwrap().as_float().unwrap(), 99.5); + + let pos_map = rhai_map.get("position").unwrap().clone().try_cast::().unwrap(); + assert_eq!(pos_map.get("x").unwrap().as_int().unwrap(), 10); + + let tags_array = rhai_map.get("tags").unwrap().clone().try_cast::().unwrap(); + assert_eq!(tags_array.len(), 2); + assert_eq!(tags_array[0].clone().into_string().unwrap(), "alpha"); + + let history_array = rhai_map.get("history").unwrap().clone().try_cast::().unwrap(); + assert_eq!(history_array.len(), 2); + let hist_p1_map = history_array[0].clone().try_cast::().unwrap(); + assert_eq!(hist_p1_map.get("x").unwrap().as_int().unwrap(), 1); +} +``` + +### How it works: + +- **Primitive Types**: Fields like `INT`, `i64`, `String`, `FLOAT`, `f64`, and `bool` are cloned and converted into their `rhai::Dynamic` equivalents. +- **Nested Structs**: If a field is another struct (e.g., `position: Point`), that struct must also implement `to_rhai_map()`. The macro will call `self.field_name.to_rhai_map()` for that field. +- **`Vec` Fields**: + - If `T` is a primitive type (e.g., `Vec`), each element is cloned and converted to `rhai::Dynamic`, then collected into a `rhai::Array`. + - If `T` is a custom struct (e.g., `Vec`), `item.to_rhai_map()` is called for each element, and the resulting `rhai::Map`s are collected into a `rhai::Array`. + +## `#[derive(FromRhaiMap)]` + +This macro automatically generates an implementation of `from_rhai_map(map: rhai::Map) -> Result` for your struct. This method attempts to construct an instance of your struct from a `rhai::Map`. + +### Usage + +```rust +use rhai_macros_derive::FromRhaiMap; +use rhai::{INT, FLOAT, Map, Array, Dynamic}; + +// Assuming Point also derives FromRhaiMap and has a from_rhai_map method +#[derive(Debug, Clone, PartialEq, FromRhaiMap)] +struct Point { + x: INT, + y: INT, +} + +impl Point { // Minimal stub for example + fn from_rhai_map(mut map: Map) -> Result { + Ok(Point { + x: map.get("x").and_then(|d| d.as_int().ok()).ok_or("x missing")?, + y: map.get("y").and_then(|d| d.as_int().ok()).ok_or("y missing")?, + }) + } +} + +#[derive(FromRhaiMap, Debug, PartialEq)] // Added Debug, PartialEq for assert +struct MyStruct { + id: INT, + name: String, + is_active: bool, + score: FLOAT, + position: Point, // Nested struct + tags: Vec, // Vec of primitives + history: Vec, // Vec of custom structs +} + +fn main() { + let mut map = Map::new(); + map.insert("id".into(), (1 as INT).into()); + map.insert("name".into(), "Test".to_string().into()); + map.insert("is_active".into(), true.into()); + map.insert("score".into(), (99.5 as FLOAT).into()); + + let mut pos_map = Map::new(); + pos_map.insert("x".into(), (10 as INT).into()); + pos_map.insert("y".into(), (20 as INT).into()); + map.insert("position".into(), pos_map.into()); + + let tags_array: Array = vec![Dynamic::from("alpha".to_string()), Dynamic::from("beta".to_string())]; + map.insert("tags".into(), tags_array.into()); + + let mut hist_p1_map = Map::new(); + hist_p1_map.insert("x".into(), (1 as INT).into()); + hist_p1_map.insert("y".into(), (2 as INT).into()); + let mut hist_p2_map = Map::new(); + hist_p2_map.insert("x".into(), (3 as INT).into()); + hist_p2_map.insert("y".into(), (4 as INT).into()); + let history_array: Array = vec![Dynamic::from(hist_p1_map), Dynamic::from(hist_p2_map)]; + map.insert("history".into(), history_array.into()); + + let my_instance = MyStruct::from_rhai_map(map).unwrap(); + + assert_eq!(my_instance.id, 1); + assert_eq!(my_instance.name, "Test"); + assert_eq!(my_instance.position, Point { x: 10, y: 20 }); + assert_eq!(my_instance.tags, vec!["alpha".to_string(), "beta".to_string()]); + assert_eq!(my_instance.history, vec![Point { x: 1, y: 2 }, Point { x: 3, y: 4}]); +} +``` + +### How it works: + +- **Primitive Types**: For fields like `INT`, `String`, etc., the macro attempts to retrieve the value from the input `rhai::Map` by its key (field name) and convert it to the expected Rust type (e.g., using `as_int()`, `into_string()`). +- **Nested Structs**: If a field is another struct, it retrieves the corresponding value as a `rhai::Dynamic`, tries to cast it to `rhai::Map`, and then calls `NestedStructType::from_rhai_map()` on that sub-map. +- **`Vec` Fields**: + - Retrieves the value as `rhai::Dynamic`, casts it to `rhai::Array`. + - If `T` is a primitive type, it iterates the array, converting each `rhai::Dynamic` element to `T`. + - If `T` is a custom struct, it iterates the array, casting each `rhai::Dynamic` element to `rhai::Map`, and then calls `T::from_rhai_map()` on each sub-map. +- **Error Handling**: If a field is missing, or if a type conversion fails, `from_rhai_map` will return an `Err(String)` describing the issue. + +## Combining with `rhai::CustomType` + +For your structs to be fully usable as custom types within Rhai (e.g., to be registered with `engine.build_type::()`), they should also typically derive `rhai::CustomType`, `Clone`, and `Debug`: + +```rust +use rhai_macros_derive::{ToRhaiMap, FromRhaiMap}; +use rhai::CustomType; + +#[derive(CustomType, ToRhaiMap, FromRhaiMap, Clone, Debug, PartialEq)] +struct MyStruct { + // ... fields +} +``` + +This setup allows seamless conversion and manipulation of your Rust structs within Rhai scripts. diff --git a/rhai_macros_derive/src/lib.rs b/rhai_macros_derive/src/lib.rs new file mode 100644 index 0000000..fa91f98 --- /dev/null +++ b/rhai_macros_derive/src/lib.rs @@ -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> + 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> + 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, 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::, String>>().map(Some) + }, + None => Err(format!( + "Field '{}' (Option) 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 + 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 + 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 + 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 + 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 + 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::, 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 { + #(#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> + 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> + 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 + 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 + 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, 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 + 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 + 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) +} diff --git a/rhai_wrapper/Cargo.lock b/rhai_wrapper/Cargo.lock new file mode 100644 index 0000000..32a73c8 --- /dev/null +++ b/rhai_wrapper/Cargo.lock @@ -0,0 +1,297 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.2", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[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 = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rhai" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" +dependencies = [ + "ahash", + "bitflags", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rhai_macros_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rhai_wrapper" +version = "0.1.0" +dependencies = [ + "rhai", + "rhai_macros_derive", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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 = "thin-vec" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rhai_wrapper/Cargo.toml b/rhai_wrapper/Cargo.toml new file mode 100644 index 0000000..c12553e --- /dev/null +++ b/rhai_wrapper/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rhai_wrapper" +version = "0.1.0" +edition = "2021" + +description = "A wrapper to make generic Rust functions Rhai-compatible." + +[dependencies] +rhai = "1.21.0" +rhai_macros_derive = { path = "../rhai_macros_derive" } + +[dev-dependencies] diff --git a/rhai_wrapper/README.md b/rhai_wrapper/README.md new file mode 100644 index 0000000..57abbfd --- /dev/null +++ b/rhai_wrapper/README.md @@ -0,0 +1,162 @@ +# Rhai Wrapper Crate + +This crate provides utilities to simplify the process of wrapping Rust functions and types for use with the Rhai scripting engine. Its primary component is the `wrap_for_rhai!` macro, which generates the necessary boilerplate to make Rust functions callable from Rhai scripts, including type conversions for arguments and return values. + +This crate works in conjunction with the `rhai_macros_derive` crate, which provides `ToRhaiMap` and `FromRhaiMap` derive macros for custom Rust structs. + +## Features + +- **`wrap_for_rhai!` macro**: Simplifies registering Rust functions with Rhai. +- Support for various function signatures: + - Primitive types (`INT`, `FLOAT`, `bool`, `String`). + - `Option` for optional return values. + - `Vec` (e.g., `Vec`, `Vec`). + - `Vec` where `CustomStruct` implements `FromRhaiMap` (for arguments) and `ToRhaiMap` (for elements in return values). + - Custom structs as direct arguments (if they implement `FromRhaiMap`). + - Custom structs as direct return values (if they implement `ToRhaiMap`). +- Automatic conversion between Rhai's `Dynamic` type and Rust types. + +## Dependencies + +Ensure you have `rhai` and `rhai_macros_derive` (if using custom structs) in your `Cargo.toml`: + +```toml +[dependencies] +rhai = "" # e.g., "1.16.0" +rhai_macros_derive = { path = "../rhai_macros_derive" } # If in the same workspace + +# This crate (rhai_wrapper) would also be a local path dependency +# rhai_wrapper = { path = "../rhai_wrapper" } +``` + +## `wrap_for_rhai!` Macro + +The `wrap_for_rhai!` macro is the core of this crate. It takes your Rust function and its type signature (in a specific format) and generates a closure that Rhai can register. + +### Basic Usage + +```rust +use rhai_wrapper::wrap_for_rhai; +use rhai::{Engine, INT, FLOAT}; + +// Functions to be wrapped +fn add(a: INT, b: INT) -> INT { a + b } +fn greet(name: String) -> String { format!("Hello, {}!", name) } +fn get_pi() -> FLOAT { 3.14159 } + +fn main() { + let mut engine = Engine::new(); + + // Registering functions using the macro + engine.register_fn("add_rhai", wrap_for_rhai!(add, INT, INT -> INT)); + engine.register_fn("greet_rhai", wrap_for_rhai!(greet, String -> String)); + engine.register_fn("get_pi_rhai", wrap_for_rhai!(get_pi, -> FLOAT)); + + let result: INT = engine.eval("add_rhai(10, 32)").unwrap(); + assert_eq!(result, 42); + + let message: String = engine.eval(r#"greet_rhai("Rhai")"#).unwrap(); + assert_eq!(message, "Hello, Rhai!"); +} +``` + +### Supported Signatures & Examples + +The macro uses a pattern-matching style to handle different function signatures. + +1. **Primitives**: `(INT, INT -> INT)`, `(String -> String)`, `(FLOAT, bool -> String)` + ```rust + fn my_func(a: INT, b: String) -> bool { /* ... */ false } + // engine.register_fn("my_func_rhai", wrap_for_rhai!(my_func, INT, String -> bool)); + ``` + +2. **No Arguments**: `(-> INT)` + ```rust + fn get_answer() -> INT { 42 } + // engine.register_fn("get_answer_rhai", wrap_for_rhai!(get_answer, -> INT)); + ``` + +3. **`Option` Return Type**: `(INT -> Option)` + ```rust + fn maybe_get_name(id: INT) -> Option { + if id == 1 { Some("Alice".to_string()) } else { None } + } + // engine.register_fn("maybe_get_name_rhai", wrap_for_rhai!(maybe_get_name, INT -> Option)); + ``` + *Rhai will receive `()` (unit/nothing) if the Rust function returns `None`.* + +4. **`Vec` Argument**: `(Vec -> INT)`, `(Vec -> String)` + ```rust + fn sum_numbers(numbers: Vec) -> INT { numbers.iter().sum() } + // engine.register_fn("sum_rhai", wrap_for_rhai!(sum_numbers, Vec -> INT)); + // Rhai script: sum_rhai([1, 2, 3]) -> 6 + ``` + +5. **Custom Structs (with `rhai_macros_derive`)** + + Assume you have a struct `Point` that derives `ToRhaiMap` and `FromRhaiMap`: + ```rust + use rhai_macros_derive::{ToRhaiMap, FromRhaiMap}; + use rhai::CustomType; + + #[derive(CustomType, ToRhaiMap, FromRhaiMap, Clone, Debug, PartialEq)] + struct Point { x: INT, y: INT } + ``` + + - **Custom Struct Argument**: `(Point -> String)` + ```rust + fn print_point(p: Point) -> String { format!("Point(x={}, y={})", p.x, p.y) } + // engine.build_type::(); + // engine.register_fn("print_point_rhai", wrap_for_rhai!(print_point, Point -> String)); + // Rhai script: print_point_rhai(#{x:1, y:2}) + ``` + + - **Custom Struct Return**: `(INT, INT -> Point)` + ```rust + fn make_point(x: INT, y: INT) -> Point { Point { x, y } } + // engine.build_type::(); + // engine.register_fn("make_point_rhai", wrap_for_rhai!(make_point, INT, INT -> Point)); + // Rhai script: let p = make_point_rhai(3,4); p.x == 3 + ``` + + - **`Vec` Argument**: `(Vec -> INT)` + ```rust + fn sum_point_coords(points: Vec) -> INT { points.iter().map(|p| p.x + p.y).sum() } + // engine.build_type::(); + // engine.register_fn("sum_points_rhai", wrap_for_rhai!(sum_point_coords, Vec -> INT)); + // Rhai script: sum_points_rhai([#{x:1,y:2}, #{x:3,y:4}]) + ``` + + - **`Vec` Return**: `(INT -> Vec)` + ```rust + fn generate_points(count: INT) -> Vec { + (0..count).map(|i| Point { x: i, y: i*2 }).collect() + } + // engine.build_type::(); + // engine.register_fn("gen_points_rhai", wrap_for_rhai!(generate_points, INT -> Vec)); + // Rhai script: let arr = gen_points_rhai(2); arr[0].x == 0 + ``` + + - **`Vec` Argument and `Vec` Return**: `(Vec -> Vec)` + ```rust + fn get_x_coords(points: Vec) -> Vec { points.iter().map(|p| p.x).collect() } + // engine.build_type::(); + // engine.register_fn("get_xs_rhai", wrap_for_rhai!(get_x_coords, Vec -> Vec)); + ``` + +### How Custom Structs are Handled + +- When a custom struct is an **argument** (`MyStructType`): The macro expects the Rhai script to pass an object map. This map is then passed to `MyStructType::from_rhai_map(map)` (provided by `#[derive(FromRhaiMap)]`) to convert it into a Rust struct instance. +- When a custom struct is a **return value**: The Rust function returns an instance of `MyStructType`. The macro calls `instance.to_rhai_map()` (provided by `#[derive(ToRhaiMap)]`) to convert it into a `rhai::Map`, which Rhai receives as an object map. +- For `Vec`: Similar logic applies element-wise. Incoming `rhai::Array` elements are converted using `from_rhai_map`; outgoing `Vec` elements are converted using `to_rhai_map` before being collected into a `rhai::Array`. + +## Adding New Macro Arms + +The `wrap_for_rhai!` macro is defined with several arms, each matching a specific function signature pattern. If you need to support a new, common signature: +1. Open `rhai_wrapper/src/lib.rs`. +2. Add a new macro arm following the existing patterns. +3. Pay attention to argument conversion (from `rhai::Dynamic` or `rhai::Map`) and return value conversion (to `rhai::Dynamic` or `rhai::Map`). +4. For custom types, rely on `YourType::from_rhai_map(...)` and `your_instance.to_rhai_map()`. +5. Consider the order of macro arms if a more specific arm needs to match before a more general one. + +This crate aims to reduce boilerplate and make Rhai integration smoother for common Rust patterns. diff --git a/rhai_wrapper/src/lib.rs b/rhai_wrapper/src/lib.rs new file mode 100644 index 0000000..0520514 --- /dev/null +++ b/rhai_wrapper/src/lib.rs @@ -0,0 +1,210 @@ +//! # Rhai Wrapper +//! Provides a macro to simplify wrapping Rust functions for the Rhai scripting engine. +//! +//! This crate provides a macro and utilities to wrap generic Rust functions so they can be registered with the Rhai scripting engine. +//! It currently supports functions with primitive arguments (i64, f64, String) and return values. + +use rhai::Map; // Kept Map for traits. Dynamic, INT, FLOAT removed as they are qualified in macro. + +/// Trait for converting a Rust struct into a Rhai `Map`. +/// This trait is intended to be derived using `#[derive(ToRhaiMap)]` from the `rhai_macros_derive` crate. +pub trait ToRhaiMap { + /// Converts `&self` into a `rhai::Map`. + fn to_rhai_map(&self) -> Map; +} + +/// Trait for converting a Rhai `Map` into a Rust struct. +/// This trait is intended to be derived using `#[derive(FromRhaiMap)]` from the `rhai_macros_derive` crate. +pub trait FromRhaiMap: Sized { + /// Attempts to convert a `rhai::Map` into an instance of `Self`. + /// Returns a `Result` which is `Ok(Self)` on success, or an `Err(String)` describing the error. + fn from_rhai_map(map: Map) -> Result; +} + +/// Macro to wrap a Rust function for Rhai +/// +/// Usage: +/// ``` +/// use rhai_wrapper::wrap_for_rhai; +/// use rhai::{Engine, INT}; +/// fn add(a: INT, b: INT) -> INT { a + b } +/// let mut engine = Engine::new(); +/// engine.register_fn("add", wrap_for_rhai!(add)); +/// let result = engine.eval::("add(2, 3)").unwrap(); +/// assert_eq!(result, 5); +/// ``` +#[macro_export] +macro_rules! wrap_for_rhai { + // New arm for functions like: fn name(Vec) -> rhai::INT + // Usage: wrap_for_rhai!(my_function_name, Vec -> INT) + ($func:ident, Vec -> INT) => { + |arr: rhai::Array| -> rhai::INT { + let vec_arg: std::vec::Vec = arr.into_iter() + .map(|x: rhai::Dynamic| x.as_int().expect("Rhai array element is not an INT or could not be converted")) + .collect(); + $func(vec_arg) + } + }; + + // New arm for functions like: fn name(Vec) -> String + // Usage: wrap_for_rhai!(my_function_name, Vec -> String) + ($func:ident, Vec -> String) => { + |arr: rhai::Array| -> String { + let vec_arg: std::vec::Vec = arr.into_iter() + .map(|x: rhai::Dynamic| x.into_string().expect("Rhai array element is not a String or could not be converted")) + .collect(); + $func(vec_arg) + } + }; + + // New arm for functions like: fn name(Vec) -> FLOAT + // Usage: wrap_for_rhai!(my_function_name, Vec -> FLOAT) + ($func:ident, Vec -> FLOAT) => { + |arr: rhai::Array| -> rhai::FLOAT { + let vec_arg: std::vec::Vec = arr.into_iter() + .map(|x: rhai::Dynamic| x.as_float().expect("Rhai array element is not a FLOAT or could not be converted")) + .collect(); + $func(vec_arg) + } + }; + + // Specific arms for Vec -> Vec + // These must come BEFORE the Vec -> Vec arm. + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + + // Arm for Vec -> Vec + // This must come AFTER specific primitive Vec outputs to allow them to match first. + // Requires $InputStructType::from_rhai_map and $OutputStructType::to_rhai_map + ($func:ident, Vec<$InputStructType:ident> -> Vec<$OutputStructType:ident>) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::() + .expect("Rhai array element is not an object map for $InputStructType conversion"); + $InputStructType::from_rhai_map(map) + .expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + + let result_vec: Vec<$OutputStructType> = $func(vec_arg); + + result_vec.into_iter() + .map(|item: $OutputStructType| item.to_rhai_map().into()) // Convert struct to rhai::Map, then to rhai::Dynamic + .collect::() + } + }; + + // Func(CustomStruct) -> String + ($func:ident, $InputStructType:ident -> String) => { + |arg_dyn: rhai::Dynamic| -> String { + let arg_map = arg_dyn.try_cast::() + .expect(&format!("Argument not an object map for {}", stringify!($InputStructType))); + let arg_struct = $InputStructType::from_rhai_map(arg_map) + .expect(&format!("Failed to convert map to {}", stringify!($InputStructType))); + $func(arg_struct) + } + }; + + // Func(String, INT, INT) -> CustomReturnType + ($func:ident, String, INT, INT -> $ReturnType:ident) => { + |id_str: rhai::ImmutableString, cx_int: rhai::INT, cy_int: rhai::INT| -> rhai::Dynamic { + let id = id_str.to_string(); + // cx_int and cy_int are already rhai::INT, matching typical Rust INT type alias for i64 + + let result_struct: $ReturnType = $func(id, cx_int, cy_int); + let result_map = result_struct.to_rhai_map(); + rhai::Dynamic::from(result_map) + } + }; + + // Func(CustomType1, CustomType2, String) -> CustomTypeReturn + ($func:ident, $Arg1Type:ident, $Arg2Type:ident, $Arg3Type:ident -> $ReturnType:ident) => { + |arg1_dyn: rhai::Dynamic, arg2_dyn: rhai::Dynamic, arg3_str: rhai::ImmutableString| -> rhai::Dynamic { + let arg1_map = arg1_dyn.try_cast::() + .expect(&format!("Argument 1 not an object map for {}", stringify!($Arg1Type))); + let arg1 = $Arg1Type::from_rhai_map(arg1_map) + .expect(&format!("Failed to convert map to {}", stringify!($Arg1Type))); + + let arg2_map = arg2_dyn.try_cast::() + .expect(&format!("Argument 2 not an object map for {}", stringify!($Arg2Type))); + let arg2 = $Arg2Type::from_rhai_map(arg2_map) + .expect(&format!("Failed to convert map to {}", stringify!($Arg2Type))); + + let arg3 = arg3_str.to_string(); + + let result_struct: $ReturnType = $func(arg1, arg2, arg3); + let result_map = result_struct.to_rhai_map(); + rhai::Dynamic::from(result_map) + } + }; + + // Generic arm for functions like: fn name(Vec) -> ReturnType + // Where MyCustomStruct derives rhai::CustomType, Clone, 'static + // and implements a method like `from_rhai_map(map: rhai::Map) -> Result>` + ($func:ident, Vec<$StructType:ident> -> $ReturnType:ty) => { + |arr: rhai::Array| -> $ReturnType { + let vec_arg: std::vec::Vec<$StructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::() + .expect("Rhai array element is not an object map"); + // Assuming $StructType implements a method to convert from rhai::Map + $StructType::from_rhai_map(map) + .expect(&format!("Failed to convert rhai::Map to {}", stringify!($StructType))) + }) + .collect(); + $func(vec_arg) + } + }; + + // Passthrough for functions that are already Rhai-compatible or take no arguments + ($func:ident) => { + $func + }; +} diff --git a/rhai_wrapper/src/lib.rs.bak b/rhai_wrapper/src/lib.rs.bak new file mode 100644 index 0000000..3cbab7c --- /dev/null +++ b/rhai_wrapper/src/lib.rs.bak @@ -0,0 +1,193 @@ +//! # Rhai Wrapper +//! Provides a macro to simplify wrapping Rust functions for the Rhai scripting engine. +//! +//! This crate provides a macro and utilities to wrap generic Rust functions so they can be registered with the Rhai scripting engine. +//! It currently supports functions with primitive arguments (i64, f64, String) and return values. + +/// Macro to wrap a Rust function for Rhai +/// +/// Usage: +/// ``` +/// use rhai_wrapper::wrap_for_rhai; +/// use rhai::{Engine, INT}; +/// fn add(a: INT, b: INT) -> INT { a + b } +/// let mut engine = Engine::new(); +/// engine.register_fn("add", wrap_for_rhai!(add)); +/// let result = engine.eval::("add(2, 3)").unwrap(); +/// assert_eq!(result, 5); +/// ``` +#[macro_export] +macro_rules! wrap_for_rhai { + // New arm for functions like: fn name(Vec) -> rhai::INT + // Usage: wrap_for_rhai!(my_function_name, Vec -> INT) + ($func:ident, Vec -> INT) => { + |arr: rhai::Array| -> rhai::INT { + let vec_arg: std::vec::Vec = arr.into_iter() + .map(|x: rhai::Dynamic| x.as_int().expect("Rhai array element is not an INT or could not be converted")) + .collect(); + $func(vec_arg) + } + }; + + // New arm for functions like: fn name(Vec) -> String + // Usage: wrap_for_rhai!(my_function_name, Vec -> String) + ($func:ident, Vec -> String) => { + |arr: rhai::Array| -> String { + let vec_arg: std::vec::Vec = arr.into_iter() + .map(|x: rhai::Dynamic| x.into_string().expect("Rhai array element is not a String or could not be converted")) + .collect(); + $func(vec_arg) + } + }; + + // New arm for functions like: fn name(Vec) -> FLOAT + // Usage: wrap_for_rhai!(my_function_name, Vec -> FLOAT) + ($func:ident, Vec -> FLOAT) => { + |arr: rhai::Array| -> rhai::FLOAT { + let vec_arg: std::vec::Vec = arr.into_iter() + .map(|x: rhai::Dynamic| x.as_float().expect("Rhai array element is not a FLOAT or could not be converted")) + .collect(); + $func(vec_arg) + } + }; + + // Specific arms for Vec -> Vec + // These must come BEFORE the Vec -> Vec arm. + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + + // Arm for Vec -> Vec + // This must come AFTER specific primitive Vec outputs to allow them to match first. + // Requires $InputStructType::from_rhai_map and $OutputStructType::to_rhai_map + ($func:ident, Vec<$InputStructType:ident> -> Vec<$OutputStructType:ident>) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::() + .expect("Rhai array element is not an object map for $InputStructType conversion"); + $InputStructType::from_rhai_map(map) + .expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + + let result_vec: Vec<$OutputStructType> = $func(vec_arg); + + result_vec.into_iter() + .map(|item: $OutputStructType| item.to_rhai_map().into()) // Convert struct to rhai::Map, then to rhai::Dynamic + .collect::() + } + }; + + // Func(CustomStruct) -> String + ($func:ident, $InputStructType:ident -> String) => { + |arg_dyn: rhai::Dynamic| -> String { + let arg_map = arg_dyn.try_cast::() + .expect(&format!("Argument not an object map for {}", stringify!($InputStructType))); + let arg_struct = $InputStructType::from_rhai_map(arg_map) + .expect(&format!("Failed to convert map to {}", stringify!($InputStructType))); + $func(arg_struct) + } + }; + + // Func(String, INT, INT) -> CustomReturnType + ($func:ident, String, INT, INT -> $ReturnType:ident) => { + |id_str: rhai::ImmutableString, cx_int: rhai::INT, cy_int: rhai::INT| -> rhai::Dynamic { + let id = id_str.to_string(); + // cx_int and cy_int are already rhai::INT, matching typical Rust INT type alias for i64 + + let result_struct: $ReturnType = $func(id, cx_int, cy_int); + let result_map = result_struct.to_rhai_map(); + rhai::Dynamic::from(result_map) + } + }; + + // Func(CustomType1, CustomType2, String) -> CustomTypeReturn + ($func:ident, $Arg1Type:ident, $Arg2Type:ident, $Arg3Type:ident -> $ReturnType:ident) => { + |arg1_dyn: rhai::Dynamic, arg2_dyn: rhai::Dynamic, arg3_str: rhai::ImmutableString| -> rhai::Dynamic { + let arg1_map = arg1_dyn.try_cast::() + .expect(&format!("Argument 1 not an object map for {}", stringify!($Arg1Type))); + let arg1 = $Arg1Type::from_rhai_map(arg1_map) + .expect(&format!("Failed to convert map to {}", stringify!($Arg1Type))); + + let arg2_map = arg2_dyn.try_cast::() + .expect(&format!("Argument 2 not an object map for {}", stringify!($Arg2Type))); + let arg2 = $Arg2Type::from_rhai_map(arg2_map) + .expect(&format!("Failed to convert map to {}", stringify!($Arg2Type))); + + let arg3 = arg3_str.to_string(); + + let result_struct: $ReturnType = $func(arg1, arg2, arg3); + let result_map = result_struct.to_rhai_map(); + rhai::Dynamic::from(result_map) + } + }; + + // Generic arm for functions like: fn name(Vec) -> ReturnType + // Where MyCustomStruct derives rhai::CustomType, Clone, 'static + // and implements a method like `from_rhai_map(map: rhai::Map) -> Result>` + ($func:ident, Vec<$StructType:ident> -> $ReturnType:ty) => { + |arr: rhai::Array| -> $ReturnType { + let vec_arg: std::vec::Vec<$StructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::() + .expect("Rhai array element is not an object map"); + // Assuming $StructType implements a method to convert from rhai::Map + $StructType::from_rhai_map(map) + .expect(&format!("Failed to convert rhai::Map to {}", stringify!($StructType))) + }) + .collect(); + $func(vec_arg) + } + }; + + // Passthrough for functions that are already Rhai-compatible or take no arguments + ($func:ident) => { + $func + }; +} diff --git a/rhai_wrapper/src/lib.rs.bak2 b/rhai_wrapper/src/lib.rs.bak2 new file mode 100644 index 0000000..2cf30db --- /dev/null +++ b/rhai_wrapper/src/lib.rs.bak2 @@ -0,0 +1,355 @@ +//! # Rhai Wrapper +//! Provides a macro to simplify wrapping Rust functions for the Rhai scripting engine. +//! +//! This crate provides a macro and utilities to wrap generic Rust functions so they can be registered with the Rhai scripting engine. +//! It currently supports functions with primitive arguments (i64, f64, String) and return values. + +/// Macro to wrap a Rust function for Rhai +/// +/// Usage: +/// ``` +/// use rhai_wrapper::wrap_for_rhai; +/// use rhai::{Engine, INT}; +/// fn add(a: INT, b: INT) -> INT { a + b } +/// let mut engine = Engine::new(); +/// engine.register_fn("add", wrap_for_rhai!(add)); +/// let result = engine.eval::("add(2, 3)").unwrap(); +/// assert_eq!(result, 5); +/// ``` +#[macro_export] +macro_rules! wrap_for_rhai { + // New arm for functions like: fn name(Vec) -> rhai::INT + // Usage: wrap_for_rhai!(my_function_name, Vec -> INT) + ($func:ident, Vec -> INT) => { + |arr: rhai::Array| -> rhai::INT { + let vec_arg: std::vec::Vec = arr.into_iter() + .map(|x: rhai::Dynamic| x.as_int().expect("Rhai array element is not an INT or could not be converted")) + .collect(); + $func(vec_arg) + } + }; + + // New arm for functions like: fn name(Vec) -> String + // Usage: wrap_for_rhai!(my_function_name, Vec -> String) + ($func:ident, Vec -> String) => { + |arr: rhai::Array| -> String { + let vec_arg: std::vec::Vec = arr.into_iter() + .map(|x: rhai::Dynamic| x.into_string().expect("Rhai array element is not a String or could not be converted")) + .collect(); + $func(vec_arg) + } + }; + + // New arm for functions like: fn name(Vec) -> FLOAT + // Usage: wrap_for_rhai!(my_function_name, Vec -> FLOAT) + ($func:ident, Vec -> FLOAT) => { + |arr: rhai::Array| -> rhai::FLOAT { + let vec_arg: std::vec::Vec = arr.into_iter() + .map(|x: rhai::Dynamic| x.as_float().expect("Rhai array element is not a FLOAT or could not be converted")) + .collect(); + $func(vec_arg) + } + }; + + // Specific arms for Vec -> Vec + // These must come BEFORE the Vec -> Vec arm. + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + ($func:ident, Vec<$InputStructType:ident> -> Vec) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::().expect("Rhai array element not map for $InputStructType"); + $InputStructType::from_rhai_map(map).expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + let result_vec: Vec = $func(vec_arg); + result_vec.into_iter().map(rhai::Dynamic::from).collect::() + } + }; + + // Arm for Vec -> Vec + // This must come AFTER specific primitive Vec outputs to allow them to match first. + // Requires $InputStructType::from_rhai_map and $OutputStructType::to_rhai_map + ($func:ident, Vec<$InputStructType:ident> -> Vec<$OutputStructType:ident>) => { + |arr: rhai::Array| -> rhai::Array { + let vec_arg: std::vec::Vec<$InputStructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::() + .expect("Rhai array element is not an object map for $InputStructType conversion"); + $InputStructType::from_rhai_map(map) + .expect(&format!("Failed to convert map to {}", stringify!($InputStructType))) + }) + .collect(); + + let result_vec: Vec<$OutputStructType> = $func(vec_arg); + + result_vec.into_iter() + .map(|item: $OutputStructType| item.to_rhai_map().into()) // Convert struct to rhai::Map, then to rhai::Dynamic + .collect::() + } + }; + + // Func(CustomStruct) -> String + ($func:ident, $InputStructType:ident -> String) => { + |arg_dyn: rhai::Dynamic| -> String { + let arg_map = arg_dyn.try_cast::() + .expect(&format!("Argument not an object map for {}", stringify!($InputStructType))); + let arg_struct = $InputStructType::from_rhai_map(arg_map) + .expect(&format!("Failed to convert map to {}", stringify!($InputStructType))); + $func(arg_struct) + } + }; + + // Func(String, INT, INT) -> CustomReturnType + ($func:ident, String, INT, INT -> $ReturnType:ident) => { + |id_str: rhai::ImmutableString, cx_int: rhai::INT, cy_int: rhai::INT| -> rhai::Dynamic { + let id = id_str.to_string(); + // cx_int and cy_int are already rhai::INT, matching typical Rust INT type alias for i64 + + let result_struct: $ReturnType = $func(id, cx_int, cy_int); + let result_map = result_struct.to_rhai_map(); + rhai::Dynamic::from(result_map) + } + }; + + // Func(CustomType1, CustomType2, String) -> CustomTypeReturn + ($func:ident, $Arg1Type:ident, $Arg2Type:ident, $Arg3Type:ident -> $ReturnType:ident) => { + |arg1_dyn: rhai::Dynamic, arg2_dyn: rhai::Dynamic, arg3_str: rhai::ImmutableString| -> rhai::Dynamic { + let arg1_map = arg1_dyn.try_cast::() + .expect(&format!("Argument 1 not an object map for {}", stringify!($Arg1Type))); + let arg1 = $Arg1Type::from_rhai_map(arg1_map) + .expect(&format!("Failed to convert map to {}", stringify!($Arg1Type))); + + let arg2_map = arg2_dyn.try_cast::() + .expect(&format!("Argument 2 not an object map for {}", stringify!($Arg2Type))); + let arg2 = $Arg2Type::from_rhai_map(arg2_map) + .expect(&format!("Failed to convert map to {}", stringify!($Arg2Type))); + + let arg3 = arg3_str.to_string(); + + let result_struct: $ReturnType = $func(arg1, arg2, arg3); + let result_map = result_struct.to_rhai_map(); + rhai::Dynamic::from(result_map) + } + }; + + // Generic arm for functions like: fn name(Vec) -> ReturnType + // Where MyCustomStruct derives rhai::CustomType, Clone, 'static + // and implements a method like `from_rhai_map(map: rhai::Map) -> Result>` + ($func:ident, Vec<$StructType:ident> -> $ReturnType:ty) => { + |arr: rhai::Array| -> $ReturnType { + let vec_arg: std::vec::Vec<$StructType> = arr.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::() + .expect("Rhai array element is not an object map"); + // Assuming $StructType implements a method to convert from rhai::Map + $StructType::from_rhai_map(map) + .expect(&format!("Failed to convert rhai::Map to {}", stringify!($StructType))) + }) + .collect(); + $func(vec_arg) + } + }; + + // Passthrough for functions that are already Rhai-compatible or take no arguments + ($func:ident) => { + $func + }; + + // General arm for arbitrary parameter combinations + // This should be matched only if none of the specific arms above match + // Special case for Vec return type + ($func:ident, $($ArgType:ident),* -> Vec) => { + paste::paste! { + |$([]: $crate::__param_type!($ArgType)),*| -> rhai::Array { + $(let [] = $crate::__convert_in!($ArgType, []);)* + + let result: Vec = $func($([]),*); + result.into_iter().map(rhai::Dynamic::from).collect::() + } + } + }; + + // Special case for Vec return type + ($func:ident, $($ArgType:ident),* -> Vec) => { + paste::paste! { + |$([]: $crate::__param_type!($ArgType)),*| -> rhai::Array { + $(let [] = $crate::__convert_in!($ArgType, []);)* + + let result: Vec = $func($([]),*); + result.into_iter().map(rhai::Dynamic::from).collect::() + } + } + }; + + // Special case for Vec return type + ($func:ident, $($ArgType:ident),* -> Vec) => { + paste::paste! { + |$([]: $crate::__param_type!($ArgType)),*| -> rhai::Array { + $(let [] = $crate::__convert_in!($ArgType, []);)* + + let result: Vec = $func($([]),*); + result.into_iter().map(|s| rhai::Dynamic::from(s)).collect::() + } + } + }; + + // Special case for Vec return type + ($func:ident, $($ArgType:ident),* -> Vec<$CustomReturnType:ident>) => { + paste::paste! { + |$([]: $crate::__param_type!($ArgType)),*| -> rhai::Array { + $(let [] = $crate::__convert_in!($ArgType, []);)* + + let result: Vec<$CustomReturnType> = $func($([]),*); + result.into_iter() + .map(|item| rhai::Dynamic::from(item.to_rhai_map())) + .collect::() + } + } + }; + + // General case for other return types + ($func:ident, $($ArgType:ident),* -> $ReturnType:ty) => { + paste::paste! { + |$([]: $crate::__param_type!($ArgType)),*| -> $crate::__return_type!($ReturnType) { + $(let [] = $crate::__convert_in!($ArgType, []);)* + + let result = $func($([]),*); + $crate::__convert_out!($ReturnType, result) + } + } + }; +} + +// Helper macros for the general parameter handling + +// The helper macros for argument naming have been removed and replaced with direct paste usage + +#[doc(hidden)] +#[macro_export] +macro_rules! __param_type { + // Map Rust types to corresponding Rhai parameter types + (INT) => { rhai::INT }; + (FLOAT) => { rhai::FLOAT }; + (String) => { rhai::ImmutableString }; + (bool) => { bool }; + (Vec<$InnerType:ident>) => { rhai::Array }; + ($CustomType:ident) => { rhai::Dynamic }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __return_type { + // Map return types + (INT) => { rhai::INT }; + (FLOAT) => { rhai::FLOAT }; + (String) => { String }; + (bool) => { bool }; + (Vec<$InnerType:ident>) => { rhai::Array }; // Vec always returns rhai::Array + ($CustomType:ident) => { rhai::Dynamic }; + ($ReturnType:ty) => { $ReturnType }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __convert_in { + // Convert Rhai argument types to Rust types + (INT, $arg:ident) => { $arg }; + (FLOAT, $arg:ident) => { $arg }; + (String, $arg:ident) => { $arg.to_string() }; + (bool, $arg:ident) => { $arg }; + (Vec, $arg:ident) => { + $arg.into_iter() + .map(|x: rhai::Dynamic| x.as_int().expect("Rhai array element is not an INT or could not be converted")) + .collect::>() + }; + (Vec, $arg:ident) => { + $arg.into_iter() + .map(|x: rhai::Dynamic| x.as_float().expect("Rhai array element is not a FLOAT or could not be converted")) + .collect::>() + }; + (Vec, $arg:ident) => { + $arg.into_iter() + .map(|x: rhai::Dynamic| x.into_string().expect("Rhai array element is not a String or could not be converted")) + .collect::>() + }; + (Vec<$CustomType:ident>, $arg:ident) => { + $arg.into_iter() + .map(|dyn_obj: rhai::Dynamic| { + let map = dyn_obj.try_cast::() + .expect(&format!("Rhai array element is not an object map for {}", stringify!($CustomType))); + $CustomType::from_rhai_map(map) + .expect(&format!("Failed to convert map to {}", stringify!($CustomType))) + }) + .collect::>() + }; + ($CustomType:ident, $arg:ident) => { + { + let map = $arg.try_cast::() + .expect(&format!("Argument not an object map for {}", stringify!($CustomType))); + $CustomType::from_rhai_map(map) + .expect(&format!("Failed to convert map to {}", stringify!($CustomType))) + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __convert_out { + // Process return values + (INT, $result:expr) => { $result }; + (FLOAT, $result:expr) => { $result }; + (String, $result:expr) => { $result }; + (bool, $result:expr) => { $result }; + (Vec, $result:expr) => { $result.into_iter().map(rhai::Dynamic::from).collect::() }; + (Vec, $result:expr) => { $result.into_iter().map(rhai::Dynamic::from).collect::() }; + (Vec, $result:expr) => { $result.into_iter().map(|s| rhai::Dynamic::from(s)).collect::() }; + (Vec, $result:expr) => { $result.into_iter().map(rhai::Dynamic::from).collect::() }; + (Vec<$CustomType:ident>, $result:expr) => { + $result.into_iter() + .map(|item| rhai::Dynamic::from(item.to_rhai_map())) + .collect::() + }; + ($CustomType:ident, $result:expr) => { + { + let map = $result.to_rhai_map(); + rhai::Dynamic::from(map) + } + }; + ($ReturnType:ty, $result:expr) => { $result }; +} diff --git a/rhai_wrapper/tests/integration.rs b/rhai_wrapper/tests/integration.rs new file mode 100644 index 0000000..c438466 --- /dev/null +++ b/rhai_wrapper/tests/integration.rs @@ -0,0 +1,572 @@ +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}; + +fn add(a: INT, b: INT) -> INT { a + b } +fn mul(a: INT, b: INT) -> INT { a * b } +fn greet(name: String) -> String { format!("Hello, {name}!") } +fn get_forty_two() -> INT { 42 } +fn shout() -> String { "HEY!".to_string() } +fn add_float(a: FLOAT, b: FLOAT) -> FLOAT { a + b } +fn is_even(n: INT) -> bool { n % 2 == 0 } +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 +fn sum_arr(arr: Array) -> INT { arr.into_iter().map(|x| x.as_int().unwrap_or(0)).sum() } + +// New sum_vec, takes Vec +fn sum_vec(v: Vec) -> INT { v.iter().sum() } + +fn describe(name: String, age: INT, height: FLOAT) -> String { + format!("{name} is {age} years old and {height:.1}m tall.") +} +fn swap(a: INT, b: INT) -> (INT, INT) { (b, a) } + +fn join_strings(strings: Vec) -> String { + strings.join(", ") +} + +// New function for Vec +fn sum_float_vec(floats: Vec) -> FLOAT { + floats.iter().sum() +} + +#[derive(Debug, Clone, PartialEq, CustomType, ToRhaiMapDerive, FromRhaiMapDerive)] +pub struct Point { + pub x: INT, + pub y: INT, +} + +// --- Test Custom Struct: Line (contains Point structs) --- +#[derive(Debug, Clone, PartialEq, rhai::CustomType, ToRhaiMapDerive, FromRhaiMapDerive)] +struct Line { + start: Point, + end: Point, + label: String, +} + +// --- Test Custom Struct: Polygon (contains Vec and Point) --- +#[derive(Debug, Clone, PartialEq, rhai::CustomType, ToRhaiMapDerive, FromRhaiMapDerive)] +struct Polygon { + id: String, + vertices: Vec, + center_approx: Point, +} + +// New function for Vec +fn sum_points(points: Vec) -> INT { + points.iter().map(|p| p.x + p.y).sum() +} + +// New function: Vec -> String +fn points_to_string(points: Vec) -> String { + points.iter() + .map(|p| format!("(x:{},y:{})", p.x, p.y)) + .collect::>() + .join("; ") +} + +// New function: Vec -> Vec +fn get_all_x_coordinates(points: Vec) -> Vec { + points.iter().map(|p| p.x).collect() +} + +// New function: Vec -> Vec +fn make_points_origin_symmetric(points: Vec) -> Vec { + points.into_iter().map(|p| Point { x: -p.x, y: -p.y }).collect() +} + +fn get_line_midpoints(lines: Vec) -> Vec { + lines.into_iter().map(|line| { + Point { + x: (line.start.x + line.end.x) / 2, + y: (line.start.y + line.end.y) / 2, + } + }).collect() +} + +fn create_line(p1: Point, p2: Point, label: String) -> Line { + Line { start: p1, end: p2, label } +} + +fn create_sample_polygon(id: String, center_x: INT, center_y: INT) -> Polygon { + Polygon { + id, + vertices: vec![ + Point { x: center_x - 10, y: center_y - 10 }, + Point { x: center_x + 10, y: center_y - 10 }, + Point { x: center_x + 10, y: center_y + 10 }, + Point { x: center_x - 10, y: center_y + 10 }, + ], + center_approx: Point { x: center_x, y: center_y }, + } +} + +fn get_polygon_id_and_num_vertices(poly: Polygon) -> String { + format!("ID: {}, Vertices: {}", poly.id, poly.vertices.len()) +} + +#[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(); + 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(); + 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(); + 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(); + 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(); + 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); +} + +#[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); +} + +#[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); +} + +#[test] +// Renamed from test_sum_vec +fn test_sum_arr() { + let mut engine = Engine::new(); + engine.register_fn("sum_arr", wrap_for_rhai!(sum_arr)); + let result = engine.eval::("sum_arr([1, 2, 3, 4])").unwrap(); + assert_eq!(result, 10); +} + +#[test] +// Test for the new sum_vec(v: Vec) +fn test_sum_vec() { + let mut engine = Engine::new(); + engine.register_fn("sum_vec", wrap_for_rhai!(sum_vec, Vec -> INT)); + let result = engine.eval::("sum_vec([1, 2, 3, 4])").unwrap(); + assert_eq!(result, 10); +} + +#[test] +fn test_join_strings() { + let mut engine = Engine::new(); + engine.register_fn("join_strings_rhai", wrap_for_rhai!(join_strings, Vec -> String)); + let result = engine.eval::(r#"join_strings_rhai(["hello", "world", "rhai"]);"#).unwrap(); + assert_eq!(result, "hello, world, rhai"); +} + +#[test] +fn test_sum_float_vec() { + let mut engine = Engine::new(); + engine.register_fn("sum_float_vec_rhai", wrap_for_rhai!(sum_float_vec, Vec -> FLOAT)); + let result = engine.eval::(r#"sum_float_vec_rhai([1.1, 2.2, 3.3]);"#).unwrap(); + assert!((result - 6.6).abs() < std::f64::EPSILON); +} + +#[test] +fn test_sum_points() { + let mut engine = Engine::new(); + + // Register the Point type with Rhai. The name "Point" will be used in Rhai scripts. + // This is crucial for the generic macro arm to work with Vec. + engine.build_type::(); + + engine.register_fn("sum_points_rhai", wrap_for_rhai!(sum_points, Vec -> INT)); + let script = r#" + sum_points_rhai([ + #{ x: 1, y: 2 }, + #{ x: 3, y: 4 }, + #{ x: 5, y: 6 } + ]) + "#; + let result = engine.eval::(script).unwrap(); + assert_eq!(result, 1 + 2 + 3 + 4 + 5 + 6); // 21 +} + +#[test] +fn test_points_to_string() { + let mut engine = Engine::new(); + engine.build_type::(); // Register Point type + + // Wrap the new function + engine.register_fn("points_to_string_rhai", wrap_for_rhai!(points_to_string, Vec -> String)); + + let script = r#" + points_to_string_rhai([ + #{ x: 1, y: 2 }, + #{ x: 10, y: 20 } + ]) + "#; + let result: String = engine.eval(script).unwrap(); + assert_eq!(result, "(x:1,y:2); (x:10,y:20)"); +} + +#[test] +fn test_get_all_x_coordinates() { + let mut engine = Engine::new(); + engine.build_type::(); // Register Point type + + engine.register_fn("get_all_x_rhai", wrap_for_rhai!(get_all_x_coordinates, Vec -> Vec)); + + let script = r#" + let result = get_all_x_rhai([ + #{ x: 1, y: 2 }, + #{ x: 10, y: 20 }, + #{ x: 50, y: 60 } + ]); + result // Rhai returns the last expression value + "#; + let result_array: rhai::Array = engine.eval(script).unwrap(); + + // Convert rhai::Array to Vec for comparison + let result_vec: Vec = result_array.into_iter().map(|d| d.as_int().unwrap()).collect(); + assert_eq!(result_vec, vec![1, 10, 50]); +} + +#[test] +fn test_make_points_origin_symmetric() { + let mut engine = Engine::new(); + engine.build_type::(); + engine.register_fn("make_symmetric_rhai", wrap_for_rhai!(make_points_origin_symmetric, Vec -> Vec)); + + let script = r#" + let points = [ #{x:1, y:2}, #{x:-3, y:4} ]; + make_symmetric_rhai(points) + "#; + let result_array: rhai::Array = engine.eval(script).unwrap(); + + assert_eq!(result_array.len(), 2); + + // Check first point + let p1_map = result_array[0].clone().try_cast::().expect("Failed to cast result[0] to Map"); + assert_eq!(p1_map.get("x").and_then(|d| d.as_int().ok()).expect("p1.x not found or not INT"), -1); + assert_eq!(p1_map.get("y").and_then(|d| d.as_int().ok()).expect("p1.y not found or not INT"), -2); + + // Check second point + let p2_map = result_array[1].clone().try_cast::().expect("Failed to cast result[1] to Map"); + assert_eq!(p2_map.get("x").and_then(|d| d.as_int().ok()).expect("p2.x not found or not INT"), 3); + assert_eq!(p2_map.get("y").and_then(|d| d.as_int().ok()).expect("p2.y not found or not INT"), -4); +} + +#[test] +fn test_describe() { + let mut engine = Engine::new(); + engine.register_fn("describe", wrap_for_rhai!(describe)); + let result = engine.eval::(r#"describe("Bob", 30, 1.8)"#).unwrap(); + assert_eq!(result, "Bob is 30 years old and 1.8m tall."); +} + +#[test] +fn test_swap() { + let mut engine = Engine::new(); + engine.register_fn("swap", wrap_for_rhai!(swap)); + let result = engine.eval::<(INT, INT)>("swap(1, 2)").unwrap(); + assert_eq!(result, (2, 1)); +} + +#[test] +fn test_get_line_midpoints() { + let mut engine = Engine::new(); + engine.build_type::(); + engine.build_type::(); + engine.register_fn("get_midpoints_rhai", wrap_for_rhai!(get_line_midpoints, Vec -> Vec)); + + let script = r#" + let lines = [ + #{ start: #{x:0, y:0}, end: #{x:2, y:2}, label: "A" }, + #{ start: #{x:10, y:10}, end: #{x:20, y:30}, label: "B" } + ]; + get_midpoints_rhai(lines) + "#; + let result_array: rhai::Array = engine.eval(script).unwrap(); + + assert_eq!(result_array.len(), 2); + + // Check first midpoint (Point) + let p1_map = result_array[0].clone().try_cast::().expect("Result[0] not a Map for Point"); + assert_eq!(p1_map.get("x").and_then(|d| d.as_int().ok()).expect("p1.x not INT"), 1); + assert_eq!(p1_map.get("y").and_then(|d| d.as_int().ok()).expect("p1.y not INT"), 1); + + // Check second midpoint (Point) + let p2_map = result_array[1].clone().try_cast::().expect("Result[1] not a Map for Point"); + assert_eq!(p2_map.get("x").and_then(|d| d.as_int().ok()).expect("p2.x not INT"), 15); + assert_eq!(p2_map.get("y").and_then(|d| d.as_int().ok()).expect("p2.y not INT"), 20); +} + +#[test] +fn test_create_line() { + let mut engine = Engine::new(); + engine.build_type::(); + engine.build_type::(); + engine.register_fn("create_line_rhai", wrap_for_rhai!(create_line, Point, Point, String -> Line)); + + let script = r#" + let p_start = #{ x: 1, y: 2 }; + let p_end = #{ x: 3, y: 4 }; + create_line_rhai(p_start, p_end, "MyLine") + "#; + let result_line_map: rhai::Map = engine.eval(script).unwrap(); + + // Check label + assert_eq!(result_line_map.get("label").unwrap().clone().into_string().unwrap(), "MyLine"); + + // Check start point + let start_map = result_line_map.get("start").unwrap().clone().try_cast::().unwrap(); + assert_eq!(start_map.get("x").unwrap().as_int().unwrap(), 1); + assert_eq!(start_map.get("y").unwrap().as_int().unwrap(), 2); + + // Check end point + let end_map = result_line_map.get("end").unwrap().clone().try_cast::().unwrap(); + assert_eq!(end_map.get("x").unwrap().as_int().unwrap(), 3); + assert_eq!(end_map.get("y").unwrap().as_int().unwrap(), 4); +} + +#[test] +fn test_create_sample_polygon() { + let mut engine = Engine::new(); + engine.build_type::(); + engine.build_type::(); + engine.register_fn("new_polygon_rhai", wrap_for_rhai!(create_sample_polygon, String, INT, INT -> Polygon)); + + let script = r#" + new_polygon_rhai("poly1", 100, 200) + "#; + let result_poly_map: rhai::Map = engine.eval(script).unwrap(); + + assert_eq!(result_poly_map.get("id").unwrap().clone().into_string().unwrap(), "poly1"); + + let center_map = result_poly_map.get("center_approx").unwrap().clone().try_cast::().unwrap(); + assert_eq!(center_map.get("x").unwrap().as_int().unwrap(), 100); + assert_eq!(center_map.get("y").unwrap().as_int().unwrap(), 200); + + let vertices_array = result_poly_map.get("vertices").unwrap().clone().try_cast::().unwrap(); + assert_eq!(vertices_array.len(), 4); + + // Check one vertex (e.g., the first one: center_x - 10, center_y - 10 -> 90, 190) + let v1_map = vertices_array[0].clone().try_cast::().unwrap(); + assert_eq!(v1_map.get("x").unwrap().as_int().unwrap(), 90); + assert_eq!(v1_map.get("y").unwrap().as_int().unwrap(), 190); +} + +#[test] +fn test_get_polygon_id_and_num_vertices() { + let mut engine = Engine::new(); + engine.build_type::(); // Needed if Polygon::from_rhai_map reconstructs Points, even if not directly used by this func's signature in Rhai + engine.build_type::(); + engine.register_fn("poly_info_rhai", wrap_for_rhai!(get_polygon_id_and_num_vertices, Polygon -> String)); + + let script = r#" + let my_poly = #{ + id: "test_poly", + vertices: [ #{x:0,y:0}, #{x:1,y:0}, #{x:0,y:1} ], + center_approx: #{x:0,y:0} + }; + poly_info_rhai(my_poly) + "#; + let result_string: String = engine.eval(script).unwrap(); + + assert_eq!(result_string, "ID: test_poly, Vertices: 3"); +} + +#[test] +fn test_polygon_derives() { + let original_polygon = Polygon { + id: "poly_derive_test".to_string(), + vertices: vec![ + Point { x: 10, y: 20 }, + Point { x: 30, y: 40 }, + ], + center_approx: Point { x: 20, y: 30 }, + }; + + // Test ToRhaiMap (derived) + let rhai_map = original_polygon.to_rhai_map(); + + // Verify id + assert_eq!( + rhai_map.get("id").unwrap().clone().into_string().unwrap(), + "poly_derive_test" + ); + + // Verify center_approx (which uses Point's derived ToRhaiMap) + let center_map_dyn = rhai_map.get("center_approx").unwrap().clone(); + let center_map = center_map_dyn.try_cast::().unwrap(); + assert_eq!(center_map.get("x").unwrap().as_int().unwrap(), 20); + assert_eq!(center_map.get("y").unwrap().as_int().unwrap(), 30); + + // Verify vertices (Vec) + let vertices_array_dyn = rhai_map.get("vertices").unwrap().clone(); + let vertices_array = vertices_array_dyn.try_cast::().unwrap(); + assert_eq!(vertices_array.len(), 2); + + let v1_map_dyn = vertices_array[0].clone(); + let v1_map = v1_map_dyn.try_cast::().unwrap(); + assert_eq!(v1_map.get("x").unwrap().as_int().unwrap(), 10); + assert_eq!(v1_map.get("y").unwrap().as_int().unwrap(), 20); + + let v2_map_dyn = vertices_array[1].clone(); + let v2_map = v2_map_dyn.try_cast::().unwrap(); + assert_eq!(v2_map.get("x").unwrap().as_int().unwrap(), 30); + assert_eq!(v2_map.get("y").unwrap().as_int().unwrap(), 40); + + // Test FromRhaiMap (derived) + let deserialized_polygon = Polygon::from_rhai_map(rhai_map).unwrap(); + + assert_eq!(original_polygon, deserialized_polygon); + assert_eq!(deserialized_polygon.id, "poly_derive_test"); + assert_eq!(deserialized_polygon.vertices.len(), 2); + assert_eq!(deserialized_polygon.vertices[0], Point { x: 10, y: 20 }); + assert_eq!(deserialized_polygon.vertices[1], Point { x: 30, y: 40 }); + assert_eq!(deserialized_polygon.center_approx, Point { x: 20, y: 30 }); +} + +#[cfg(test)] +mod new_export_fn_tests { + use rhai::{Engine, INT, CustomType, TypeBuilder}; + use rhai_wrapper::{FromRhaiMap, ToRhaiMap}; + use crate::Point; + use rhai_macros_derive::{export_fn, FromRhaiMap as FromRhaiMapDerive, ToRhaiMap as ToRhaiMapDerive}; + + // Define the exported functions directly in this module or ensure they are in scope. + // Assuming 'add_for_attr_test' and 'offset_simple_point' are defined here or imported. + + // Correctly define the SampleStruct and its FromRhaiMap implementation if not already present + #[derive(Debug, Clone, PartialEq, FromRhaiMapDerive, ToRhaiMapDerive, CustomType)] + struct SampleStruct { + value: INT, + name: String, + } + + #[export_fn] + fn add_for_attr_test(a: INT, b: INT) -> INT { + a + b + } + + #[export_fn] + fn offset_simple_point(mut pt: Point, dx: INT) -> Point { + pt.x += dx; + pt + } + + #[test] + fn test_export_fn_simple_add() { + let mut engine = Engine::new(); + engine.register_fn("add_for_attr_test", add_for_attr_test_rhai_wrapper); + let result = engine.eval::("add_for_attr_test(5, 10)").unwrap(); + 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); + + // // 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)] + struct AnotherSampleStruct { + id: String, + value: INT, + maybe_value: Option, + nested_vec: Vec, + optional_nested_vec: Option> + } + + #[test] + fn test_from_rhai_map_derive_full_struct() { + let mut engine = Engine::new(); + engine.build_type::(); + engine.build_type::(); + + let script = r#" + let data = #{ + id: "test_id", + value: 123, + maybe_value: 456, + nested_vec: [ #{value: 1, name: "n1"}, #{value: 2, name: "n2"} ], + optional_nested_vec: [ #{value: 3, name: "n3"} ] + }; + data // Return the map directly for Rust to process + "#; + let map = engine.eval::(script).unwrap(); + let result_struct = AnotherSampleStruct::from_rhai_map(map).unwrap(); + + assert_eq!(result_struct.id, "test_id"); + assert_eq!(result_struct.value, 123); + assert_eq!(result_struct.maybe_value, Some(456)); + assert_eq!(result_struct.nested_vec.len(), 2); + assert_eq!(result_struct.nested_vec[0], SampleStruct { value: 1, name: "n1".to_string() }); + assert_eq!(result_struct.nested_vec[1], SampleStruct { value: 2, name: "n2".to_string() }); + 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); + } +}