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 {
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 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 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 let result = i32::try_from_js(&JsValue::from(4.000_000_000_000_001), context);
238 assert!(result.is_err());
239
240 let result = i32::try_from_js(&JsValue::nan(), context);
242 assert!(result.is_err());
243
244 let result = i32::try_from_js(&JsValue::positive_infinity(), context);
246 assert!(result.is_err());
247
248 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}