diff --git a/Cargo.toml b/Cargo.toml index cb97505b650..158df382b2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,7 @@ exclude = ["crates/msrv/resolver", "crates/msrv/lib", "crates/msrv/cli"] members = [ "benchmarks", "crates/cli", + "crates/creation-macros", "crates/js-sys", "crates/test", "crates/test/sample", diff --git a/crates/creation-macros/Cargo.toml b/crates/creation-macros/Cargo.toml new file mode 100644 index 00000000000..ec62526f3c0 --- /dev/null +++ b/crates/creation-macros/Cargo.toml @@ -0,0 +1,38 @@ +[package] +authors = ["The wasm-bindgen Developers"] +categories = ["wasm"] +description = """ +Convenience macros for creating Javascript objects and arrays. For more +information see https://github.com/rustwasm/wasm-bindgen. +""" +documentation = "https://rustwasm.github.io/wasm-bindgen/" +edition = "2021" +homepage = "https://rustwasm.github.io/wasm-bindgen/" +include = ["/LICENSE-*", "/src"] +license = "MIT OR Apache-2.0" +name = "wasm-bindgen-creation-macros" +repository = "https://github.com/rustwasm/wasm-bindgen/tree/master/crates/creation-macros" +rust-version = "1.76" +version = "0.2.100" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.95" +quote = "1.0.40" +syn = "2.0.101" + +[dev-dependencies] +js-sys = { path = "../js-sys" } +wasm-bindgen = { path = "../../" } + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = { path = '../test' } + +[lints] +workspace = true + +[[test]] +name = "creation" +path = "tests/creation.rs" diff --git a/crates/creation-macros/LICENSE-APACHE b/crates/creation-macros/LICENSE-APACHE new file mode 120000 index 00000000000..1cd601d0a3a --- /dev/null +++ b/crates/creation-macros/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/creation-macros/LICENSE-MIT b/crates/creation-macros/LICENSE-MIT new file mode 120000 index 00000000000..b2cfbdc7b0b --- /dev/null +++ b/crates/creation-macros/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/creation-macros/README.md b/crates/creation-macros/README.md new file mode 100644 index 00000000000..378dbd01e51 --- /dev/null +++ b/crates/creation-macros/README.md @@ -0,0 +1,147 @@ +# wasm-bindgen-creation-macros + +This crate provides procedural macros for the `wasm-bindgen` project, specifically focused on code generation and WebAssembly bindings creation. + +## Overview + +The `json!` and `array!` macros help in the creation of `js_sys::Object` and `js_sys::Array`, respectively. Specifically, they cut down on the verbose repetitive code needed to initialize `Object` and `Array` objects using plain Rust. Both macros support the use of any literals or variables that implement [`Into`](https://docs.rs/wasm-bindgen/latest/wasm_bindgen/struct.JsValue.html#trait-implementations). That includes rust strings, floating point numbers and integers, etc. + +### Examples + +```rust +use wasm_bindgen::prelude::*; +use js_sys::{Array, Object}; +use wasm_bindgen_creation_macros::{array, json}; + +fn create_person() -> Object { + let address = json! { + street: "123 Main St", + city: "Tech City", + country: "Rustland" + }; + + json! { + name: "Alice", + age: 30, + hobbies: ["reading", "coding", "hiking"], + address: address, // Note the use of a variable! + fav_floats: [ 1.618, 3.14, 2.718 ], + nested_obj: { + num: 42, + inner_obj: { + msg: "Arbitrary nesting is supported!" + } + } + } +} + +fn create_numbers() -> Array { + // variables work in array! as well. + const FIVE: u32 = 5; + array![1, 2, 3, 4, FIVE] +} + +// Since variables are supported, array! and json! can be +// used together to create complex objects. +fn mix_and_match() -> Object { + let evens = array![2, 4, 6, 8]; + let odds = array![1, 3, 6, 7]; + + let rust = json! { + language: "Rust", + mascot: "Crab" + }; + + let go = json! { + language: "Go", + mascot: "Gopher" + }; + + let languages_array = array![ rust, go, { language: "Python", mascot: "Snakes" } ]; + + json! { + evens: evens, + odds: odds, + languages: languages_array + } +} +``` + +## A Note on Parsing + +The parser used is not sophisticated; Rust values that are not **simple** Rust literals should be stored in a variable first, then the variable should be added to the macro. Attempting to pass non-simple rust syntax will cause compilation to fail. + +```rust +use wasm_bindgen::prelude::*; +use js_sys::{Array, Object}; +use wasm_bindgen_creation_macros::{array, json}; + +pub struct CustomJsValue(u32); + +impl Into for CustomJsValue { + fn into(self) -> JsValue { + self.0.into() + } +} + +// WILL NOT COMPILE +fn incorrect() -> Object { + json! { + custom: CustomJsValue(42) + } +} + +// Do this instead +fn correct() -> Object { + let custom = CustomJsValue(42); + json! { + js_value: custom + } +} + +// WILL NOT COMPILE +fn also_invalid() -> Object { + json! { + array: array![1, 2, 3] + } +} + +// Do this instead +fn also_correct() -> Object { + let array = array![1, 2, 3]; + json! { + array: array + } + +} +``` + +Also, `json!` does not (currently) support string literal JSON keys. + + +```rust +use wasm_bindgen::prelude::*; +use js_sys::{Array, Object}; +use wasm_bindgen_creation_macros::{array, json}; + +// WILL NOT COMPILE +fn incorrect() -> Object { + json! { + "key": 42 + } +} + +// Do this instead +fn correct() -> Object { + json! { + key: 42 + } +} +``` + +## Testing +To run the test suite, run `cargo test --target wasm32-unknown-unknown`. + +```bash +cargo test --target wasm32-unknown-unknown +``` \ No newline at end of file diff --git a/crates/creation-macros/src/lib.rs b/crates/creation-macros/src/lib.rs new file mode 100644 index 00000000000..40052c3ee4d --- /dev/null +++ b/crates/creation-macros/src/lib.rs @@ -0,0 +1,498 @@ +//! Procedural macros for creating JavaScript objects and arrays in WebAssembly. +//! +//! This crate provides two macros: +//! - [`json!`] - Creates `js_sys::Object` instances using JavaScript object literal syntax +//! - [`array!`] - Creates `js_sys::Array` instances using array literal syntax +//! +//! Both macros support any Rust value that implements `Into`, including: +//! - **Strings**: `&'static str`, `String` and `&str` +//! - **Numbers**: Integers and floating-point +//! - **Booleans** +//! - **Nested objects and arrays**: Using `{}` and `[]` syntax +//! - **`null` and `undefined` JavaScript values** +//! - **Custom types**: Type must implement `Into` +//! +//! # Example +//! ```rust,no_run +//! use js_sys::{Array, Object}; +//! use wasm_bindgen_creation_macros::{array, json}; +//! +//! // Create a JavaScript object with nested structure +//! let person = json! { +//! name: "Alice", +//! age: 30, +//! hobbies: ["reading", "coding"], +//! address: { +//! street: "123 Main St", +//! city: "Techville" +//! } +//! }; +//! +//! // Create a JavaScript array with mixed types +//! let data = array![ +//! "string", +//! 42, +//! true, +//! { key: "value" } +//! ]; +//! ``` +//! +//! See the individual macro documentation for more details and examples. + +use proc_macro::TokenStream; +use proc_macro2::{TokenStream as TokenStream2, TokenTree}; +use quote::quote; +use syn::{ + braced, bracketed, + parse::{Parse, ParseBuffer, ParseStream}, + parse_macro_input, + token::{Brace, Bracket}, + Ident, LitBool, LitFloat, LitInt, LitStr, Token, +}; + +#[derive(Clone)] +struct JsonObject(JsonValue); + +#[derive(Clone)] +struct JsonArray(Vec); + +impl JsonObject { + fn to_tokens(&self) -> TokenStream2 { + self.0.to_tokens() + } +} + +impl Parse for JsonObject { + fn parse(input: ParseStream) -> syn::Result { + if input.is_empty() { + return Ok(JsonObject(JsonValue::Object(vec![]))); + } + + Ok(JsonObject(JsonValue::parse_object_inner(input)?)) + } +} + +impl Parse for JsonArray { + fn parse(input: ParseStream) -> syn::Result { + Ok(JsonArray(JsonValue::parse_array_inner(input)?)) + } +} + +impl JsonArray { + fn to_tokens(&self) -> TokenStream2 { + JsonValue::array_to_tokens(&self.0) + } +} + +#[derive(Clone)] +enum JsonValue { + Object(Vec<(Ident, JsonValue)>), + External(Ident), // Local object, array, or rust variable passed to macro + Array(Vec), + String(String), + Number(String), + Boolean(bool), + Null, + Undefined, +} + +impl Parse for JsonValue { + fn parse(input: ParseStream) -> syn::Result { + let value = if input.peek(Brace) { + let content; + braced!(content in input); + Self::parse_object_inner(&content)? + } else if input.peek(Bracket) { + let content; + bracketed!(content in input); + JsonValue::Array(Self::parse_array_inner(&content)?) + } else if input.peek(LitStr) { + let lit = input.parse::()?; + if input.peek2(Token![:]) { + return Err(syn::Error::new( + lit.span(), + "String literal keys are not supported", + )); + } + JsonValue::String(lit.value()) + } else if input.peek(LitInt) || input.peek(LitFloat) { + JsonValue::Number(Self::parse_number(input)?) + } else if input.peek(LitBool) { + JsonValue::Boolean(input.parse::()?.value) + } else if input.peek(Ident) { + Self::parse_ident(input)? + } else { + return Err(syn::Error::new(input.span(), "Expected valid JSON")); + }; + + Ok(value) + } +} + +impl JsonValue { + fn array_to_tokens(elements: &[JsonValue]) -> TokenStream2 { + let element_tokens = elements.iter().map(JsonValue::to_tokens); + quote! { + { + let array = js_sys::Array::new(); + #( + array.push(&#element_tokens.into()); + )* + array + } + } + } + + fn parse_object_inner(content: &ParseBuffer<'_>) -> syn::Result { + let mut pairs = Vec::new(); + + while content.peek(Ident) { + let key = content.parse::()?; + content.parse::()?; + let value = content.parse::()?; + pairs.push((key, value)); + + if !content.is_empty() { + content.parse::()?; + } + } + + if !content.is_empty() { + return Err(syn::Error::new( + content.span(), + "Expected ident, found something else", + )); + } + + Ok(JsonValue::Object(pairs)) + } + + fn parse_array_inner(content: &ParseBuffer<'_>) -> syn::Result> { + let mut elements = Vec::new(); + while !content.is_empty() { + elements.push(content.parse::()?); + if !content.is_empty() { + content.parse::()?; + } + } + Ok(elements) + } + + fn parse_ident(content: &ParseBuffer<'_>) -> syn::Result { + const NULL: &str = "null"; + const UNDEFINED: &str = "undefined"; + + let ident = content.parse::()?; + Ok(if content.peek(Token![:]) { + content.parse::()? + } else if ident == NULL { + JsonValue::Null + } else if ident == UNDEFINED { + JsonValue::Undefined + } else { + JsonValue::External(ident) + }) + } + + fn parse_number(content: &ParseBuffer<'_>) -> syn::Result { + assert!(content.peek(LitInt) || content.peek(LitFloat)); + let value = content.step(|cursor| { + let mut rest = *cursor; + let mut content = String::new(); + + if let Some((TokenTree::Literal(lit), next)) = rest.token_tree() { + content.push_str(&lit.to_string()); + rest = next; + } + Ok((content, rest)) + })?; + Ok(value) + } + + fn to_tokens(&self) -> TokenStream2 { + match self { + JsonValue::Object(pairs) => { + let keys = pairs.iter().map(|(key, _)| key); + let values = pairs.iter().map(|(_, value)| value.to_tokens()); + quote! { + { + let obj = js_sys::Object::new(); + #( + js_sys::Reflect::set(&obj, &stringify!(#keys).into(), &#values.into()).unwrap(); + )* + obj + } + } + } + JsonValue::Array(elements) => Self::array_to_tokens(elements), + JsonValue::External(ident) => quote! { #ident }, + JsonValue::String(s) => quote! { #s }, + JsonValue::Number(n) => { + let n = n.parse::().unwrap_or(0.0); + quote! { #n } + } + JsonValue::Boolean(b) => quote! { #b }, + JsonValue::Null => quote! { wasm_bindgen::JsValue::NULL }, + JsonValue::Undefined => quote! { wasm_bindgen::JsValue::UNDEFINED }, + } + } +} + +/// The `json!` macro allows you to create `js_sys::Object`s using JavaScript object literal syntax. +/// It supports all basic JavaScript types and can handle nested structures. +/// +/// # Supported Types +/// - **Strings**: `&'static str`, `String` and `&str` +/// - **Numbers**: Integers and floating-point +/// - **Booleans** +/// - **JavaScript array literals**: Using `[]` syntax +/// - **JavaScript object literals**: Using `{}` syntax +/// - **`null` and `undefined` JavaScript values/keywords** +/// - **Variables**: Any Rust variable whose type implements `Into` can be passed to `json!` +/// - **Custom types**: Type must implement `Into` (See [Syntax Limitations](#syntax-limitations)) +/// +/// # Basic Usage +/// ```rust,no_run +/// use js_sys::Object; +/// use wasm_bindgen_creation_macros::json; +/// +/// let obj = json! { +/// name: "John", +/// age: 30, +/// is_student: true, +/// hobbies: ["reading", "coding"] +/// }; +/// ``` +/// +/// # Nested Structures +/// The `json!` macro supports nested structures of arbitrary depth. +/// ```rust,no_run +/// use js_sys::Object; +/// use wasm_bindgen_creation_macros::json; +/// +/// let obj = json! { +/// name: "John", +/// address: { +/// street: "123 Main St", +/// city: "Anytown" +/// }, +/// friends: [ +/// { name: "Jane", age: 25 }, +/// { name: "Jim", age: 30 } +/// ] +/// }; +/// ``` +/// +/// # Variable Usage +/// All Rust values that implement `Into` can be used in the macro. For simple types, +/// literals can be added directly. For more complex types, they should be stored in a variable first (see [Syntax Limitations](#syntax-limitations)). +/// ```rust,no_run +/// use js_sys::Object; +/// use wasm_bindgen_creation_macros::json; +/// +/// let name = "John"; +/// let hobbies = vec!["reading".to_string(), "coding".to_string()]; +/// let address_obj = json! { +/// street: "123 Main St", +/// city: "Anytown" +/// }; +/// +/// let obj = json! { +/// name: name, +/// hobbies: hobbies, +/// address: address_obj +/// }; +/// ``` +/// +/// # Comments +/// The macro supports Rust-style comments: +/// ```rust,no_run +/// use js_sys::Object; +/// use wasm_bindgen_creation_macros::json; +/// +/// let obj = json! { +/// name: "John", // The person's name +/// age: 30 // Their age +/// }; +/// ``` +/// +/// # Syntax Limitations +/// The parser is unsophisticated; it only supports simple Rust literals. Expressions, struct instantiations, etc. should be +/// stored in variables first: +/// +/// ```compile_fail +/// use js_sys::Object; +/// use wasm_bindgen_creation_macros::{array, json}; +/// +/// struct CustomJsValue(u32); +/// impl Into for CustomJsValue { +/// fn into(self) -> JsValue { +/// self.0.into() +/// } +/// } +/// +/// let obj = json! { +/// custom: CustomJsValue(42), +/// array: array![1, 2, 3] +/// }; +/// ``` +/// +/// Do this instead: +/// ```rust,no_run +/// use js_sys::Object; +/// use wasm_bindgen::JsValue; +/// use wasm_bindgen_creation_macros::{array, json}; +/// +/// struct CustomJsValue(u32); +/// impl Into for CustomJsValue { +/// fn into(self) -> JsValue { +/// self.0.into() +/// } +/// } +/// +/// let custom = CustomJsValue(42); +/// let array = array![1, 2, 3]; +/// let obj = json! { +/// custom: custom, +/// array: array +/// }; +/// ``` +/// +/// String literal keys are not (currently) supported: +/// ```compile_fail +/// use js_sys::Object; +/// use wasm_bindgen_creation_macros::json; +/// +/// let obj = json! { +/// "key": 42 +/// }; +/// ``` +/// +/// Do this instead: +/// ```rust,no_run +/// use js_sys::Object; +/// use wasm_bindgen_creation_macros::json; +/// +/// let obj = json! { +/// key: 42 +/// }; +/// ``` +#[proc_macro] +pub fn json(input: TokenStream) -> TokenStream { + let value = parse_macro_input!(input as JsonObject); + value.to_tokens().into() +} + +/// The `array!` macro provides a convenient way to create `js_sys::Array` instances. +/// +/// # Supported Types +/// - **Strings**: `&'static str`, `String` and `&str` +/// - **Numbers**: Integers and floating-point +/// - **Booleans** +/// - **Nested arrays**: Using `[]` syntax +/// - **JavaScript object literals**: Using `{}` syntax +/// - **`null` and `undefined` JavaScript values** +/// - **Variables**: Any Rust variable whose type implements `Into` can be passed to `array!` +/// - **Custom types**: Type must implement `Into` (See [Syntax Limitations](#syntax-limitations)) +/// +/// # Basic Usage +/// ```rust,no_run +/// use js_sys::Array; +/// use wasm_bindgen_creation_macros::array; +/// +/// let numbers = array![1, 2, 3, 4, 5]; +/// let strings = array!["hello", "world"]; +/// ``` +/// +/// # Nested Arrays +/// ```rust,no_run +/// use js_sys::Array; +/// use wasm_bindgen_creation_macros::array; +/// +/// let matrix = array![ +/// [1, 2, 3], +/// [4, 5, 6] +/// ]; +/// ``` +/// +/// # Variable Usage +/// ```rust,no_run +/// use js_sys::Array; +/// use wasm_bindgen_creation_macros::array; +/// +/// let name = "John".to_string(); +/// let arr = array![name, "Jane", "Jim"]; +/// ``` +/// +/// # Custom Types +/// Works with any type that implements `Into`: +/// ```rust,no_run +/// use js_sys::Array; +/// use wasm_bindgen::JsValue; +/// use wasm_bindgen_creation_macros::array; +/// +/// struct CustomJsValue(u32); +/// impl Into for CustomJsValue { +/// fn into(self) -> JsValue { +/// self.0.into() +/// } +/// } +/// +/// let custom = CustomJsValue(42); +/// let arr = array![custom]; +/// ``` +/// +/// # Comments +/// Supports Rust-style comments: +/// ```rust,no_run +/// use js_sys::Array; +/// use wasm_bindgen_creation_macros::array; +/// +/// let arr = array![ +/// 1, // First element +/// 2, // Second element +/// 3 // Third element +/// ]; +/// ``` +/// +/// # Syntax Limitations +/// The parser only supports simple Rust literals. Complex expressions or struct instantiations should be +/// stored in variables first: +/// +/// ```compile_fail +/// use js_sys::Array; +/// use wasm_bindgen::JsValue; +/// use wasm_bindgen_creation_macros::array; +/// +/// struct CustomJsValue(u32); +/// impl Into for CustomJsValue { +/// fn into(self) -> JsValue { +/// self.0.into() +/// } +/// } +/// +/// let arr = array![CustomJsValue(42), array![1, 2, 3]]; +/// ``` +/// +/// Do this instead: +/// ```rust,no_run +/// use js_sys::Array; +/// use wasm_bindgen::JsValue; +/// use wasm_bindgen_creation_macros::array; +/// +/// struct CustomJsValue(u32); +/// impl Into for CustomJsValue { +/// fn into(self) -> JsValue { +/// self.0.into() +/// } +/// } +/// +/// let custom = CustomJsValue(42); +/// let inner_array = array![1, 2, 3]; +/// let arr = array![custom, inner_array]; +/// ``` +#[proc_macro] +pub fn array(input: TokenStream) -> TokenStream { + let value = parse_macro_input!(input as JsonArray); + value.to_tokens().into() +} diff --git a/crates/creation-macros/tests/creation.rs b/crates/creation-macros/tests/creation.rs new file mode 100644 index 00000000000..8a005c30158 --- /dev/null +++ b/crates/creation-macros/tests/creation.rs @@ -0,0 +1,593 @@ +#![cfg(target_arch = "wasm32")] + +use js_sys::{Array, Object, Reflect}; +use wasm_bindgen::JsValue; +use wasm_bindgen_creation_macros::{array, json}; +use wasm_bindgen_test::wasm_bindgen_test; +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +const JOHN: &str = "John"; +const JOHN_AGE: i32 = 30; +const IS_STUDENT: bool = true; + +const JANE: &str = "Jane"; +const JANE_AGE: i32 = 25; + +const JIM: &str = "Jim"; +const JIM_AGE: i32 = 56; + +const STREET: &str = "123 Main St"; +const CITY: &str = "Anytown"; +const STATE: &str = "CA"; +const ZIP: &str = "12345"; + +#[cfg(test)] +macro_rules! assert_js_eq { + ($left:expr, $right:expr) => { + assert_eq!(format!("{:#?}", $left), format!("{:#?}", $right)); + }; +} + +#[wasm_bindgen_test] +fn sanity_check() { + assert_js_eq!(Object::new(), Object::new()); +} + +#[wasm_bindgen_test] +fn empty_json() { + let obj = json! {}; + let expected = Object::new(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn simple_json() { + let obj = json! { + name: "John", + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn with_all_literals() { + let obj = json! { + name: "John", + age: 30, + favorite_float: 3.14, + is_student: true, + hobbies: ["reading", "traveling", "coding"], + address: { + street: "123 Main St", + city: "Anytown", + state: "CA", + zip: "12345", + }, + empty: null, + empty2: undefined, + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JOHN_AGE.into()).unwrap(); + Reflect::set(&expected, &"favorite_float".into(), &3.14.into()).unwrap(); + Reflect::set(&expected, &"is_student".into(), &IS_STUDENT.into()).unwrap(); + Reflect::set( + &expected, + &"hobbies".into(), + &Array::of3(&"reading".into(), &"traveling".into(), &"coding".into()).into(), + ) + .unwrap(); + + let address = Object::new(); + Reflect::set(&address, &"street".into(), &STREET.into()).unwrap(); + Reflect::set(&address, &"city".into(), &CITY.into()).unwrap(); + Reflect::set(&address, &"state".into(), &STATE.into()).unwrap(); + Reflect::set(&address, &"zip".into(), &ZIP.into()).unwrap(); + Reflect::set(&expected, &"address".into(), &address.into()).unwrap(); + + Reflect::set(&expected, &"empty".into(), &JsValue::NULL).unwrap(); + Reflect::set(&expected, &"empty2".into(), &JsValue::UNDEFINED).unwrap(); + + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn level_1_nested() { + let obj = json! { + name: JOHN, + address: { + street: STREET, + city: CITY, + state: STATE, + zip: ZIP + } + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + let address = Object::new(); + Reflect::set(&address, &"street".into(), &STREET.into()).unwrap(); + Reflect::set(&address, &"city".into(), &CITY.into()).unwrap(); + Reflect::set(&address, &"state".into(), &STATE.into()).unwrap(); + Reflect::set(&address, &"zip".into(), &ZIP.into()).unwrap(); + Reflect::set(&expected, &"address".into(), &address.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn with_array() { + let obj = json! { + name: JOHN, + friends: [ + { + name: JANE, + age: JANE_AGE, + }, + { + name: JIM, + age: JIM_AGE, + } + ], + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + let array = { + let friend1 = Object::new(); + Reflect::set(&friend1, &"name".into(), &JANE.into()).unwrap(); + Reflect::set(&friend1, &"age".into(), &JANE_AGE.into()).unwrap(); + + let friend2 = Object::new(); + Reflect::set(&friend2, &"name".into(), &JIM.into()).unwrap(); + Reflect::set(&friend2, &"age".into(), &JIM_AGE.into()).unwrap(); + Array::of2(&friend1, &friend2) + }; + Reflect::set(&expected, &"friends".into(), &array.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn complex_json() { + let obj = json! { + name: JOHN, + age: JOHN_AGE, + is_student: IS_STUDENT, + hobbies: ["reading", "traveling", "coding"], + address: { + street: STREET, + city: CITY, + state: STATE, + zip: ZIP, + }, + friends: [ + { + name: JANE, + age: JANE_AGE, + }, + { + name: JIM, + age: JIM_AGE, + } + ], + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JOHN_AGE.into()).unwrap(); + Reflect::set(&expected, &"is_student".into(), &IS_STUDENT.into()).unwrap(); + Reflect::set( + &expected, + &"hobbies".into(), + &Array::of3(&"reading".into(), &"traveling".into(), &"coding".into()).into(), + ) + .unwrap(); + let address = Object::new(); + Reflect::set(&address, &"street".into(), &STREET.into()).unwrap(); + Reflect::set(&address, &"city".into(), &CITY.into()).unwrap(); + Reflect::set(&address, &"state".into(), &STATE.into()).unwrap(); + Reflect::set(&address, &"zip".into(), &ZIP.into()).unwrap(); + Reflect::set(&expected, &"address".into(), &address.into()).unwrap(); + let friends = { + let friend1 = Object::new(); + Reflect::set(&friend1, &"name".into(), &JANE.into()).unwrap(); + Reflect::set(&friend1, &"age".into(), &JANE_AGE.into()).unwrap(); + + let friend2 = Object::new(); + Reflect::set(&friend2, &"name".into(), &JIM.into()).unwrap(); + Reflect::set(&friend2, &"age".into(), &JIM_AGE.into()).unwrap(); + Array::of2(&friend1, &friend2) + }; + Reflect::set(&expected, &"friends".into(), &friends.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn json_with_local_var() { + let local = Object::new(); + let local_clone = local.clone(); + Reflect::set(&local, &"property".into(), &"value".into()).unwrap(); + + let local_array = Array::of2(&"local".into(), &"local2".into()); + let local_array_clone = local_array.clone(); + + let obj = json! { + name: JOHN, + age: JOHN_AGE, + is_student: IS_STUDENT, + hobbies: ["reading", "traveling", "coding"], + locals: [local], + local_array: local_array, + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JOHN_AGE.into()).unwrap(); + Reflect::set(&expected, &"is_student".into(), &true.into()).unwrap(); + Reflect::set( + &expected, + &"hobbies".into(), + &Array::of3(&"reading".into(), &"traveling".into(), &"coding".into()).into(), + ) + .unwrap(); + Reflect::set( + &expected, + &"locals".into(), + &Array::of1(&local_clone.into()), + ) + .unwrap(); + Reflect::set(&expected, &"local_array".into(), &local_array_clone.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn json_with_local_vars() { + const HOBBIES: [&str; 3] = ["reading", "traveling", "coding"]; + let jane = Object::new(); + Reflect::set(&jane, &"name".into(), &JANE.into()).unwrap(); + Reflect::set(&jane, &"age".into(), &JANE_AGE.into()).unwrap(); + let friends: Array = Array::of1(&jane.clone().into()); + + let friends_clone = friends.clone(); + let obj = json! { + name: JOHN, + age: JOHN_AGE, + is_student: IS_STUDENT, + hobbies: ["reading", "traveling", "coding"], // [&str; 3] does not impl Into + address: { + street: STREET, + city: CITY, + state: STATE, + zip: ZIP, + }, + friends: friends, + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JOHN_AGE.into()).unwrap(); + Reflect::set(&expected, &"is_student".into(), &IS_STUDENT.into()).unwrap(); + Reflect::set( + &expected, + &"hobbies".into(), + &Array::of3(&"reading".into(), &"traveling".into(), &"coding".into()).into(), + ) + .unwrap(); + let address = Object::new(); + Reflect::set(&address, &"street".into(), &STREET.into()).unwrap(); + Reflect::set(&address, &"city".into(), &CITY.into()).unwrap(); + Reflect::set(&address, &"state".into(), &STATE.into()).unwrap(); + Reflect::set(&address, &"zip".into(), &ZIP.into()).unwrap(); + Reflect::set(&expected, &"address".into(), &address.into()).unwrap(); + Reflect::set(&expected, &"friends".into(), &friends_clone.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn chain_json() { + let one = json! { + num: 1 + }; + let one_clone = one.clone(); + + let two = json! { + num: 2 + }; + let two_clone = two.clone(); + + let three = json! { + num: 3 + }; + let three_clone = three.clone(); + + let address = json! { + street: STREET, + city: CITY, + state: STATE, + zip: ZIP, + }; + let address_clone = address.clone(); + + let obj = json! { + name: JOHN, + age: JOHN_AGE, + address: address, + numbers: [one, two, three], + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JOHN_AGE.into()).unwrap(); + Reflect::set(&expected, &"address".into(), &address_clone.into()).unwrap(); + Reflect::set( + &expected, + &"numbers".into(), + &Array::of3(&one_clone.into(), &two_clone.into(), &three_clone.into()).into(), + ) + .unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn with_null_and_undefined() { + let obj = json! { + name: JOHN, + age: null, + is_student: undefined, + friends: [null, undefined], + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JsValue::NULL).unwrap(); + Reflect::set(&expected, &"is_student".into(), &JsValue::UNDEFINED).unwrap(); + Reflect::set( + &expected, + &"friends".into(), + &Array::of2(&JsValue::NULL, &JsValue::UNDEFINED).into(), + ) + .unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn with_string() { + let john = JOHN.to_string(); + let obj = json! { + name: john, + age: JOHN_AGE, + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JOHN_AGE.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn with_string_borrow() { + let john = &JOHN.to_string(); + let obj = json! { + name: john, + age: JOHN_AGE, + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JOHN_AGE.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn with_str() { + let john = "John"; + let obj = json! { + name: john, + age: JOHN_AGE, + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JOHN_AGE.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn with_vecs() { + // Test both Vec and Vec + let hobbies: Vec = vec!["reading".into(), "traveling".into(), "coding".into()]; + let vec2 = vec![ + "reading".to_string(), + "traveling".to_string(), + "coding".to_string(), + ]; + let vec2_clone = vec2.clone(); + let hobbies_clone = hobbies.clone(); + let obj = json! { + name: JOHN, + age: JOHN_AGE, + hobbies: hobbies, + vec2: vec2, + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JOHN_AGE.into()).unwrap(); + Reflect::set( + &expected, + &"hobbies".into(), + &Array::from_iter(&hobbies_clone), + ) + .unwrap(); + Reflect::set(&expected, &"vec2".into(), &vec2_clone.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn with_comments() { + let obj = json! { + name: JOHN, // name: "John" + age: JOHN_AGE, // age: 30 + }; + + let expected = Object::new(); + Reflect::set(&expected, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&expected, &"age".into(), &JOHN_AGE.into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn obj_with_custom_js_value() { + struct CustomJsValue(u32); + impl Into for CustomJsValue { + fn into(self) -> JsValue { + self.0.into() + } + } + + let custom = CustomJsValue(42); + let obj = json! { + custom: custom + }; + + let expected = Object::new(); + Reflect::set(&expected, &"custom".into(), &CustomJsValue(42).into()).unwrap(); + assert_js_eq!(obj, expected); +} + +#[wasm_bindgen_test] +fn simple_array() { + let arr = array![1, 2, 3]; + let expected = Array::of3(&1.into(), &2.into(), &3.into()); + assert_js_eq!(arr, expected); +} + +#[wasm_bindgen_test] +fn array_with_local_var() { + let string1 = "string1".to_string(); + let string2 = "string2".to_string(); + let string1_clone = string1.clone(); + let string2_clone = string2.clone(); + + let arr = array![string1, string2]; + let expected = Array::of2(&string1_clone.into(), &string2_clone.into()); + assert_js_eq!(arr, expected); +} + +#[wasm_bindgen_test] +fn with_objects() { + let john = Object::new(); + Reflect::set(&john, &"name".into(), &JOHN.into()).unwrap(); + Reflect::set(&john, &"age".into(), &JOHN_AGE.into()).unwrap(); + let john_clone = john.clone(); + + let jane = Object::new(); + Reflect::set(&jane, &"name".into(), &JANE.into()).unwrap(); + Reflect::set(&jane, &"age".into(), &JANE_AGE.into()).unwrap(); + let jane_clone = jane.clone(); + + let arr = array![john, jane]; + let expected = Array::of2(&john_clone.into(), &jane_clone.into()); + assert_js_eq!(arr, expected); +} + +#[wasm_bindgen_test] +fn array_of_arrays() { + let arr = array![[1, 2, 3], [4, 5, 6]]; + let expected = Array::of2( + &Array::of3(&1.into(), &2.into(), &3.into()).into(), + &Array::of3(&4.into(), &5.into(), &6.into()).into(), + ); + assert_js_eq!(arr, expected); +} + +#[wasm_bindgen_test] +fn array_with_comments() { + let arr = array![ + 1, // 1 + 2, // 2 + 3, // 3 + ]; + let expected = Array::of3(&1.into(), &2.into(), &3.into()); + assert_js_eq!(arr, expected); +} + +#[wasm_bindgen_test] +fn array_with_custom_js_value() { + struct CustomJsValue(u32); + impl Into for CustomJsValue { + fn into(self) -> JsValue { + self.0.into() + } + } + + let custom = CustomJsValue(42); + let array = array![custom]; + let expected = Array::of1(&CustomJsValue(42).into()); + assert_js_eq!(array, expected); +} + +#[wasm_bindgen_test] +fn mix_and_match() { + let evens = array![2, 4, 6, 8]; + let odds = array![1, 3, 6, 7]; + + let rust = json! { + language: "Rust", + mascot: "Crab" + }; + + let go = json! { + language: "Go", + mascot: "Gopher" + }; + + let languages_array = array![rust, go, { language: "Python", mascot: "Snakes" } ]; + + let obj = json! { + evens: evens, + odds: odds, + languages: languages_array + }; + + let rust_expected = Object::new(); + Reflect::set(&rust_expected, &"language".into(), &"Rust".into()).unwrap(); + Reflect::set(&rust_expected, &"mascot".into(), &"Crab".into()).unwrap(); + + let go_expected = Object::new(); + Reflect::set(&go_expected, &"language".into(), &"Go".into()).unwrap(); + Reflect::set(&go_expected, &"mascot".into(), &"Gopher".into()).unwrap(); + + let python_expected = Object::new(); + Reflect::set(&python_expected, &"language".into(), &"Python".into()).unwrap(); + Reflect::set(&python_expected, &"mascot".into(), &"Snakes".into()).unwrap(); + + let expected = Object::new(); + Reflect::set( + &expected, + &"evens".into(), + &Array::of4(&2.into(), &4.into(), &6.into(), &8.into()).into(), + ) + .unwrap(); + Reflect::set( + &expected, + &"odds".into(), + &Array::of4(&1.into(), &3.into(), &6.into(), &7.into()).into(), + ) + .unwrap(); + Reflect::set( + &expected, + &"languages".into(), + &Array::of3( + &rust_expected.into(), + &go_expected.into(), + &python_expected.into(), + ) + .into(), + ) + .unwrap(); + + assert_js_eq!(obj, expected); +}