Browse Source

`TryIntoJs` trait and derive macro for it (#3999)

* #3874: `TryIntoJs` impl for primitive types

* #3874: `#[derive(TryIntoJs)]`

is it ok to use `create_data_property_or_throw`?
in other words, am I create an object correctly?

* #3874: some (but not enough) tests

* #3874: fix `TryintoJs` derive bug in multi attr case

* #3874: `TryIntoJs` derive macro example

* fix paths in derive macro

* make lint happy
pull/4025/head
Nikita-str 1 month ago committed by GitHub
parent
commit
905e4c6f90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      core/engine/src/value/conversions/mod.rs
  2. 289
      core/engine/src/value/conversions/try_into_js.rs
  3. 5
      core/engine/src/value/mod.rs
  4. 107
      core/macros/src/lib.rs
  5. 96
      examples/src/bin/try_into_js_derive.rs

1
core/engine/src/value/conversions/mod.rs

@ -7,6 +7,7 @@ use super::{JsBigInt, JsObject, JsString, JsSymbol, JsValue, Profiler};
mod either; mod either;
mod serde_json; mod serde_json;
pub(super) mod try_from_js; pub(super) mod try_from_js;
pub(super) mod try_into_js;
pub(super) mod convert; pub(super) mod convert;

289
core/engine/src/value/conversions/try_into_js.rs

@ -0,0 +1,289 @@
use crate::{Context, JsNativeError, JsResult, JsValue};
use boa_string::JsString;
/// This trait adds a conversions from a Rust Type into [`JsValue`].
pub trait TryIntoJs: Sized {
/// This function tries to convert a `Self` into [`JsValue`].
fn try_into_js(&self, context: &mut Context) -> JsResult<JsValue>;
}
impl TryIntoJs for bool {
fn try_into_js(&self, _context: &mut Context) -> JsResult<JsValue> {
JsResult::Ok(JsValue::Boolean(*self))
}
}
impl TryIntoJs for &str {
fn try_into_js(&self, _context: &mut Context) -> JsResult<JsValue> {
JsResult::Ok(JsValue::String(JsString::from(*self)))
}
}
impl TryIntoJs for String {
fn try_into_js(&self, _context: &mut Context) -> JsResult<JsValue> {
JsResult::Ok(JsValue::String(JsString::from(self.as_str())))
}
}
macro_rules! impl_try_into_js_by_from {
($t:ty) => {
impl TryIntoJs for $t {
fn try_into_js(&self, _context: &mut Context) -> JsResult<JsValue> {
JsResult::Ok(JsValue::from(self.clone()))
}
}
};
[$($ts:ty),+] => {
$(impl_try_into_js_by_from!($ts);)+
}
}
impl_try_into_js_by_from![i8, u8, i16, u16, i32, u32, f32, f64];
impl_try_into_js_by_from![
JsValue,
JsString,
crate::JsBigInt,
crate::JsObject,
crate::JsSymbol,
crate::object::JsArray,
crate::object::JsArrayBuffer,
crate::object::JsDataView,
crate::object::JsDate,
crate::object::JsFunction,
crate::object::JsGenerator,
crate::object::JsMapIterator,
crate::object::JsMap,
crate::object::JsSetIterator,
crate::object::JsSet,
crate::object::JsSharedArrayBuffer,
crate::object::JsInt8Array,
crate::object::JsInt16Array,
crate::object::JsInt32Array,
crate::object::JsUint8Array,
crate::object::JsUint16Array,
crate::object::JsUint32Array,
crate::object::JsFloat32Array,
crate::object::JsFloat64Array
];
const MAX_SAFE_INTEGER_I64: i64 = (1 << 53) - 1;
const MIN_SAFE_INTEGER_I64: i64 = -MAX_SAFE_INTEGER_I64;
fn err_outside_safe_range() -> crate::JsError {
JsNativeError::typ()
.with_message("cannot convert value into JsValue: the value is outside the safe range")
.into()
}
fn convert_safe_i64(value: i64) -> JsValue {
i32::try_from(value).map_or(JsValue::Rational(value as f64), JsValue::Integer)
}
impl TryIntoJs for i64 {
fn try_into_js(&self, _context: &mut Context) -> JsResult<JsValue> {
let value = *self;
#[allow(clippy::manual_range_contains)]
if value < MIN_SAFE_INTEGER_I64 || MAX_SAFE_INTEGER_I64 < value {
JsResult::Err(err_outside_safe_range())
} else {
JsResult::Ok(convert_safe_i64(value))
}
}
}
impl TryIntoJs for u64 {
fn try_into_js(&self, _context: &mut Context) -> JsResult<JsValue> {
let value = *self;
if (MAX_SAFE_INTEGER_I64 as u64) < value {
JsResult::Err(err_outside_safe_range())
} else {
JsResult::Ok(convert_safe_i64(value as i64))
}
}
}
impl TryIntoJs for i128 {
fn try_into_js(&self, _context: &mut Context) -> JsResult<JsValue> {
let value = *self;
if value < i128::from(MIN_SAFE_INTEGER_I64) || i128::from(MAX_SAFE_INTEGER_I64) < value {
JsResult::Err(err_outside_safe_range())
} else {
JsResult::Ok(convert_safe_i64(value as i64))
}
}
}
impl TryIntoJs for u128 {
fn try_into_js(&self, _context: &mut Context) -> JsResult<JsValue> {
let value = *self;
if (MAX_SAFE_INTEGER_I64 as u128) < value {
JsResult::Err(err_outside_safe_range())
} else {
JsResult::Ok(convert_safe_i64(value as i64))
}
}
}
impl<T> TryIntoJs for Option<T>
where
T: TryIntoJs,
{
fn try_into_js(&self, context: &mut Context) -> JsResult<JsValue> {
match self {
Some(x) => x.try_into_js(context),
None => JsResult::Ok(JsValue::Null),
}
}
}
impl<T> TryIntoJs for Vec<T>
where
T: TryIntoJs,
{
fn try_into_js(&self, context: &mut Context) -> JsResult<JsValue> {
let arr = crate::object::JsArray::new(context);
for value in self {
let value = value.try_into_js(context)?;
arr.push(value, context)?;
}
JsResult::Ok(arr.into())
}
}
macro_rules! impl_try_into_js_for_tuples {
($($names:ident : $ts:ident),+) => {
impl<$($ts: TryIntoJs,)+> TryIntoJs for ($($ts,)+) {
fn try_into_js(&self, context: &mut Context) -> JsResult<JsValue> {
let ($($names,)+) = self;
let arr = crate::object::JsArray::new(context);
$(arr.push($names.try_into_js(context)?, context)?;)+
JsResult::Ok(arr.into())
}
}
};
}
impl_try_into_js_for_tuples!(a: A);
impl_try_into_js_for_tuples!(a: A, b: B);
impl_try_into_js_for_tuples!(a: A, b: B, c: C);
impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D);
impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E);
impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F);
impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F, g: G);
impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H);
impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I);
impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J);
impl_try_into_js_for_tuples!(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K);
impl TryIntoJs for () {
fn try_into_js(&self, _context: &mut Context) -> JsResult<JsValue> {
JsResult::Ok(JsValue::Null)
}
}
impl<T, S> TryIntoJs for std::collections::HashSet<T, S>
where
T: TryIntoJs,
{
fn try_into_js(&self, context: &mut Context) -> JsResult<JsValue> {
let set = crate::object::JsSet::new(context);
for value in self {
let value = value.try_into_js(context)?;
set.add(value, context)?;
}
JsResult::Ok(set.into())
}
}
impl<K, V, S> TryIntoJs for std::collections::HashMap<K, V, S>
where
K: TryIntoJs,
V: TryIntoJs,
{
fn try_into_js(&self, context: &mut Context) -> JsResult<JsValue> {
let map = crate::object::JsMap::new(context);
for (key, value) in self {
let key = key.try_into_js(context)?;
let value = value.try_into_js(context)?;
map.set(key, value, context)?;
}
JsResult::Ok(map.into())
}
}
#[cfg(test)]
mod try_into_js_tests {
use crate::value::{TryFromJs, TryIntoJs};
use crate::{Context, JsResult};
#[test]
fn big_int_err() {
fn assert<T: TryIntoJs>(int: &T, context: &mut Context) {
let expect_err = int.try_into_js(context);
assert!(expect_err.is_err());
}
let mut context = Context::default();
let context = &mut context;
let int = (1 << 55) + 17i64;
assert(&int, context);
let int = (1 << 55) + 17u64;
assert(&int, context);
let int = (1 << 55) + 17u128;
assert(&int, context);
let int = (1 << 55) + 17i128;
assert(&int, context);
}
#[test]
fn int_tuple() -> JsResult<()> {
let mut context = Context::default();
let context = &mut context;
let tuple_initial = (
-42i8,
42u8,
1764i16,
7641u16,
-((1 << 27) + 13),
(1 << 27) + 72u32,
(1 << 49) + 1793i64,
(1 << 49) + 1793u64,
-((1 << 49) + 7193i128),
(1 << 49) + 9173u128,
);
// it will rewrite without reading, so it's just for auto type resolving.
#[allow(unused_assignments)]
let mut tuple_after_transform = tuple_initial;
let js_value = tuple_initial.try_into_js(context)?;
tuple_after_transform = TryFromJs::try_from_js(&js_value, context)?;
assert_eq!(tuple_initial, tuple_after_transform);
Ok(())
}
#[test]
fn string() -> JsResult<()> {
let mut context = Context::default();
let context = &mut context;
let s_init = "String".to_string();
let js_value = s_init.try_into_js(context)?;
let s: String = TryFromJs::try_from_js(&js_value, context)?;
assert_eq!(s_init, s);
Ok(())
}
#[test]
fn vec() -> JsResult<()> {
let mut context = Context::default();
let context = &mut context;
let vec_init = vec![(-4i64, 2u64), (15, 15), (32, 23)];
let js_value = vec_init.try_into_js(context)?;
println!("JsValue: {}", js_value.display());
let vec: Vec<(i64, u64)> = TryFromJs::try_from_js(&js_value, context)?;
assert_eq!(vec_init, vec);
Ok(())
}
}

