diff --git a/Cargo.toml b/Cargo.toml index 4b1706ae..ed5ba5b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ num-traits = { version = "0.2.11", default-features = false } [features] #default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"] -default = [] +default = ["serde"] plugins = [] unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync @@ -65,5 +65,11 @@ default-features = false features = ["compile-time-rng"] optional = true +[dependencies.serde] +package = "serde" +version = "1.0.111" +features = ["derive"] +optional = true + [target.'cfg(target_arch = "wasm32")'.dependencies] instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant diff --git a/README.md b/README.md index 681ae280..c170099d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Features * Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html). * Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html). * Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html). +* Serialization/deserialization support via [serde](https://crates.io/crates/serde) * Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations. * Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html). diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index f9491dd3..a7c3946b 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -96,14 +96,15 @@ The Rhai Scripting Language 9. [Maximum Statement Depth](safety/max-stmt-depth.md) 8. [Advanced Topics](advanced.md) 1. [Object-Oriented Programming (OOP)](language/oop.md) - 2. [Script Optimization](engine/optimize/index.md) + 2. [Serialization/Deserialization of `Dynamic` with `serde`](rust/serde.md) + 3. [Script Optimization](engine/optimize/index.md) 1. [Optimization Levels](engine/optimize/optimize-levels.md) 2. [Re-Optimize an AST](engine/optimize/reoptimize.md) 3. [Eager Function Evaluation](engine/optimize/eager.md) 4. [Side-Effect Considerations](engine/optimize/side-effects.md) 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) - 3. [Eval Statement](language/eval.md) + 4. [Eval Statement](language/eval.md) 9. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) 2. [Operators](appendix/operators.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 0768115f..3f19bf63 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -39,6 +39,8 @@ Dynamic * Some support for [object-oriented programming (OOP)][OOP]. +* Serialization/deserialization support via [`serde`]. + Safe ---- diff --git a/doc/src/links.md b/doc/src/links.md index b7147a50..cf367206 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -29,6 +29,7 @@ [package]: {{rootUrl}}/rust/packages/index.md [packages]: {{rootUrl}}/rust/packages/index.md [`Scope`]: {{rootUrl}}/rust/scope.md +[`serde`]: {{rootUrl}}/rust/serde.md [`type_of()`]: {{rootUrl}}/language/type-of.md [`to_string()`]: {{rootUrl}}/language/values-and-types.md diff --git a/doc/src/rust/serde.md b/doc/src/rust/serde.md new file mode 100644 index 00000000..bf3743b2 --- /dev/null +++ b/doc/src/rust/serde.md @@ -0,0 +1,63 @@ +Serialization and Deserialization of `Dynamic` with `serde` +========================================================= + +{{#include ../links.md}} + +Rhai's [`Dynamic`] type supports serialization and deserialization by [`serde`](https://crates.io/crates/serde) +via the [`serde`][features] feature. + +A [`Dynamic`] can be seamlessly converted to and from a type that implements `serde::Serialize` and/or +`serde::Deserialize`. + + +Serialization +------------- + +Serialization by [`serde`](https://crates.io/crates/serde) is not yet implemented. + +It is simple to serialize a Rust type to `JSON` via `serde`, then use [`Engine::parse_json`]({{rootUrl}}/language/json.md) to convert +it into an [object map]. + + +Deserialization +--------------- + +The function `rhai::de::from_dynamic` automatically converts a [`Dynamic`] value into any Rust type +that implements `serde::Deserialize`. + +In particular, [object maps] are converted into Rust `struct`'s (or any type that is marked as +a `serde` map) while [arrays] are converted into Rust `Vec`'s (or any type that is marked +as a `serde` sequence). + +```rust +use rhai::{Engine, Dynamic}; +use rhai::de::from_dynamic; + +#[derive(Debug, serde::Deserialize)] +struct Point { + x: f64, + y: f64 +} + +#[derive(Debug, serde::Deserialize)] +struct MyStruct { + a: i64, + b: Vec, + c: bool, + d: Point +} + +let engine = Engine::new(); + +let result: Dynamic = engine.eval(r#" + #{ + a: 42, + b: [ "hello", "world" ], + c: true, + d: #{ x: 123.456, y: 999.0 } + } + "#)?; + +// Convert the 'Dynamic' object map into 'MyStruct' +let x: MyStruct = from_dynamic(&result)?; +``` diff --git a/doc/src/start/features.md b/doc/src/start/features.md index b4a415a9..77a17e10 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -24,6 +24,7 @@ more control over what a script can (or cannot) do. | `no_function` | Disable script-defined [functions]. | | `no_module` | Disable loading external [modules]. | | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | +| `serde` | Enable serialization/deserialization via [`serde`]. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | | `internals` | Expose internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version. | diff --git a/src/engine.rs b/src/engine.rs index 4f6ec6f3..27d35222 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -35,7 +35,7 @@ use crate::stdlib::{ #[cfg(not(feature = "no_index"))] pub type Array = Vec; -/// Hash map of `Dynamic` values with `String` keys. +/// Hash map of `Dynamic` values with `ImmutableString` keys. /// /// Not available under the `no_object` feature. #[cfg(not(feature = "no_object"))] diff --git a/src/lib.rs b/src/lib.rs index aa205dec..9d60ecea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,7 @@ //! | `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | //! | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | //! | `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, `Engine`, `Scope` and [`AST`] are all `Send + Sync`. | +//! | `serde` | Enable serialization/deserialization via [`serde`]. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | //! | `internals` | Expose internal data structures (beware they may be volatile from version to version). | //! //! See [The Rhai Book](https://schungx.github.io/rhai) for details on the Rhai script engine and language. @@ -86,6 +87,7 @@ pub mod packages; mod parser; mod result; mod scope; +mod serde; mod stdlib; mod token; mod r#unsafe; @@ -127,6 +129,11 @@ pub mod module_resolvers { pub use crate::module::resolvers::*; } +#[cfg(feature = "serde")] +pub mod de { + pub use crate::serde::de::from_dynamic; +} + #[cfg(not(feature = "no_optimize"))] pub use optimize::OptimizationLevel; diff --git a/src/serde/de.rs b/src/serde/de.rs new file mode 100644 index 00000000..63924dd1 --- /dev/null +++ b/src/serde/de.rs @@ -0,0 +1,375 @@ +use super::str::ImmutableStringDeserializer; +use crate::any::{Dynamic, Union}; +use crate::result::EvalAltResult; +use crate::token::Position; +use crate::utils::ImmutableString; + +use serde::de::{DeserializeSeed, Deserializer, Error, MapAccess, SeqAccess, Visitor}; +use serde::Deserialize; + +#[cfg(not(feature = "no_index"))] +use crate::engine::Array; +#[cfg(not(feature = "no_object"))] +use crate::engine::Map; + +use crate::stdlib::{any::type_name, fmt}; + +#[cfg(not(feature = "no_std"))] +#[cfg(not(target_arch = "wasm32"))] +use crate::stdlib::time::Instant; + +#[cfg(not(feature = "no_std"))] +#[cfg(target_arch = "wasm32")] +use instant::Instant; + +pub struct DynamicDeserializer<'a> { + value: &'a Dynamic, +} + +impl<'a> DynamicDeserializer<'a> { + pub fn from_dynamic(value: &'a Dynamic) -> Self { + Self { value } + } + pub fn type_error(&self) -> Result> { + self.type_error_str(type_name::()) + } + pub fn type_error_str(&self, name: &str) -> Result> { + Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + name.into(), + self.value.type_name().into(), + Position::none(), + ))) + } +} + +pub fn from_dynamic<'de, T: Deserialize<'de>>( + value: &'de Dynamic, +) -> Result> { + T::deserialize(&mut DynamicDeserializer::from_dynamic(value)) +} + +impl Error for Box { + fn custom(err: T) -> Self { + Box::new(EvalAltResult::ErrorRuntime( + err.to_string(), + Position::none(), + )) + } +} + +impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { + type Error = Box; + + fn deserialize_any>(self, visitor: V) -> Result> { + match &self.value.0 { + Union::Unit(_) => self.deserialize_unit(visitor), + Union::Bool(_) => self.deserialize_bool(visitor), + Union::Str(_) => self.deserialize_str(visitor), + Union::Char(_) => self.deserialize_char(visitor), + #[cfg(not(feature = "only_i32"))] + Union::Int(_) => self.deserialize_i64(visitor), + #[cfg(feature = "only_i32")] + Union::Int(_) => self.deserialize_i32(visitor), + #[cfg(not(feature = "no_float"))] + Union::Float(_) => self.deserialize_f64(visitor), + #[cfg(not(feature = "no_index"))] + Union::Array(_) => self.deserialize_seq(visitor), + #[cfg(not(feature = "no_object"))] + Union::Map(_) => self.deserialize_map(visitor), + Union::FnPtr(_) => unimplemented!(), + + #[cfg(not(feature = "no_std"))] + Union::Variant(value) if value.is::() => unimplemented!(), + + Union::Variant(value) if value.is::() => self.deserialize_i8(visitor), + Union::Variant(value) if value.is::() => self.deserialize_i16(visitor), + Union::Variant(value) if value.is::() => self.deserialize_i32(visitor), + Union::Variant(value) if value.is::() => self.deserialize_i64(visitor), + Union::Variant(value) if value.is::() => self.deserialize_u8(visitor), + Union::Variant(value) if value.is::() => self.deserialize_u16(visitor), + Union::Variant(value) if value.is::() => self.deserialize_u32(visitor), + Union::Variant(value) if value.is::() => self.deserialize_u64(visitor), + + Union::Variant(_) => self.type_error_str("any"), + } + } + + fn deserialize_bool>(self, visitor: V) -> Result> { + visitor.visit_bool( + self.value + .as_bool() + .or_else(|_| self.type_error::())?, + ) + } + + fn deserialize_i8>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_i8(x)) + } + + fn deserialize_i16>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_i16(x)) + } + + fn deserialize_i32>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_i32(x)) + } + + fn deserialize_i64>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_i64(x)) + } + + fn deserialize_u8>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_u8(x)) + } + + fn deserialize_u16>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_u16(x)) + } + + fn deserialize_u32>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_u32(x)) + } + + fn deserialize_u64>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_u64(x)) + } + + fn deserialize_f32>(self, visitor: V) -> Result> { + #[cfg(not(feature = "no_float"))] + { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_f32(x)) + } + #[cfg(feature = "no_float")] + self.type_error_str("f32") + } + + fn deserialize_f64>(self, visitor: V) -> Result> { + #[cfg(not(feature = "no_float"))] + { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_f64(x)) + } + #[cfg(feature = "no_float")] + self.type_error_str("f64") + } + + fn deserialize_char>(self, visitor: V) -> Result> { + self.value + .downcast_ref::() + .map_or_else(|| self.type_error::(), |&x| visitor.visit_char(x)) + } + + fn deserialize_str>(self, visitor: V) -> Result> { + self.value.downcast_ref::().map_or_else( + || self.type_error::(), + |x| visitor.visit_borrowed_str(x.as_str()), + ) + } + + fn deserialize_string>( + self, + visitor: V, + ) -> Result> { + self.deserialize_str(visitor) + } + + fn deserialize_bytes>(self, _: V) -> Result> { + self.type_error_str("bytes array") + } + + fn deserialize_byte_buf>(self, _: V) -> Result> { + self.type_error_str("bytes array") + } + + fn deserialize_option>(self, _: V) -> Result> { + self.type_error_str("bytes array") + } + + fn deserialize_unit>(self, visitor: V) -> Result> { + self.value + .downcast_ref::<()>() + .map_or_else(|| self.type_error::<(), _>(), |_| visitor.visit_unit()) + } + + fn deserialize_unit_struct>( + self, + _name: &'static str, + visitor: V, + ) -> Result> { + self.deserialize_unit(visitor) + } + + fn deserialize_newtype_struct>( + self, + _name: &'static str, + visitor: V, + ) -> Result> { + visitor.visit_newtype_struct(self) + } + + fn deserialize_seq>(self, visitor: V) -> Result> { + #[cfg(not(feature = "no_index"))] + { + self.value.downcast_ref::().map_or_else( + || self.type_error::(), + |arr| visitor.visit_seq(IterateArray::new(arr.iter())), + ) + } + #[cfg(feature = "no_index")] + self.type_error_str("array") + } + + fn deserialize_tuple>( + self, + _len: usize, + visitor: V, + ) -> Result> { + self.deserialize_seq(visitor) + } + + fn deserialize_tuple_struct>( + self, + _name: &'static str, + _len: usize, + visitor: V, + ) -> Result> { + self.deserialize_seq(visitor) + } + + fn deserialize_map>(self, visitor: V) -> Result> { + #[cfg(not(feature = "no_object"))] + { + self.value.downcast_ref::().map_or_else( + || self.type_error::(), + |map| visitor.visit_map(IterateMap::new(map.keys(), map.values())), + ) + } + #[cfg(feature = "no_object")] + self.type_error_str("map") + } + + fn deserialize_struct>( + self, + _name: &'static str, + _fields: &'static [&'static str], + visitor: V, + ) -> Result> { + self.deserialize_map(visitor) + } + + fn deserialize_enum>( + self, + _name: &'static str, + _variants: &'static [&'static str], + _: V, + ) -> Result> { + self.type_error_str("num") + } + + fn deserialize_identifier>( + self, + visitor: V, + ) -> Result> { + self.deserialize_str(visitor) + } + + fn deserialize_ignored_any>( + self, + visitor: V, + ) -> Result> { + self.deserialize_any(visitor) + } +} + +struct IterateArray<'a, ITER: Iterator> { + iter: ITER, +} + +impl<'a, ITER: Iterator> IterateArray<'a, ITER> { + pub fn new(iter: ITER) -> Self { + Self { iter } + } +} + +impl<'a: 'de, 'de, ITER: Iterator> SeqAccess<'de> for IterateArray<'a, ITER> { + type Error = Box; + + fn next_element_seed>( + &mut self, + seed: T, + ) -> Result, Box> { + match self.iter.next() { + None => Ok(None), + Some(item) => seed + .deserialize(&mut DynamicDeserializer::from_dynamic(item)) + .map(Some), + } + } +} + +struct IterateMap< + 'a, + KEYS: Iterator, + VALUES: Iterator, +> { + keys: KEYS, + values: VALUES, +} + +impl<'a, KEYS: Iterator, VALUES: Iterator> + IterateMap<'a, KEYS, VALUES> +{ + pub fn new(keys: KEYS, values: VALUES) -> Self { + Self { keys, values } + } +} + +impl< + 'a: 'de, + 'de, + KEYS: Iterator, + VALUES: Iterator, + > MapAccess<'de> for IterateMap<'a, KEYS, VALUES> +{ + type Error = Box; + + fn next_key_seed>( + &mut self, + seed: K, + ) -> Result, Box> { + match self.keys.next() { + None => Ok(None), + Some(item) => seed + .deserialize(&mut ImmutableStringDeserializer::from_str(item)) + .map(Some), + } + } + + fn next_value_seed>( + &mut self, + seed: V, + ) -> Result> { + seed.deserialize(&mut DynamicDeserializer::from_dynamic( + self.values.next().unwrap(), + )) + } +} diff --git a/src/serde/mod.rs b/src/serde/mod.rs new file mode 100644 index 00000000..a4cf45de --- /dev/null +++ b/src/serde/mod.rs @@ -0,0 +1,2 @@ +pub mod de; +mod str; diff --git a/src/serde/str.rs b/src/serde/str.rs new file mode 100644 index 00000000..131cc0fe --- /dev/null +++ b/src/serde/str.rs @@ -0,0 +1,152 @@ +use crate::result::EvalAltResult; +use crate::token::Position; +use crate::utils::ImmutableString; + +use serde::de::{Deserializer, Visitor}; + +use crate::stdlib::any::type_name; + +pub struct ImmutableStringDeserializer<'a> { + value: &'a ImmutableString, +} + +impl<'a> ImmutableStringDeserializer<'a> { + pub fn from_str(value: &'a ImmutableString) -> Self { + Self { value } + } + pub fn type_error(&self) -> Result> { + self.type_error_str(type_name::()) + } + pub fn type_error_str(&self, name: &str) -> Result> { + Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + name.into(), + "string".into(), + Position::none(), + ))) + } +} + +impl<'de> Deserializer<'de> for &mut ImmutableStringDeserializer<'de> { + type Error = Box; + + fn deserialize_any>(self, v: V) -> Result> { + self.deserialize_str(v) + } + fn deserialize_bool>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_i8>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_i16>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_i32>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_i64>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_u8>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_u16>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_u32>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_u64>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_f32>(self, _: V) -> Result> { + self.type_error_str("f32") + } + fn deserialize_f64>(self, _: V) -> Result> { + self.type_error_str("f64") + } + fn deserialize_char>(self, _: V) -> Result> { + self.type_error::() + } + fn deserialize_str>(self, v: V) -> Result> { + v.visit_borrowed_str(self.value.as_str()) + } + fn deserialize_string>( + self, + visitor: V, + ) -> Result> { + self.deserialize_str(visitor) + } + fn deserialize_bytes>(self, _: V) -> Result> { + self.type_error_str("bytes array") + } + fn deserialize_byte_buf>(self, _: V) -> Result> { + self.type_error_str("bytes array") + } + fn deserialize_option>(self, _: V) -> Result> { + self.type_error_str("option") + } + fn deserialize_unit>(self, _: V) -> Result> { + self.type_error::<(), _>() + } + fn deserialize_unit_struct>( + self, + _name: &'static str, + v: V, + ) -> Result> { + self.deserialize_unit(v) + } + fn deserialize_newtype_struct>( + self, + _name: &'static str, + v: V, + ) -> Result> { + v.visit_newtype_struct(self) + } + fn deserialize_seq>(self, _: V) -> Result> { + self.type_error_str("array") + } + fn deserialize_tuple>( + self, + _len: usize, + v: V, + ) -> Result> { + self.deserialize_seq(v) + } + fn deserialize_tuple_struct>( + self, + _name: &'static str, + _len: usize, + v: V, + ) -> Result> { + self.deserialize_seq(v) + } + fn deserialize_map>(self, _: V) -> Result> { + self.type_error_str("map") + } + fn deserialize_struct>( + self, + _name: &'static str, + _fields: &'static [&'static str], + v: V, + ) -> Result> { + self.deserialize_map(v) + } + fn deserialize_enum>( + self, + _name: &'static str, + _variants: &'static [&'static str], + _: V, + ) -> Result> { + self.type_error_str("enum") + } + fn deserialize_identifier>(self, v: V) -> Result> { + self.deserialize_str(v) + } + fn deserialize_ignored_any>( + self, + v: V, + ) -> Result> { + self.deserialize_any(v) + } +} diff --git a/tests/serde.rs b/tests/serde.rs new file mode 100644 index 00000000..daf712f0 --- /dev/null +++ b/tests/serde.rs @@ -0,0 +1,108 @@ +#![cfg(feature = "serde")] + +use rhai::{de::from_dynamic, Dynamic, Engine, EvalAltResult, INT}; +use serde::Deserialize; + +#[cfg(not(feature = "no_index"))] +use rhai::Array; +#[cfg(not(feature = "no_object"))] +use rhai::Map; + +#[test] +fn test_serde_de_primary_types() { + assert_eq!(42_u16, from_dynamic(&Dynamic::from(42_u16)).unwrap()); + assert_eq!(42 as INT, from_dynamic(&(42 as INT).into()).unwrap()); + assert_eq!(true, from_dynamic(&true.into()).unwrap()); + assert_eq!((), from_dynamic(&().into()).unwrap()); + + #[cfg(not(feature = "no_float"))] + { + assert_eq!(123.456_f64, from_dynamic(&123.456_f64.into()).unwrap()); + assert_eq!( + 123.456_f32, + from_dynamic(&Dynamic::from(123.456_f32)).unwrap() + ); + } + + assert_eq!( + "hello", + from_dynamic::(&"hello".to_string().into()).unwrap() + ); +} + +#[test] +#[cfg(not(feature = "no_index"))] +fn test_serde_de_array() { + let arr: Vec = vec![123, 456, 42, 999]; + assert_eq!(arr, from_dynamic::>(&arr.clone().into()).unwrap()); +} + +#[test] +fn test_serde_de_struct() { + #[derive(Debug, Deserialize, PartialEq)] + struct Hello { + a: INT, + b: bool, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct Test { + int: u32, + seq: Vec, + obj: Hello, + } + + let mut map = Map::new(); + map.insert("int".into(), Dynamic::from(42_u32)); + + let mut map2 = Map::new(); + map2.insert("a".into(), (123 as INT).into()); + map2.insert("b".into(), true.into()); + + map.insert("obj".into(), map2.into()); + + let arr: Array = vec!["hello".into(), "kitty".into(), "world".into()]; + map.insert("seq".into(), arr.into()); + + let expected = Test { + int: 42, + seq: vec!["hello".into(), "kitty".into(), "world".into()], + obj: Hello { a: 123, b: true }, + }; + assert_eq!(expected, from_dynamic(&map.into()).unwrap()); +} + +#[test] +fn test_serde_de_script() -> Result<(), Box> { + #[derive(Debug, Deserialize)] + struct Point { + x: f64, + y: f64, + } + + #[derive(Debug, Deserialize)] + struct MyStruct { + a: i64, + b: Vec, + c: bool, + d: Point, + } + + let engine = Engine::new(); + + let result: Dynamic = engine.eval( + r#" + #{ + a: 42, + b: [ "hello", "world" ], + c: true, + d: #{ x: 123.456, y: 999.0 } + } + "#, + )?; + + // Convert the 'Dynamic' object map into 'MyStruct' + let x: MyStruct = from_dynamic(&result)?; + + Ok(()) +}