Browse Source

Add a new type Convert<> to convert values (#3786)

* Add TryFromJs for more types

* Add a new type Coerce<> to coerce values

This can be used to transform values in to coerced versions by
using the type system and TryFromJs.

* Fix build error

* Rename Coerce<> to Convert<>

* replace missed coerce to Convert

* cargo fmt and clippy
pull/3812/head
Hans Larsen 9 months ago committed by GitHub
parent
commit
0d7018f16c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 130
      core/engine/src/value/conversions/convert.rs
  2. 2
      core/engine/src/value/conversions/mod.rs
  3. 24
      core/engine/src/value/conversions/try_from_js.rs
  4. 50
      core/engine/src/value/mod.rs

130
core/engine/src/value/conversions/convert.rs

@ -0,0 +1,130 @@
//! Types and functions for applying JavaScript Convert rules to [`JsValue`] when
//! converting. See <https://262.ecma-international.org/5.1/#sec-9> (Section 9) for
//! conversion rules of JavaScript types.
//!
//! Some conversions are not specified in the spec (e.g. integer conversions),
//! and we apply rules that make sense (e.g. converting to Number and rounding
//! if necessary).
use boa_engine::JsNativeError;
use crate::value::TryFromJs;
use crate::{Context, JsResult, JsString, JsValue};
/// A wrapper type that allows converting a `JsValue` to a specific type.
/// This is useful when you want to convert a `JsValue` to a Rust type.
///
/// # Example
/// Convert a string to number.
/// ```
/// # use boa_engine::{Context, js_string, JsValue};
/// # use boa_engine::value::{Convert, TryFromJs};
/// # let mut context = Context::default();
/// let value = JsValue::from(js_string!("42"));
/// let Convert(converted): Convert<i32> = Convert::try_from_js(&value, &mut context).unwrap();
///
/// assert_eq!(converted, 42);
/// ```
///
/// Convert a number to a bool.
/// ```
/// # use boa_engine::{Context, js_string, JsValue};
/// # use boa_engine::value::{Convert, TryFromJs};
/// # let mut context = Context::default();
/// let Convert(conv0): Convert<bool> = Convert::try_from_js(&JsValue::Integer(0), &mut context).unwrap();
/// let Convert(conv5): Convert<bool> = Convert::try_from_js(&JsValue::Integer(5), &mut context).unwrap();
/// let Convert(conv_nan): Convert<bool> = Convert::try_from_js(&JsValue::Rational(f64::NAN), &mut context).unwrap();
///
/// assert_eq!(conv0, false);
/// assert_eq!(conv5, true);
/// assert_eq!(conv_nan, false);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Convert<T: TryFromJs>(pub T);
impl<T: TryFromJs> From<T> for Convert<T> {
fn from(value: T) -> Self {
Self(value)
}
}
macro_rules! decl_convert_to_int {
($($ty:ty),*) => {
$(
impl TryFromJs for Convert<$ty> {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
value.to_numeric_number(context).and_then(|num| {
if num.is_finite() {
if num >= f64::from(<$ty>::MAX) {
Err(JsNativeError::typ()
.with_message("cannot convert value to integer, it is too large")
.into())
} else if num <= f64::from(<$ty>::MIN) {
Err(JsNativeError::typ()
.with_message("cannot convert value to integer, it is too small")
.into())
// Only round if it differs from the next integer by an epsilon
} else if num.abs().fract() >= (1.0 - f64::EPSILON) {
Ok(Convert(num.round() as $ty))
} else {
Ok(Convert(num as $ty))
}
} else if num.is_nan() {
Err(JsNativeError::typ()
.with_message("cannot convert NaN to integer")
.into())
} else if num.is_infinite() {
Err(JsNativeError::typ()
.with_message("cannot convert Infinity to integer")
.into())
} else {
Err(JsNativeError::typ()
.with_message("cannot convert non-finite number to integer")
.into())
}
})
}
}
)*
};
}
decl_convert_to_int!(i8, i16, i32, u8, u16, u32);
macro_rules! decl_convert_to_float {
($($ty:ty),*) => {
$(
impl TryFromJs for Convert<$ty> {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
value.to_numeric_number(context).and_then(|num| Ok(Convert(<$ty>::try_from(num).map_err(|_| {
JsNativeError::typ()
.with_message("cannot convert value to float")
})?)))
}
}
)*
};
}
decl_convert_to_float!(f64);
impl TryFromJs for Convert<String> {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
value
.to_string(context)
.and_then(|s| s.to_std_string().map_err(|_| JsNativeError::typ().into()))
.map(Convert)
}
}
impl TryFromJs for Convert<JsString> {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
value.to_string(context).map(Convert)
}
}
impl TryFromJs for Convert<bool> {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
Ok(Self(value.to_boolean()))
}
}

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

