Skip to main content

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