1use 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
11pub trait TryFromJs: Sized {
13 fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self>;
15}
16
17impl JsValue {
18 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 let result = i32::try_from_js(&JsValue::from(4.000_000_000_000_001), context);
400 assert!(result.is_err());
401
402 let result = i32::try_from_js(&JsValue::nan(), context);
404 assert!(result.is_err());
405
406 let result = i32::try_from_js(&JsValue::positive_infinity(), context);
408 assert!(result.is_err());
409
410 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}