5
core/engine/src/value/mod.rs

@ -17,6 +17,7 @@ use once_cell::sync::Lazy;
use boa_gc::{custom_trace, Finalize, Trace}; use boa_gc::{custom_trace, Finalize, Trace};
#[doc(inline)] #[doc(inline)]
pub use boa_macros::TryFromJs; pub use boa_macros::TryFromJs;
pub use boa_macros::TryIntoJs;
use boa_profiler::Profiler; use boa_profiler::Profiler;
#[doc(inline)] #[doc(inline)]
pub use conversions::convert::Convert; pub use conversions::convert::Convert;
@ -24,8 +25,8 @@ pub use conversions::convert::Convert;
pub(crate) use self::conversions::IntoOrUndefined; pub(crate) use self::conversions::IntoOrUndefined;
#[doc(inline)] #[doc(inline)]
pub use self::{ pub use self::{
conversions::try_from_js::TryFromJs, display::ValueDisplay, integer::IntegerOrInfinity, conversions::try_from_js::TryFromJs, conversions::try_into_js::TryIntoJs,
operations::*, r#type::Type, display::ValueDisplay, integer::IntegerOrInfinity, operations::*, r#type::Type,
}; };
use crate::builtins::RegExp; use crate::builtins::RegExp;
use crate::object::{JsFunction, JsPromise, JsRegExp}; use crate::object::{JsFunction, JsPromise, JsRegExp};

