boa_engine/value/conversions/
try_from_js.rs

1//! This module contains the [`TryFromJs`] trait, and conversions to basic Rust types.
2
3use crate::{Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsValue, js_error};
4use boa_string::StaticJsStrings;
5use num_bigint::BigInt;
6use num_traits::AsPrimitive;
7
8mod collections;
9mod tuples;
10
11/// This trait adds a fallible and efficient conversions from a [`JsValue`] to Rust types.
12pub trait TryFromJs: Sized {
13    /// This function tries to convert a JavaScript value into `Self`.
14    fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self>;
15}
16
17impl JsValue {
18    /// This function is the inverse of [`TryFromJs`]. It tries to convert a [`JsValue`] to a given
19    /// Rust type.
20    pub fn try_js_into<T>(&self, context: &mut Context) -> JsResult<T>
21    where
22        T: TryFromJs,
23    {
24        T::try_from_js(self, context)
25    }
26}
27
28impl TryFromJs for bool {
29    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
30        if let Some(b) = value.as_boolean() {
31            Ok(b)
32        } else {
33            Err(JsNativeError::typ()
34                .with_message("cannot convert value to a boolean")
35                .into())
36        }
37    }
38}
39
40impl TryFromJs for () {
41    fn try_from_js(_value: &JsValue, _context: &mut Context) -> JsResult<Self> {
42        Ok(())
43    }
44}
45
46impl TryFromJs for String {
47    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
48        if let Some(s) = value.as_string() {
49            s.to_std_string().map_err(|e| {
50                JsNativeError::typ()
51                    .with_message(format!("could not convert JsString to Rust string: {e}"))
52                    .into()
53            })
54        } else {
55            Err(JsNativeError::typ()
56                .with_message("cannot convert value to a String")
57                .into())
58        }
59    }
60}
61
62impl TryFromJs for JsString {
63    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
64        if let Some(s) = value.as_string() {
65            Ok(s.clone())
66        } else {
67            Err(JsNativeError::typ()
68                .with_message("cannot convert value to a JsString")
69                .into())
70        }
71    }
72}
73
74impl<T> TryFromJs for Option<T>
75where
76    T: TryFromJs,
77{
78    fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
79        if value.is_undefined() {
80            Ok(None)
81        } else {
82            Ok(Some(T::try_from_js(value, context)?))
83        }
84    }
85}
86
87impl<T> TryFromJs for Vec<T>
88where
89    T: TryFromJs,
90{
91    fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
92        let Some(object) = &value.as_object() else {
93            return Err(JsNativeError::typ()
94                .with_message("cannot convert value to a Vec")
95                .into());
96        };
97
98        let length = object.get(StaticJsStrings::LENGTH, context)?;
99        // If there's no length, return an error.
100        if length.is_null_or_undefined() {
101            return Err(js_error!(TypeError: "Not an array"));
102        }
103        let length = length.to_length(context)?;
104
105        let length = match usize::try_from(length) {
106            Ok(length) => length,
107            Err(e) => {
108                return Err(JsNativeError::typ()
109                    .with_message(format!("could not convert length to usize: {e}"))
110                    .into());
111            }
112        };
113        let mut vec = Vec::with_capacity(length);
114        for i in 0..length {
115            let value = object.get(i, context)?;
116            vec.push(T::try_from_js(&value, context)?);
117        }
118
119        Ok(vec)
120    }
121}
122
123impl TryFromJs for JsObject {
124    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
125        if let Some(o) = value.as_object() {
126            Ok(o.clone())
127        } else {
128            Err(JsNativeError::typ()
129                .with_message("cannot convert value to a Object")
130                .into())
131        }
132    }
133}
134
135impl TryFromJs for JsBigInt {
136    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
137        if let Some(b) = value.as_bigint() {
138            Ok(b.clone())
139        } else {
140            Err(JsNativeError::typ()
141                .with_message("cannot convert value to a BigInt")
142                .into())
143        }
144    }
145}
146
147impl TryFromJs for BigInt {
148    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
149        if let Some(b) = value.as_bigint() {
150            Ok(b.as_inner().clone())
151        } else {
152            Err(JsNativeError::typ()
153                .with_message("cannot convert value to a BigInt")
154                .into())
155        }
156    }
157}
158
159impl TryFromJs for JsValue {
160    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
161        Ok(value.clone())
162    }
163}
164
165impl TryFromJs for f64 {
166    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
167        if let Some(i) = value.0.as_integer32() {
168            Ok(f64::from(i))
169        } else if let Some(f) = value.0.as_float64() {
170            Ok(f)
171        } else {
172            Err(JsNativeError::typ()
173                .with_message("cannot convert value to a f64")
174                .into())
175        }
176    }
177}
178
179fn from_f64<T>(v: f64) -> Option<T>
180where
181    T: AsPrimitive<f64>,
182    f64: AsPrimitive<T>,
183{
184    if <f64 as AsPrimitive<T>>::as_(v).as_().to_bits() == v.to_bits() {
185        return Some(v.as_());
186    }
187    None
188}
189
190macro_rules! impl_try_from_js_integer {
191    ( $( $type: ty ),* ) => {
192        $(
193            impl TryFromJs for $type {
194                fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
195                    if let Some(i) = value.as_i32() {
196                        i.try_into().map_err(|e| {
197                            JsNativeError::typ()
198                                .with_message(format!(
199                                    concat!("cannot convert value to a ", stringify!($type), ": {}"),
200                                    e)
201                                )
202                                .into()
203                        })
204                    } else if let Some(f) = value.as_number() {
205                        from_f64(f).ok_or_else(|| {
206                            JsNativeError::typ()
207                                .with_message(concat!("cannot convert value to a ", stringify!($type)))
208                                .into()
209                        })
210                    } else {
211                        Err(JsNativeError::typ()
212                            .with_message(concat!("cannot convert value to a ", stringify!($type)))
213                            .into())
214                    }
215                }
216            }
217        )*
218    }
219}
220
221impl_try_from_js_integer!(i8, u8, i16, u16, i32, u32, i64, u64, usize, i128, u128);
222
223#[test]
224fn integer_floating_js_value_to_integer() {
225    let context = &mut Context::default();
226
227    assert_eq!(i8::try_from_js(&JsValue::from(4.0), context), Ok(4));
228    assert_eq!(u8::try_from_js(&JsValue::from(4.0), context), Ok(4));
229    assert_eq!(i16::try_from_js(&JsValue::from(4.0), context), Ok(4));
230    assert_eq!(u16::try_from_js(&JsValue::from(4.0), context), Ok(4));
231    assert_eq!(i32::try_from_js(&JsValue::from(4.0), context), Ok(4));
232    assert_eq!(u32::try_from_js(&JsValue::from(4.0), context), Ok(4));
233    assert_eq!(i64::try_from_js(&JsValue::from(4.0), context), Ok(4));
234    assert_eq!(u64::try_from_js(&JsValue::from(4.0), context), Ok(4));
235
236    // Floating with fractional part
237    let result = i32::try_from_js(&JsValue::from(4.000_000_000_000_001), context);
238    assert!(result.is_err());
239
240    // NaN
241    let result = i32::try_from_js(&JsValue::nan(), context);
242    assert!(result.is_err());
243
244    // +Infinity
245    let result = i32::try_from_js(&JsValue::positive_infinity(), context);
246    assert!(result.is_err());
247
248    // -Infinity
249    let result = i32::try_from_js(&JsValue::negative_infinity(), context);
250    assert!(result.is_err());
251}
252
253#[test]
254fn value_into_vec() {
255    use boa_engine::{TestAction, run_test_actions};
256    use indoc::indoc;
257
258    #[derive(Debug, PartialEq, Eq, boa_macros::TryFromJs)]
259    struct TestStruct {
260        inner: bool,
261        my_int: i16,
262        my_vec: Vec<String>,
263    }
264
265    run_test_actions([
266        TestAction::assert_with_op(
267            indoc! {r#"
268            let value = {
269                inner: true,
270                my_int: 11,
271                my_vec: ["a", "b", "c"]
272            };
273            value
274        "#},
275            |value, context| {
276                let value = TestStruct::try_from_js(&value, context);
277
278                match value {
279                    Ok(value) => {
280                        value
281                            == TestStruct {
282                                inner: true,
283                                my_int: 11,
284                                my_vec: vec!["a".to_string(), "b".to_string(), "c".to_string()],
285                            }
286                    }
287                    _ => false,
288                }
289            },
290        ),
291        TestAction::assert_with_op(
292            indoc!(
293                r#"
294            let wrong = {
295                inner: false,
296                my_int: 22,
297                my_vec: [{}, "e", "f"]
298            };
299            wrong"#
300            ),
301            |value, context| {
302                let Err(value) = TestStruct::try_from_js(&value, context) else {
303                    return false;
304                };
305                assert!(value.to_string().contains("TypeError"));
306                true
307            },
308        ),
309    ]);
310}
311
312#[test]
313fn value_into_tuple() {
314    use boa_engine::{TestAction, run_test_actions};
315    use indoc::indoc;
316
317    run_test_actions([
318        TestAction::assert_with_op(indoc! {r#" [42, "hello", true] "#}, |value, context| {
319            type TestType = (i32, String, bool);
320            TestType::try_from_js(&value, context).unwrap() == (42, "hello".to_string(), true)
321        }),
322        TestAction::assert_with_op(indoc! {r#" [42, "hello", true] "#}, |value, context| {
323            type TestType = (i32, String, Option<bool>, Option<u8>);
324            TestType::try_from_js(&value, context).unwrap()
325                == (42, "hello".to_string(), Some(true), None)
326        }),
327        TestAction::assert_with_op(indoc! {r#" [] "#}, |value, context| {
328            type TestType = (
329                Option<bool>,
330                Option<bool>,
331                Option<bool>,
332                Option<bool>,
333                Option<bool>,
334                Option<bool>,
335                Option<bool>,
336                Option<bool>,
337                Option<bool>,
338                Option<bool>,
339            );
340            TestType::try_from_js(&value, context).unwrap()
341                == (None, None, None, None, None, None, None, None, None, None)
342        }),
343        TestAction::assert_with_op(indoc!(r#"[42, "hello", {}]"#), |value, context| {
344            type TestType = (i32, String, bool);
345            let Err(value) = TestType::try_from_js(&value, context) else {
346                return false;
347            };
348            assert!(value.to_string().contains("TypeError"));
349            true
350        }),
351        TestAction::assert_with_op(indoc!(r#"[42, "hello"]"#), |value, context| {
352            type TestType = (i32, String, bool);
353            let Err(value) = TestType::try_from_js(&value, context) else {
354                return false;
355            };
356            assert!(value.to_string().contains("TypeError"));
357            true
358        }),
359    ]);
360}
361
362#[test]
363fn value_into_map() {
364    use boa_engine::{TestAction, run_test_actions};
365    use indoc::indoc;
366
367    run_test_actions([
368        TestAction::assert_with_op(indoc! {r#" ({ a: 1, b: 2, c: 3 }) "#}, |value, context| {
369            let value = std::collections::BTreeMap::<String, i32>::try_from_js(&value, context);
370
371            match value {
372                Ok(value) => {
373                    value
374                        == vec![
375                            ("a".to_string(), 1),
376                            ("b".to_string(), 2),
377                            ("c".to_string(), 3),
378                        ]
379                        .into_iter()
380                        .collect::<std::collections::BTreeMap<String, i32>>()
381                }
382                _ => false,
383            }
384        }),
385        TestAction::assert_with_op(indoc! {r#" ({ a: 1, b: 2, c: 3 }) "#}, |value, context| {
386            let value = std::collections::HashMap::<String, i32>::try_from_js(&value, context);
387
388            match value {
389                Ok(value) => {
390                    value
391                        == std::collections::HashMap::from_iter(
392                            vec![
393                                ("a".to_string(), 1),
394                                ("b".to_string(), 2),
395                                ("c".to_string(), 3),
396                            ]
397                            .into_iter()
398                            .collect::<std::collections::BTreeMap<String, i32>>(),
399                        )
400                }
401                _ => false,
402            }
403        }),
404    ]);
405}
406
407#[test]
408fn js_map_into_rust_map() -> JsResult<()> {
409    use boa_engine::Source;
410    use std::collections::{BTreeMap, HashMap};
411
412    let js_code = "new Map([['a', 1], ['b', 3], ['aboba', 42024]])";
413    let mut context = Context::default();
414
415    let js_value = context.eval(Source::from_bytes(js_code))?;
416
417    let hash_map = HashMap::<String, i32>::try_from_js(&js_value, &mut context)?;
418    let btree_map = BTreeMap::<String, i32>::try_from_js(&js_value, &mut context)?;
419
420    let expect = [("a".into(), 1), ("aboba".into(), 42024), ("b".into(), 3)];
421
422    let expected_hash_map: HashMap<String, _> = expect.iter().cloned().collect();
423    assert_eq!(expected_hash_map, hash_map);
424
425    let expected_btree_map: BTreeMap<String, _> = expect.iter().cloned().collect();
426    assert_eq!(expected_btree_map, btree_map);
427    Ok(())
428}