1use 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
11pub trait TryFromJs: Sized {
35 fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self>;
37}
38
39impl JsValue {
40 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 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 let result = i32::try_from_js(&JsValue::from(4.000_000_000_000_001), context);
260 assert!(result.is_err());
261
262 let result = i32::try_from_js(&JsValue::nan(), context);
264 assert!(result.is_err());
265
266 let result = i32::try_from_js(&JsValue::positive_infinity(), context);
268 assert!(result.is_err());
269
270 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}