@ -7,6 +7,8 @@ use super::{JsBigInt, JsObject, JsString, JsSymbol, JsValue, Profiler};
mod serde_json;
pub(super) mod try_from_js;
pub(super) mod convert;
impl From<JsString> for JsValue {
fn from(value: JsString) -> Self {
let _timer = Profiler::global().start_event("From<JsString>", "value");

24
core/engine/src/value/conversions/try_from_js.rs

@ -2,7 +2,7 @@
use num_bigint::BigInt;
use crate::{js_string, Context, JsBigInt, JsNativeError, JsResult, JsValue};
use crate::{js_string, Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsValue};
/// This trait adds a fallible and efficient conversions from a [`JsValue`] to Rust types.
pub trait TryFromJs: Sized {
@ -47,6 +47,17 @@ impl TryFromJs for String {
}
}
impl TryFromJs for JsString {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
match value {
JsValue::String(s) => Ok(s.clone()),
_ => Err(JsNativeError::typ()
.with_message("cannot convert value to a String")
.into()),
}
}
}
impl<T> TryFromJs for Option<T>
where
T: TryFromJs,
@ -91,6 +102,17 @@ where
}
}
impl TryFromJs for JsObject {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
match value {
JsValue::Object(o) => Ok(o.clone()),
_ => Err(JsNativeError::typ()
.with_message("cannot convert value to a Object")
.into()),
}
}
}
impl TryFromJs for JsBigInt {
fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
match value {

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

@ -2,16 +2,23 @@
//!
//! Javascript values, utility methods and conversion between Javascript values and Rust values.
mod conversions;
pub(crate) mod display;
mod equality;
mod hash;
mod integer;
mod operations;
mod r#type;
use std::{
collections::HashSet,
fmt::{self, Display},
ops::Sub,
};
#[cfg(test)]
mod tests;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{ToPrimitive, Zero};
use once_cell::sync::Lazy;
use boa_gc::{custom_trace, Finalize, Trace};
#[doc(inline)]
pub use boa_macros::TryFromJs;
use boa_profiler::Profiler;
#[doc(inline)]
pub use conversions::convert::Convert;
use crate::{
builtins::{
@ -25,27 +32,24 @@ use crate::{
symbol::JsSymbol,
Context, JsBigInt, JsResult, JsString,
};
use boa_gc::{custom_trace, Finalize, Trace};
use boa_profiler::Profiler;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{ToPrimitive, Zero};
use once_cell::sync::Lazy;
use std::{
collections::HashSet,
fmt::{self, Display},
ops::Sub,
};
pub(crate) use self::conversions::IntoOrUndefined;
#[doc(inline)]
pub use self::{
conversions::try_from_js::TryFromJs, display::ValueDisplay, integer::IntegerOrInfinity,
operations::*, r#type::Type,
};
#[doc(inline)]
pub use boa_macros::TryFromJs;
pub(crate) use self::conversions::IntoOrUndefined;
mod conversions;
pub(crate) mod display;
mod equality;
mod hash;
mod integer;
mod operations;
mod r#type;
#[cfg(test)]
mod tests;
static TWO_E_64: Lazy<BigInt> = Lazy::new(|| {
const TWO_E_64: u128 = 2u128.pow(64);

Loading…
Cancel
Save