107
core/macros/src/lib.rs

@ -497,3 +497,110 @@ fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
let compile_errors = errors.iter().map(syn::Error::to_compile_error); let compile_errors = errors.iter().map(syn::Error::to_compile_error);
quote!(#(#compile_errors)*) quote!(#(#compile_errors)*)
} }
/// Derives the `TryIntoJs` trait, with the `#[boa()]` attribute.
///
/// # Panics
///
/// It will panic if the user tries to derive the `TryIntoJs` trait in an `enum` or a tuple struct.
#[proc_macro_derive(TryIntoJs, attributes(boa))]
pub fn derive_try_into_js(input: TokenStream) -> TokenStream {
// Parse the input tokens into a syntax tree
let input = parse_macro_input!(input as DeriveInput);
let Data::Struct(data) = input.data else {
panic!("you can only derive TryFromJs for structs");
};
// TODO: Enums ?
let Fields::Named(fields) = data.fields else {
panic!("you can only derive TryFromJs for named-field structs")
};
let props = generate_obj_properties(fields)
.map_err(|err| vec![err])
.unwrap_or_else(to_compile_errors);
let type_name = input.ident;
// Build the output, possibly using quasi-quotation
let expanded = quote! {
impl ::boa_engine::value::TryIntoJs for #type_name {
fn try_into_js(&self, context: &mut boa_engine::Context) -> boa_engine::JsResult<boa_engine::JsValue> {
let obj = boa_engine::JsObject::default();
#props
boa_engine::JsResult::Ok(obj.into())
}
}
};
// Hand the output tokens back to the compiler
expanded.into()
}
/// Generates property creation for object.
fn generate_obj_properties(fields: FieldsNamed) -> Result<proc_macro2::TokenStream, syn::Error> {
use syn::spanned::Spanned;
let mut prop_ctors = Vec::with_capacity(fields.named.len());
for field in fields.named {
let span = field.span();
let name = field.ident.ok_or_else(|| {
syn::Error::new(
span,
"you can only derive `TryIntoJs` for named-field structs",
)
})?;
let mut into_js_with = None;
let mut prop_key = format!("{name}");
let mut skip = false;
for attr in field
.attrs
.into_iter()
.filter(|attr| attr.path().is_ident("boa"))
{
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("into_js_with") {
let value = meta.value()?;
into_js_with = Some(value.parse::<LitStr>()?);
Ok(())
} else if meta.path.is_ident("rename") {
let value = meta.value()?;
prop_key = value.parse::<LitStr>()?.value();
Ok(())
} else if meta.path.is_ident("skip") & meta.input.is_empty() {
skip = true;
Ok(())
} else {
Err(meta.error(
"invalid syntax in the `#[boa()]` attribute. \
Note that this attribute only accepts the following syntax: \
\n* `#[boa(into_js_with = \"fully::qualified::path\")]`\
\n* `#[boa(rename = \"jsPropertyName\")]` \
\n* `#[boa(skip)]` \
",
))
}
})?;
}
if skip {
continue;
}
let value = if let Some(into_js_with) = into_js_with {
let into_js_with = Ident::new(&into_js_with.value(), into_js_with.span());
quote! { #into_js_with(&self.#name, context)? }
} else {
quote! { boa_engine::value::TryIntoJs::try_into_js(&self.#name, context)? }
};
prop_ctors.push(quote! {
obj.create_data_property_or_throw(boa_engine::js_str!(#prop_key), #value, context)?;
});
}
Ok(quote! { #(#prop_ctors)* })
}

96
examples/src/bin/try_into_js_derive.rs

@ -0,0 +1,96 @@
use boa_engine::{
js_string,
value::{TryFromJs, TryIntoJs},
Context, JsResult, JsValue, Source,
};
#[derive(TryIntoJs)]
struct Test {
x: i32,
#[boa(rename = "y")]
y_point: i32,
#[allow(unused)]
#[boa(skip)]
tuple: (i32, u8, String),
#[boa(rename = "isReadable")]
#[boa(into_js_with = "readable_into_js")]
is_readable: i8,
}
#[derive(TryFromJs, Debug, PartialEq, Eq)]
struct ResultVerifier {
x: i32,
y: i32,
#[boa(rename = "isReadable")]
is_readable: bool,
}
fn main() -> JsResult<()> {
let js_code = r#"
function pointShift(pointA, pointB) {
if (pointA.isReadable === true && pointB.isReadable === true) {
return {
x: pointA.x + pointB.x,
y: pointA.y + pointB.y,
isReadable: true,
}
}
return undefined
}
"#;
let mut context = Context::default();
let context = &mut context;
context.eval(Source::from_bytes(js_code))?;
let point_shift = context
.global_object()
.get(js_string!("pointShift"), context)?;
let point_shift = point_shift.as_callable().unwrap();
let a = Test {
x: 10,
y_point: 20,
tuple: (30, 40, "no matter".into()),
is_readable: 1,
};
let b = Test {
x: 2,
y_point: 1,
tuple: (30, 40, "no matter".into()),
is_readable: 2,
};
let c = Test {
x: 2,
y_point: 1,
tuple: (30, 40, "no matter".into()),
is_readable: 0,
};
let result = point_shift.call(
&JsValue::Undefined,
&[a.try_into_js(context)?, b.try_into_js(context)?],
context,
)?;
let verifier = ResultVerifier::try_from_js(&result, context)?;
let expect = ResultVerifier {
x: 10 + 2,
y: 20 + 1,
is_readable: true,
};
assert_eq!(verifier, expect);
let result = point_shift.call(
&JsValue::Undefined,
&[a.try_into_js(context)?, c.try_into_js(context)?],
context,
)?;
assert!(result.is_undefined());
Ok(())
}
fn readable_into_js(value: &i8, _context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::Boolean(*value != 0))
}
Loading…
Cancel
Save