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