mirror of https://github.com/boa-dev/boa.git
Browse Source
This Pull Request closes #1975. It's still a work in progress, but tries to go in that direction. It changes the following: - Adds a new `TryFromJs` trait, that can be derived using a new `boa_derive` crate. - Adds a new `try_js_into()` function that, similarly to the standard library `TryInto` trait Things to think about: - Should the `boa_derive` crate be re-exported in `boa_engine` using a `derive` feature, similar to how it's done in `serde`? - The current implementation only converts perfectly valid values. So, if we try to convert a big integer into an `i8`, or any floating point number to an `f32`. So, you cannot derive `TryFromJs` for structures that contain an `f32` for example (you can still manually implement the trait, though, and decide in favour of a loss of precision). Should we also provide some traits for transparent loss of precision? - Currently, you cannot convert between types, so if the JS struct has an integer, you cannot cast it to a boolean, for example. Should we provide a `TryConvertJs` trait, for example to force conversions? - Currently we only have basic types and object conversions. Should add `Array` to `Vec` conversion, for example, right? Should we also add `TypedArray` conversions? What about `Map` and `Set`? Does this step over the fine grained APIs that we were creating? Note that this still requires a bunch of documentation, tests, and validation from the dev team and from the users that requested this feature. I'm particularly interested in @lastmjs's thoughts on this API. I already added an usage example in `boa_examples/src/bin/derive.rs`. Co-authored-by: jedel1043 <jedel0124@gmail.com>pull/2769/head
Iban Eguia Moraza
2 years ago
17 changed files with 714 additions and 102 deletions
@ -0,0 +1,237 @@ |
|||||||
|
//! This module contains the [`TryFromJs`] trait, and conversions to basic Rust types.
|
||||||
|
|
||||||
|
use crate::{Context, JsBigInt, JsNativeError, JsResult, JsValue}; |
||||||
|
use num_bigint::BigInt; |
||||||
|
|
||||||
|
/// This trait adds a fallible and efficient conversions from a [`JsValue`] to Rust types.
|
||||||
|
pub trait TryFromJs: Sized { |
||||||
|
/// This function tries to convert a JavaScript value into `Self`.
|
||||||
|
fn try_from_js(value: &JsValue, context: &mut Context<'_>) -> JsResult<Self>; |
||||||
|
} |
||||||
|
|
||||||
|
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<T>(&self, context: &mut Context<'_>) -> JsResult<T> |
||||||
|
where |
||||||
|
T: TryFromJs, |
||||||
|
{ |
||||||
|
T::try_from_js(self, context) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for bool { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Boolean(b) => Ok(*b), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a boolean") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for String { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::String(s) => s.to_std_string().map_err(|e| { |
||||||
|
JsNativeError::typ() |
||||||
|
.with_message(format!("could not convert JsString to Rust string, since it has UTF-16 characters: {e}")) |
||||||
|
.into() |
||||||
|
}), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a String") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<T> TryFromJs for Option<T> |
||||||
|
where |
||||||
|
T: TryFromJs, |
||||||
|
{ |
||||||
|
fn try_from_js(value: &JsValue, context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
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<Self> { |
||||||
|
match value { |
||||||
|
JsValue::BigInt(b) => Ok(b.clone()), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a BigInt") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for BigInt { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::BigInt(b) => Ok(b.as_inner().clone()), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a BigInt") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for JsValue { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
Ok(value.clone()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for f64 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => Ok((*i).into()), |
||||||
|
JsValue::Rational(r) => Ok(*r), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a f64") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for i8 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => (*i).try_into().map_err(|e| { |
||||||
|
JsNativeError::typ() |
||||||
|
.with_message(format!("cannot convert value to a i8: {e}")) |
||||||
|
.into() |
||||||
|
}), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a i8") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for u8 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => (*i).try_into().map_err(|e| { |
||||||
|
JsNativeError::typ() |
||||||
|
.with_message(format!("cannot convert value to a u8: {e}")) |
||||||
|
.into() |
||||||
|
}), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a u8") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for i16 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => (*i).try_into().map_err(|e| { |
||||||
|
JsNativeError::typ() |
||||||
|
.with_message(format!("cannot convert value to a i16: {e}")) |
||||||
|
.into() |
||||||
|
}), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a i16") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for u16 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => (*i).try_into().map_err(|e| { |
||||||
|
JsNativeError::typ() |
||||||
|
.with_message(format!("cannot convert value to a iu16: {e}")) |
||||||
|
.into() |
||||||
|
}), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a u16") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for i32 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => Ok(*i), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a i32") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for u32 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => (*i).try_into().map_err(|e| { |
||||||
|
JsNativeError::typ() |
||||||
|
.with_message(format!("cannot convert value to a u32: {e}")) |
||||||
|
.into() |
||||||
|
}), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a u32") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for i64 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => Ok((*i).into()), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a i64") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for u64 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => (*i).try_into().map_err(|e| { |
||||||
|
JsNativeError::typ() |
||||||
|
.with_message(format!("cannot convert value to a u64: {e}")) |
||||||
|
.into() |
||||||
|
}), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a u64") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for i128 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => Ok((*i).into()), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a i128") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TryFromJs for u128 { |
||||||
|
fn try_from_js(value: &JsValue, _context: &mut Context<'_>) -> JsResult<Self> { |
||||||
|
match value { |
||||||
|
JsValue::Integer(i) => (*i).try_into().map_err(|e| { |
||||||
|
JsNativeError::typ() |
||||||
|
.with_message(format!("cannot convert value to a u128: {e}")) |
||||||
|
.into() |
||||||
|
}), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to a u128") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
use boa_engine::{value::TryFromJs, Context, JsNativeError, JsResult, JsValue, Source}; |
||||||
|
|
||||||
|
/// You can easily derive `TryFromJs` for structures with base Rust types.
|
||||||
|
///
|
||||||
|
/// By default, the conversion will only work if the type is directly representable by the Rust
|
||||||
|
/// type.
|
||||||
|
#[derive(Debug, TryFromJs)] |
||||||
|
#[allow(dead_code)] |
||||||
|
struct TestStruct { |
||||||
|
inner: bool, |
||||||
|
hello: String, |
||||||
|
// You can override the conversion of an attribute.
|
||||||
|
#[boa(from_js_with = "lossy_conversion")] |
||||||
|
my_float: i16, |
||||||
|
} |
||||||
|
|
||||||
|
fn main() { |
||||||
|
let js_str = r#" |
||||||
|
let x = { |
||||||
|
inner: false, |
||||||
|
hello: "World", |
||||||
|
my_float: 2.9, |
||||||
|
}; |
||||||
|
|
||||||
|
x; |
||||||
|
"#; |
||||||
|
let js = Source::from_bytes(js_str); |
||||||
|
|
||||||
|
let mut context = Context::default(); |
||||||
|
let res = context.eval_script(js).unwrap(); |
||||||
|
|
||||||
|
let str = TestStruct::try_from_js(&res, &mut context) |
||||||
|
.map_err(|e| e.to_string()) |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
println!("{str:?}"); |
||||||
|
} |
||||||
|
|
||||||
|
/// Converts the value lossly
|
||||||
|
fn lossy_conversion(value: &JsValue, _context: &mut Context) -> JsResult<i16> { |
||||||
|
match value { |
||||||
|
JsValue::Rational(r) => Ok(r.round() as i16), |
||||||
|
JsValue::Integer(i) => Ok(*i as i16), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to an i16") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
[package] |
||||||
|
name = "boa_macros_tests" |
||||||
|
description = "Testing crate for boa_macros" |
||||||
|
keywords = ["javascript", "ECMASCript", "compiler", "tester"] |
||||||
|
publish = false |
||||||
|
version.workspace = true |
||||||
|
edition.workspace = true |
||||||
|
authors.workspace = true |
||||||
|
license.workspace = true |
||||||
|
repository.workspace = true |
||||||
|
rust-version.workspace = true |
||||||
|
|
||||||
|
[dev-dependencies] |
||||||
|
trybuild = "1.0.80" |
||||||
|
boa_macros.workspace = true |
||||||
|
boa_engine.workspace = true |
@ -0,0 +1,20 @@ |
|||||||
|
use boa_engine::{value::TryFromJs, Context, JsNativeError, JsResult, JsValue}; |
||||||
|
|
||||||
|
#[derive(TryFromJs)] |
||||||
|
struct TestStruct { |
||||||
|
inner: bool, |
||||||
|
#[boa(from_js_with = "lossy_float")] |
||||||
|
my_int: i16, |
||||||
|
} |
||||||
|
|
||||||
|
fn main() {} |
||||||
|
|
||||||
|
fn lossy_float(value: &JsValue, _context: &mut Context) -> JsResult<i16> { |
||||||
|
match value { |
||||||
|
JsValue::Rational(r) => Ok(r.round() as i16), |
||||||
|
JsValue::Integer(i) => Ok(*i as i16), |
||||||
|
_ => Err(JsNativeError::typ() |
||||||
|
.with_message("cannot convert value to an i16") |
||||||
|
.into()), |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
use boa_engine::value::TryFromJs; |
||||||
|
|
||||||
|
#[derive(TryFromJs)] |
||||||
|
struct TestStruct { |
||||||
|
inner: bool, |
||||||
|
} |
||||||
|
|
||||||
|
fn main() {} |
Loading…
Reference in new issue