diff --git a/Cargo.lock b/Cargo.lock index 4dcb7a68b59..afdd5d0db8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "boa_derive" +version = "0.16.0" +dependencies = [ + "boa_engine", + "proc-macro2", + "quote", + "syn", + "trybuild", +] + [[package]] name = "boa_engine" version = "0.16.0" @@ -113,6 +124,7 @@ dependencies = [ name = "boa_examples" version = "0.16.0" dependencies = [ + "boa_derive", "boa_engine", "boa_gc", "gc", @@ -585,6 +597,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "half" version = "1.8.2" @@ -1557,6 +1575,30 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "trybuild" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e13556ba7dba80b3c76d1331989a341290c77efcf688eca6c307ee3066383dd" +dependencies = [ + "glob", + "once_cell", + "serde", + "serde_derive", + "serde_json", + "termcolor", + "toml", +] + [[package]] name = "unicode-general-category" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index 20631cf579f..d8bdad44fd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "boa_unicode", "boa_wasm", "boa_examples", + "boa_derive", ] [workspace.metadata.workspaces] diff --git a/boa_derive/Cargo.toml b/boa_derive/Cargo.toml new file mode 100644 index 00000000000..058dd83eb46 --- /dev/null +++ b/boa_derive/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "boa_derive" +version = "0.16.0" +edition = "2021" +rust-version = "1.60" +authors = ["boa-dev"] +description = "Ths crate adds derive macros for the main boa_engine crate." +repository = "https://github.com/boa-dev/boa" +license = "Unlicense/MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0.99" +quote = "1.0.21" +proc-macro2 = "1.0.43" + +[dev-dependencies] +trybuild = "1.0.64" +boa_engine = { path = "../boa_engine" } diff --git a/boa_derive/src/lib.rs b/boa_derive/src/lib.rs new file mode 100644 index 00000000000..cc8334c4613 --- /dev/null +++ b/boa_derive/src/lib.rs @@ -0,0 +1,71 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Fields, FieldsNamed}; + +#[proc_macro_derive(TryFromJs)] +pub fn derive_try_from_js(input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let input = parse_macro_input!(input as DeriveInput); + + let data = if let Data::Struct(data) = input.data { + data + } else { + panic!("you can only derive TryFromJs for structs"); + }; + + let fields = if let Fields::Named(fields) = data.fields { + fields + } else { + panic!("you can only derive TryFromJs for named-field structs") + }; + + let conv = generate_conversion(fields); + + let type_name = input.ident; + + // Build the output, possibly using quasi-quotation + let expanded = quote! { + impl boa_engine::value::conversions::try_from_js::TryFromJs for #type_name { + fn try_from_js(value: &boa_engine::JsValue, context: &mut boa_engine::Context) -> boa_engine::JsResult { + match value { + boa_engine::JsValue::Object(o) => {#conv}, + _ => context.throw_type_error("cannot convert value to a #type_name"), + } + } + } + }; + + // Hand the output tokens back to the compiler + expanded.into() +} + +fn generate_conversion(fields: FieldsNamed) -> proc_macro2::TokenStream { + let mut field_list = Vec::with_capacity(fields.named.len()); + + let fields = fields.named.into_iter().map(|field| { + let name = field + .ident + .expect("you can only derive TryFromJs for named-field structs"); + let name_str = format!("{name}"); + field_list.push(name.clone()); + + let error_str = format!("cannot get property {name_str} of value"); + + quote! { + let #name = props.get(&#name_str.into()).ok_or_else(|| { + context.construct_type_error(#error_str) + })?.value().ok_or_else(|| { + context.construct_type_error(#error_str) + })?.clone().try_js_into(context)?; + } + }); + + quote! { + let o = o.borrow(); + let props = o.properties(); + #(#fields)* + Ok(Self { + #(#field_list),* + }) + } +} diff --git a/boa_derive/tests/derive/simple_struct.rs b/boa_derive/tests/derive/simple_struct.rs new file mode 100644 index 00000000000..518a22c9414 --- /dev/null +++ b/boa_derive/tests/derive/simple_struct.rs @@ -0,0 +1,8 @@ +use boa_derive::TryFromJs; + +#[derive(TryFromJs)] +struct TestStruct { + inner: bool, +} + +fn main() {} diff --git a/boa_derive/tests/test.rs b/boa_derive/tests/test.rs new file mode 100644 index 00000000000..fabcfc1f5ba --- /dev/null +++ b/boa_derive/tests/test.rs @@ -0,0 +1,5 @@ +#[test] +fn test() { + let t = trybuild::TestCases::new(); + t.pass("tests/derive/simple_struct.rs"); +} diff --git a/boa_engine/src/value/conversions.rs b/boa_engine/src/value/conversions/mod.rs similarity index 99% rename from boa_engine/src/value/conversions.rs rename to boa_engine/src/value/conversions/mod.rs index 771acbbf8c2..2496fed2fe9 100644 --- a/boa_engine/src/value/conversions.rs +++ b/boa_engine/src/value/conversions/mod.rs @@ -1,5 +1,8 @@ use super::{Display, JsBigInt, JsObject, JsString, JsSymbol, JsValue, Profiler}; +mod serde_json; +pub mod try_from_js; + impl From<&Self> for JsValue { #[inline] fn from(value: &Self) -> Self { diff --git a/boa_engine/src/value/serde_json.rs b/boa_engine/src/value/conversions/serde_json.rs similarity index 100% rename from boa_engine/src/value/serde_json.rs rename to boa_engine/src/value/conversions/serde_json.rs diff --git a/boa_engine/src/value/conversions/try_from_js.rs b/boa_engine/src/value/conversions/try_from_js.rs new file mode 100644 index 00000000000..576b98619f3 --- /dev/null +++ b/boa_engine/src/value/conversions/try_from_js.rs @@ -0,0 +1,186 @@ +use crate::{Context, JsBigInt, JsResult, JsValue}; +use num_bigint::BigInt; + +/// This trait adds a fallible and efficient conversions from a [`JsValue`] to Rust types. +pub trait TryFromJs: Sized { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult; +} + +impl JsValue { + /// This function is the inverse of [`TryFromJs`]. It tries to convert a [`JsValue`] to a given + /// Rust type. + pub fn try_js_into(&self, context: &mut Context) -> JsResult + where + T: TryFromJs, + { + T::try_from_js(self, context) + } +} + +impl TryFromJs for bool { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Boolean(b) => Ok(*b), + _ => context.throw_type_error("cannot convert value to a boolean"), + } + } +} + +impl TryFromJs for String { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::String(s) => Ok(s.as_str().to_owned()), + _ => context.throw_type_error("cannot convert value to a String"), + } + } +} + +impl TryFromJs for Option +where + T: TryFromJs, +{ + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Null | JsValue::Undefined => Ok(None), + value => Ok(Some(T::try_from_js(value, context)?)), + } + } +} + +impl TryFromJs for JsBigInt { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::BigInt(b) => Ok(b.clone()), + _ => context.throw_type_error("cannot convert value to a BigInt"), + } + } +} + +impl TryFromJs for BigInt { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::BigInt(b) => Ok(b.as_inner().clone()), + _ => context.throw_type_error("cannot convert value to a BigInt"), + } + } +} + +impl TryFromJs for JsValue { + fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult { + Ok(value.clone()) + } +} + +impl TryFromJs for f64 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => Ok((*i).into()), + JsValue::Rational(r) => Ok(*r), + _ => context.throw_type_error("cannot convert value to a f64"), + } + } +} + +impl TryFromJs for i8 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => (*i).try_into().map_err(|e| { + context.construct_type_error(format!("cannot convert value to a i8: {e}")) + }), + _ => context.throw_type_error("cannot convert value to a i8"), + } + } +} + +impl TryFromJs for u8 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => (*i).try_into().map_err(|e| { + context.construct_type_error(format!("cannot convert value to a u8: {e}")) + }), + _ => context.throw_type_error("cannot convert value to a u8"), + } + } +} + +impl TryFromJs for i16 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => (*i).try_into().map_err(|e| { + context.construct_type_error(format!("cannot convert value to a i16: {e}")) + }), + _ => context.throw_type_error("cannot convert value to a i16"), + } + } +} + +impl TryFromJs for u16 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => (*i).try_into().map_err(|e| { + context.construct_type_error(format!("cannot convert value to a iu16: {e}")) + }), + _ => context.throw_type_error("cannot convert value to a u16"), + } + } +} + +impl TryFromJs for i32 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => Ok(*i), + _ => context.throw_type_error("cannot convert value to a i32"), + } + } +} + +impl TryFromJs for u32 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => (*i).try_into().map_err(|e| { + context.construct_type_error(format!("cannot convert value to a u32: {e}")) + }), + _ => context.throw_type_error("cannot convert value to a u32"), + } + } +} + +impl TryFromJs for i64 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => Ok((*i).into()), + _ => context.throw_type_error("cannot convert value to a i64"), + } + } +} + +impl TryFromJs for u64 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => (*i).try_into().map_err(|e| { + context.construct_type_error(format!("cannot convert value to a u64: {e}")) + }), + _ => context.throw_type_error("cannot convert value to a u64"), + } + } +} + +impl TryFromJs for i128 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => Ok((*i).into()), + _ => context.throw_type_error("cannot convert value to a i128"), + } + } +} + +impl TryFromJs for u128 { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value { + JsValue::Integer(i) => (*i).try_into().map_err(|e| { + context.construct_type_error(format!("cannot convert value to a u128: {e}")) + }), + _ => context.throw_type_error("cannot convert value to a u128"), + } + } +} diff --git a/boa_engine/src/value/mod.rs b/boa_engine/src/value/mod.rs index d7798c43129..a7131048fde 100644 --- a/boa_engine/src/value/mod.rs +++ b/boa_engine/src/value/mod.rs @@ -28,19 +28,16 @@ use std::{ str::FromStr, }; -mod conversions; +pub mod conversions; pub(crate) mod display; mod equality; mod hash; mod integer; mod operations; -mod serde_json; mod r#type; -pub use conversions::*; +pub use conversions::{try_from_js::*, *}; pub use display::ValueDisplay; -pub use equality::*; -pub use hash::*; pub use integer::IntegerOrInfinity; pub use operations::*; pub use r#type::Type; diff --git a/boa_examples/Cargo.toml b/boa_examples/Cargo.toml index c16c80e651c..2dec13a188e 100644 --- a/boa_examples/Cargo.toml +++ b/boa_examples/Cargo.toml @@ -11,5 +11,6 @@ publish = false [dependencies] boa_engine = { path = "../boa_engine", features = ["console"], version = "0.16.0" } +boa_derive = { path = "../boa_derive", version = "0.16.0" } boa_gc = { path = "../boa_gc", version = "0.16.0" } gc = "0.4.1" diff --git a/boa_examples/src/bin/derive.rs b/boa_examples/src/bin/derive.rs new file mode 100644 index 00000000000..8e0cd330003 --- /dev/null +++ b/boa_examples/src/bin/derive.rs @@ -0,0 +1,27 @@ +use boa_derive::TryFromJs; +use boa_engine::{value::TryFromJs, Context}; + +#[derive(Debug, TryFromJs)] +#[allow(dead_code)] +struct TestStruct { + inner: bool, + hello: String, +} + +fn main() { + let js = r#" + let x = { + inner: false, + hello: "World", + }; + + x; + "#; + + let mut context = Context::default(); + let res = context.eval(js).unwrap(); + + let str = TestStruct::try_from_js(&res, &mut context).unwrap(); + + println!("{str:?}"); +}