mirror of https://github.com/boa-dev/boa.git
Browse Source
* #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 happypull/4025/head
Nikita-str
1 month ago
committed by
GitHub
5 changed files with 496 additions and 2 deletions
@ -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(()) |
||||||
|
} |
||||||
|
} |
@ -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…
Reference in new issue