boa_engine/value/conversions/
try_from_js.rs

1//! This module contains the [`TryFromJs`] trait, and conversions to basic Rust types.
2
3use num_bigint::BigInt;
4use num_traits::AsPrimitive;
5
6use crate::{js_string, Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsValue};
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        match value {
31            JsValue::Boolean(b) => Ok(*b),
32            _ => Err(JsNativeError::typ()
33                .with_message("cannot convert value to a boolean")
34                .into()),
35        }
36    }
37}
38
39impl TryFromJs for () {
40    fn try_from_js(_value: &JsValue, _context: &mut Context) -> JsResult<Self> {
41        Ok(())
42    }
43}
44
45impl TryFromJs for String {
46    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
47        match value {
48            JsValue::String(s) => s.to_std_string().map_err(|e| {
49                JsNativeError::typ()
50                    .with_message(format!("could not convert JsString to Rust string: {e}"))
51                    .into()
52            }),
53            _ => Err(JsNativeError::typ()
54                .with_message("cannot convert value to a String")
55                .into()),
56        }
57    }
58}
59
60impl TryFromJs for JsString {
61    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
62        match value {
63            JsValue::String(s) => Ok(s.clone()),
64            _ => Err(JsNativeError::typ()
65                .with_message("cannot convert value to a String")
66                .into()),
67        }
68    }
69}
70
71impl<T> TryFromJs for Option<T>
72where
73    T: TryFromJs,
74{
75    fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
76        match value {
77            JsValue::Null | JsValue::Undefined => Ok(None),
78            value => Ok(Some(T::try_from_js(value, context)?)),
79        }
80    }
81}
82
83impl<T> TryFromJs for Vec<T>
84where
85    T: TryFromJs,
86{
87    fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
88        let JsValue::Object(object) = value else {
89            return Err(JsNativeError::typ()
90                .with_message("cannot convert value to a Vec")
91                .into());
92        };
93
94        let length = object
95            .get(js_string!("length"), context)?
96            .to_length(context)?;
97        let length = match usize::try_from(length) {
98            Ok(length) => length,
99            Err(e) => {
100                return Err(JsNativeError::typ()
101                    .with_message(format!("could not convert length to usize: {e}"))
102                    .into());
103            }
104        };
105        let mut vec = Vec::with_capacity(length);
106        for i in 0..length {
107            let value = object.get(i, context)?;
108            vec.push(T::try_from_js(&value, context)?);
109        }
110
111        Ok(vec)
112    }
113}
114
115impl TryFromJs for JsObject {
116    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
117        match value {
118            JsValue::Object(o) => Ok(o.clone()),
119            _ => Err(JsNativeError::typ()
120                .with_message("cannot convert value to a Object")
121                .into()),
122        }
123    }
124}
125
126impl TryFromJs for JsBigInt {
127    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
128        match value {
129            JsValue::BigInt(b) => Ok(b.clone()),
130            _ => Err(JsNativeError::typ()
131                .with_message("cannot convert value to a BigInt")
132                .into()),
133        }
134    }
135}
136
137impl TryFromJs for BigInt {
138    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
139        match value {
140            JsValue::BigInt(b) => Ok(b.as_inner().clone()),
141            _ => Err(JsNativeError::typ()
142                .with_message("cannot convert value to a BigInt")
143                .into()),
144        }
145    }
146}
147
148impl TryFromJs for JsValue {
149    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
150        Ok(value.clone())
151    }
152}
153
154impl TryFromJs for f64 {
155    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
156        match value {
157            JsValue::Integer(i) => Ok((*i).into()),
158            JsValue::Rational(r) => Ok(*r),
159            _ => Err(JsNativeError::typ()
160                .with_message("cannot convert value to a f64")
161                .into()),
162        }
163    }
164}
165
166fn from_f64<T>(v: f64) -> Option<T>
167where
168    T: AsPrimitive<f64>,
169    f64: AsPrimitive<T>,
170{
171    if <f64 as AsPrimitive<T>>::as_(v).as_().to_bits() == v.to_bits() {
172        return Some(v.as_());
173    }
174    None
175}
176
177impl TryFromJs for i8 {
178    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
179        match value {
180            JsValue::Integer(i) => (*i).try_into().map_err(|e| {
181                JsNativeError::typ()
182                    .with_message(format!("cannot convert value to a i8: {e}"))
183                    .into()
184            }),
185            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
186                JsNativeError::typ()
187                    .with_message("cannot convert value to a i8")
188                    .into()
189            }),
190            _ => Err(JsNativeError::typ()
191                .with_message("cannot convert value to a i8")
192                .into()),
193        }
194    }
195}
196
197impl TryFromJs for u8 {
198    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
199        match value {
200            JsValue::Integer(i) => (*i).try_into().map_err(|e| {
201                JsNativeError::typ()
202                    .with_message(format!("cannot convert value to a u8: {e}"))
203                    .into()
204            }),
205            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
206                JsNativeError::typ()
207                    .with_message("cannot convert value to a u8")
208                    .into()
209            }),
210            _ => Err(JsNativeError::typ()
211                .with_message("cannot convert value to a u8")
212                .into()),
213        }
214    }
215}
216
217impl TryFromJs for i16 {
218    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
219        match value {
220            JsValue::Integer(i) => (*i).try_into().map_err(|e| {
221                JsNativeError::typ()
222                    .with_message(format!("cannot convert value to a i16: {e}"))
223                    .into()
224            }),
225            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
226                JsNativeError::typ()
227                    .with_message("cannot convert value to a i16")
228                    .into()
229            }),
230            _ => Err(JsNativeError::typ()
231                .with_message("cannot convert value to a i16")
232                .into()),
233        }
234    }
235}
236
237impl TryFromJs for u16 {
238    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
239        match value {
240            JsValue::Integer(i) => (*i).try_into().map_err(|e| {
241                JsNativeError::typ()
242                    .with_message(format!("cannot convert value to a iu16: {e}"))
243                    .into()
244            }),
245            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
246                JsNativeError::typ()
247                    .with_message("cannot convert value to a u16")
248                    .into()
249            }),
250            _ => Err(JsNativeError::typ()
251                .with_message("cannot convert value to a u16")
252                .into()),
253        }
254    }
255}
256
257impl TryFromJs for i32 {
258    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
259        match value {
260            JsValue::Integer(i) => Ok(*i),
261            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
262                JsNativeError::typ()
263                    .with_message("cannot convert value to a i32")
264                    .into()
265            }),
266            _ => Err(JsNativeError::typ()
267                .with_message("cannot convert value to a i32")
268                .into()),
269        }
270    }
271}
272
273impl TryFromJs for u32 {
274    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
275        match value {
276            JsValue::Integer(i) => (*i).try_into().map_err(|e| {
277                JsNativeError::typ()
278                    .with_message(format!("cannot convert value to a u32: {e}"))
279                    .into()
280            }),
281            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
282                JsNativeError::typ()
283                    .with_message("cannot convert value to a u32")
284                    .into()
285            }),
286            _ => Err(JsNativeError::typ()
287                .with_message("cannot convert value to a u32")
288                .into()),
289        }
290    }
291}
292
293impl TryFromJs for i64 {
294    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
295        match value {
296            JsValue::Integer(i) => Ok((*i).into()),
297            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
298                JsNativeError::typ()
299                    .with_message("cannot convert value to a i64")
300                    .into()
301            }),
302            _ => Err(JsNativeError::typ()
303                .with_message("cannot convert value to a i64")
304                .into()),
305        }
306    }
307}
308
309impl TryFromJs for u64 {
310    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
311        match value {
312            JsValue::Integer(i) => (*i).try_into().map_err(|e| {
313                JsNativeError::typ()
314                    .with_message(format!("cannot convert value to a u64: {e}"))
315                    .into()
316            }),
317            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
318                JsNativeError::typ()
319                    .with_message("cannot convert value to a u64")
320                    .into()
321            }),
322            _ => Err(JsNativeError::typ()
323                .with_message("cannot convert value to a u64")
324                .into()),
325        }
326    }
327}
328
329impl TryFromJs for usize {
330    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
331        match value {
332            JsValue::Integer(i) => (*i).try_into().map_err(|e| {
333                JsNativeError::typ()
334                    .with_message(format!("cannot convert value to a usize: {e}"))
335                    .into()
336            }),
337            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
338                JsNativeError::typ()
339                    .with_message("cannot convert value to a usize")
340                    .into()
341            }),
342            _ => Err(JsNativeError::typ()
343                .with_message("cannot convert value to a usize")
344                .into()),
345        }
346    }
347}
348
349impl TryFromJs for i128 {
350    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
351        match value {
352            JsValue::Integer(i) => Ok((*i).into()),
353            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
354                JsNativeError::typ()
355                    .with_message("cannot convert value to a i128")
356                    .into()
357            }),
358            _ => Err(JsNativeError::typ()
359                .with_message("cannot convert value to a i128")
360                .into()),
361        }
362    }
363}
364
365impl TryFromJs for u128 {
366    fn try_from_js(value: &JsValue, _context: &mut Context) -> JsResult<Self> {
367        match value {
368            JsValue::Integer(i) => (*i).try_into().map_err(|e| {
369                JsNativeError::typ()
370                    .with_message(format!("cannot convert value to a u128: {e}"))
371                    .into()
372            }),
373            JsValue::Rational(f) => from_f64(*f).ok_or_else(|| {
374                JsNativeError::typ()
375                    .with_message("cannot convert value to a u128")
376                    .into()
377            }),
378            _ => Err(JsNativeError::typ()
379                .with_message("cannot convert value to a u128")
380                .into()),
381        }
382    }
383}
384
385#[test]
386fn integer_floating_js_value_to_integer() {
387    let context = &mut Context::default();
388
389    assert_eq!(i8::try_from_js(&JsValue::from(4.0), context), Ok(4));
390    assert_eq!(u8::try_from_js(&JsValue::from(4.0), context), Ok(4));
391    assert_eq!(i16::try_from_js(&JsValue::from(4.0), context), Ok(4));
392    assert_eq!(u16::try_from_js(&JsValue::from(4.0), context), Ok(4));
393    assert_eq!(i32::try_from_js(&JsValue::from(4.0), context), Ok(4));
394    assert_eq!(u32::try_from_js(&JsValue::from(4.0), context), Ok(4));
395    assert_eq!(i64::try_from_js(&JsValue::from(4.0), context), Ok(4));
396    assert_eq!(u64::try_from_js(&JsValue::from(4.0), context), Ok(4));
397
398    // Floating with fractional part
399    let result = i32::try_from_js(&JsValue::from(4.000_000_000_000_001), context);
400    assert!(result.is_err());
401
402    // NaN
403    let result = i32::try_from_js(&JsValue::nan(), context);
404    assert!(result.is_err());
405
406    // +Infinity
407    let result = i32::try_from_js(&JsValue::positive_infinity(), context);
408    assert!(result.is_err());
409
410    // -Infinity
411    let result = i32::try_from_js(&JsValue::negative_infinity(), context);
412    assert!(result.is_err());
413}
414
415#[test]
416fn value_into_vec() {
417    use boa_engine::{run_test_actions, TestAction};
418    use indoc::indoc;
419
420    #[derive(Debug, PartialEq, Eq, boa_macros::TryFromJs)]
421    struct TestStruct {
422        inner: bool,
423        my_int: i16,
424        my_vec: Vec<String>,
425    }
426
427    run_test_actions([
428        TestAction::assert_with_op(
429            indoc! {r#"
430            let value = {
431                inner: true,
432                my_int: 11,
433                my_vec: ["a", "b", "c"]
434            };
435            value
436        "#},
437            |value, context| {
438                let value = TestStruct::try_from_js(&value, context);
439
440                match value {
441                    Ok(value) => {
442                        value
443                            == TestStruct {
444                                inner: true,
445                                my_int: 11,
446                                my_vec: vec!["a".to_string(), "b".to_string(), "c".to_string()],
447                            }
448                    }
449                    _ => false,
450                }
451            },
452        ),
453        TestAction::assert_with_op(
454            indoc!(
455                r#"
456            let wrong = {
457                inner: false,
458                my_int: 22,
459                my_vec: [{}, "e", "f"]
460            };
461            wrong"#
462            ),
463            |value, context| {
464                let Err(value) = TestStruct::try_from_js(&value, context) else {
465                    return false;
466                };
467                assert!(value.to_string().contains("TypeError"));
468                true
469            },
470        ),
471    ]);
472}
473
474#[test]
475fn value_into_tuple() {
476    use boa_engine::{run_test_actions, TestAction};
477    use indoc::indoc;
478
479    run_test_actions([
480        TestAction::assert_with_op(indoc! {r#" [42, "hello", true] "#}, |value, context| {
481            type TestType = (i32, String, bool);
482            TestType::try_from_js(&value, context).unwrap() == (42, "hello".to_string(), true)
483        }),
484        TestAction::assert_with_op(indoc! {r#" [42, "hello", true] "#}, |value, context| {
485            type TestType = (i32, String, Option<bool>, Option<u8>);
486            TestType::try_from_js(&value, context).unwrap()
487                == (42, "hello".to_string(), Some(true), None)
488        }),
489        TestAction::assert_with_op(indoc! {r#" [] "#}, |value, context| {
490            type TestType = (
491                Option<bool>,
492                Option<bool>,
493                Option<bool>,
494                Option<bool>,
495                Option<bool>,
496                Option<bool>,
497                Option<bool>,
498                Option<bool>,
499                Option<bool>,
500                Option<bool>,
501            );
502            TestType::try_from_js(&value, context).unwrap()
503                == (None, None, None, None, None, None, None, None, None, None)
504        }),
505        TestAction::assert_with_op(indoc!(r#"[42, "hello", {}]"#), |value, context| {
506            type TestType = (i32, String, bool);
507            let Err(value) = TestType::try_from_js(&value, context) else {
508                return false;
509            };
510            assert!(value.to_string().contains("TypeError"));
511            true
512        }),
513        TestAction::assert_with_op(indoc!(r#"[42, "hello"]"#), |value, context| {
514            type TestType = (i32, String, bool);
515            let Err(value) = TestType::try_from_js(&value, context) else {
516                return false;
517            };
518            assert!(value.to_string().contains("TypeError"));
519            true
520        }),
521    ]);
522}
523
524#[test]
525fn value_into_map() {
526    use boa_engine::{run_test_actions, TestAction};
527    use indoc::indoc;
528
529    run_test_actions([
530        TestAction::assert_with_op(indoc! {r#" ({ a: 1, b: 2, c: 3 }) "#}, |value, context| {
531            let value = std::collections::BTreeMap::<String, i32>::try_from_js(&value, context);
532
533            match value {
534                Ok(value) => {
535                    value
536                        == vec![
537                            ("a".to_string(), 1),
538                            ("b".to_string(), 2),
539                            ("c".to_string(), 3),
540                        ]
541                        .into_iter()
542                        .collect::<std::collections::BTreeMap<String, i32>>()
543                }
544                _ => false,
545            }
546        }),
547        TestAction::assert_with_op(indoc! {r#" ({ a: 1, b: 2, c: 3 }) "#}, |value, context| {
548            let value = std::collections::HashMap::<String, i32>::try_from_js(&value, context);
549
550            match value {
551                Ok(value) => {
552                    value
553                        == std::collections::HashMap::from_iter(
554                            vec![
555                                ("a".to_string(), 1),
556                                ("b".to_string(), 2),
557                                ("c".to_string(), 3),
558                            ]
559                            .into_iter()
560                            .collect::<std::collections::BTreeMap<String, i32>>(),
561                        )
562                }
563                _ => false,
564            }
565        }),
566    ]);
567}
568
569#[test]
570fn js_map_into_rust_map() -> JsResult<()> {
571    use boa_engine::Source;
572    use std::collections::{BTreeMap, HashMap};
573
574    let js_code = "new Map([['a', 1], ['b', 3], ['aboba', 42024]])";
575    let mut context = Context::default();
576
577    let js_value = context.eval(Source::from_bytes(js_code))?;
578
579    let hash_map = HashMap::<String, i32>::try_from_js(&js_value, &mut context)?;
580    let btree_map = BTreeMap::<String, i32>::try_from_js(&js_value, &mut context)?;
581
582    let expect = [("a".into(), 1), ("aboba".into(), 42024), ("b".into(), 3)];
583
584    let expected_hash_map: HashMap<String, _> = expect.iter().cloned().collect();
585    assert_eq!(expected_hash_map, hash_map);
586
587    let expected_btree_map: BTreeMap<String, _> = expect.iter().cloned().collect();
588    assert_eq!(expected_btree_map, btree_map);
589    Ok(())
590}