Skip to main content

boa_engine/value/conversions/
convert.rs

1//! Types and functions for applying JavaScript Convert rules to [`JsValue`] when
2//! converting. See <https://262.ecma-international.org/5.1/#sec-9> (Section 9) for
3//! conversion rules of JavaScript types.
4//!
5//! Some conversions are not specified in the spec (e.g. integer conversions),
6//! and we apply rules that make sense (e.g. converting to Number and rounding
7//! if necessary).
8
9use boa_engine::JsNativeError;
10use boa_gc::{Finalize, Trace};
11
12use crate::value::TryFromJs;
13use crate::{Context, JsData, JsResult, JsString, JsValue};
14
15/// A wrapper type that allows converting a `JsValue` to a specific type.
16/// This is useful when you want to convert a `JsValue` to a Rust type.
17///
18/// # Example
19/// Convert a string to number.
20/// ```
21/// # use boa_engine::{Context, js_string, JsValue};
22/// # use boa_engine::value::{Convert, TryFromJs};
23/// # let mut context = Context::default();
24/// let value = JsValue::from(js_string!("42"));
25/// let Convert(converted): Convert<i32> =
26///     Convert::try_from_js(&value, &mut context).unwrap();
27///
28/// assert_eq!(converted, 42);
29/// ```
30///
31/// Convert a number to a bool.
32/// ```
33/// # use boa_engine::{Context, js_string, JsValue};
34/// # use boa_engine::value::{Convert, TryFromJs};
35/// # let mut context = Context::default();
36/// let Convert(conv0): Convert<bool> =
37///     Convert::try_from_js(&JsValue::new(0), &mut context).unwrap();
38/// let Convert(conv5): Convert<bool> =
39///     Convert::try_from_js(&JsValue::new(5), &mut context).unwrap();
40/// let Convert(conv_nan): Convert<bool> =
41///     Convert::try_from_js(&JsValue::new(f64::NAN), &mut context).unwrap();
42///
43/// assert_eq!(conv0, false);
44/// assert_eq!(conv5, true);
45/// assert_eq!(conv_nan, false);
46/// ```
47#[derive(Debug, Clone, PartialEq, Eq, Trace, Finalize, JsData)]
48pub struct Convert<T: TryFromJs>(pub T);
49
50impl<T: TryFromJs> From<T> for Convert<T> {
51    fn from(value: T) -> Self {
52        Self(value)
53    }
54}
55
56impl<T: TryFromJs> AsRef<T> for Convert<T> {
57    fn as_ref(&self) -> &T {
58        &self.0
59    }
60}
61
62macro_rules! decl_convert_to_int {
63    ($($ty:ty),*) => {
64        $(
65            impl TryFromJs for Convert<$ty> {
66                fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
67                    value.to_numeric_number(context).and_then(|num| {
68                        if num.is_finite() {
69                            if num >= f64::from(<$ty>::MAX) {
70                                Err(JsNativeError::typ()
71                                    .with_message("cannot convert value to integer, it is too large")
72                                    .into())
73                            } else if num <= f64::from(<$ty>::MIN) {
74                                Err(JsNativeError::typ()
75                                    .with_message("cannot convert value to integer, it is too small")
76                                    .into())
77                                // Only round if it differs from the next integer by an epsilon
78                            } else if num.abs().fract() >= (1.0 - f64::EPSILON) {
79                                Ok(Convert(num.round() as $ty))
80                            } else {
81                                Ok(Convert(num as $ty))
82                            }
83                        } else if num.is_nan() {
84                            Err(JsNativeError::typ()
85                                .with_message("cannot convert NaN to integer")
86                                .into())
87                        } else if num.is_infinite() {
88                            Err(JsNativeError::typ()
89                                .with_message("cannot convert Infinity to integer")
90                                .into())
91                        } else {
92                            Err(JsNativeError::typ()
93                                .with_message("cannot convert non-finite number to integer")
94                                .into())
95                        }
96                    })
97                }
98            }
99        )*
100    };
101}
102
103decl_convert_to_int!(i8, i16, i32, u8, u16, u32);
104
105macro_rules! decl_convert_to_float {
106    ($($ty:ty),*) => {
107        $(
108            impl TryFromJs for Convert<$ty> {
109                fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
110                    value.to_numeric_number(context).and_then(|num| Ok(Convert(<$ty>::try_from(num).map_err(|_| {
111                        JsNativeError::typ()
112                            .with_message("cannot convert value to float")
113                    })?)))
114                }
115            }
116        )*
117    };
118}
119
120decl_convert_to_float!(f64);
121
122impl TryFromJs for Convert<String> {
123    fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
124        value
125            .to_string(context)
126            .and_then(|s| s.to_std_string().map_err(|_| JsNativeError::typ().into()))
127            .map(Convert)
128    }
129}
130
131impl TryFromJs for Convert<JsString> {
132    fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
133        value.to_string(context).map(Convert)
134    }
135}
136
137impl TryFromJs for Convert<bool> {
138    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
139        Ok(Self(value.to_boolean()))
140    }
141}