rquickjs_core/value/convert/
from.rs

1use crate::{
2    convert::List, Array, CString, Ctx, Error, FromAtom, FromJs, Object, Result, StdString, String,
3    Type, Value,
4};
5use alloc::{
6    boxed::Box,
7    collections::{BTreeMap, BTreeSet, LinkedList, VecDeque},
8    rc::Rc,
9    sync::Arc,
10    vec::Vec,
11};
12#[allow(unused_imports)]
13use core::{
14    cell::{Cell, RefCell},
15    hash::{BuildHasher, Hash},
16    time::Duration,
17};
18use hashbrown::{HashMap as HashbrownMap, HashSet as HashbrownSet};
19
20#[cfg(feature = "std")]
21use std::{
22    collections::{HashMap, HashSet},
23    sync::{Mutex, RwLock},
24    time::SystemTime,
25};
26
27#[cfg(feature = "either")]
28use either::{Either, Left, Right};
29
30#[cfg(feature = "indexmap")]
31use indexmap::{IndexMap, IndexSet};
32
33impl<'js> FromJs<'js> for Value<'js> {
34    fn from_js(_: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
35        Ok(value)
36    }
37}
38
39impl<'js> FromJs<'js> for StdString {
40    fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
41        String::from_value(value).and_then(|string| string.to_string())
42    }
43}
44
45impl<'js> FromJs<'js> for char {
46    fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
47        let type_name = value.type_name();
48        let s = String::from_value(value)?.to_string()?;
49
50        let mut chars = s.chars();
51        let (c, more) = (chars.next(), chars.next());
52
53        match (c, more) {
54            (Some(c), None) => Ok(c),
55            _ => Err(Error::FromJs {
56                from: type_name,
57                to: "char",
58                message: Some("The length of the string converted to char must be 1".into()),
59            }),
60        }
61    }
62}
63
64impl<'js> FromJs<'js> for CString<'js> {
65    fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
66        String::from_value(value)?.to_cstring()
67    }
68}
69
70/// Convert from JS as any
71impl<'js> FromJs<'js> for () {
72    fn from_js(_: &Ctx<'js>, _: Value<'js>) -> Result<Self> {
73        Ok(())
74    }
75}
76
77/// Convert from JS as optional
78impl<'js, T> FromJs<'js> for Option<T>
79where
80    T: FromJs<'js>,
81{
82    fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
83        if value.type_of().is_void() {
84            Ok(None)
85        } else {
86            T::from_js(ctx, value).map(Some)
87        }
88    }
89}
90
91/// Convert from JS as result
92impl<'js, T> FromJs<'js> for Result<T>
93where
94    T: FromJs<'js>,
95{
96    // TODO this function seems a bit hacky.
97    // Exceptions are generally by the marshalling handled when returned callback.
98    fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
99        unsafe {
100            match ctx.handle_exception(value.into_js_value()) {
101                Ok(val) => T::from_js(ctx, Value::from_js_value(ctx.clone(), val)).map(Ok),
102                Err(error) => Ok(Err(error)),
103            }
104        }
105    }
106}
107
108/// Convert from JS to either
109#[cfg(feature = "either")]
110#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "either")))]
111impl<'js, L, R> FromJs<'js> for Either<L, R>
112where
113    L: FromJs<'js>,
114    R: FromJs<'js>,
115{
116    fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
117        L::from_js(ctx, value.clone()).map(Left).or_else(|error| {
118            if error.is_from_js() {
119                R::from_js(ctx, value).map(Right)
120            } else {
121                Err(error)
122            }
123        })
124    }
125}
126
127fn tuple_match_size(actual: usize, expected: usize) -> Result<()> {
128    if actual == expected {
129        Ok(())
130    } else {
131        Err(Error::new_from_js_message(
132            "array",
133            "tuple",
134            if actual < expected {
135                "Not enough values"
136            } else {
137                "Too many values"
138            },
139        ))
140    }
141}
142
143fn number_match_range<T: PartialOrd>(
144    val: T,
145    min: T,
146    max: T,
147    from: &'static str,
148    to: &'static str,
149) -> Result<()> {
150    if val < min {
151        Err(Error::new_from_js_message(from, to, "Underflow"))
152    } else if val > max {
153        Err(Error::new_from_js_message(from, to, "Overflow"))
154    } else {
155        Ok(())
156    }
157}
158
159macro_rules! from_js_impls {
160    // for reference types
161    (ref: $($(#[$meta:meta])* $type:ident,)*) => {
162        $(
163            $(#[$meta])*
164            impl<'js, T> FromJs<'js> for $type<T>
165            where
166                T: FromJs<'js>,
167            {
168                fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
169                    T::from_js(ctx, value).map($type::new)
170                }
171            }
172        )*
173    };
174
175    // for tuple types
176    (tup: $($($type:ident)*,)*) => {
177        $(
178            impl<'js, $($type,)*> FromJs<'js> for List<($($type,)*)>
179            where
180                $($type: FromJs<'js>,)*
181            {
182                fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
183                    let array = Array::from_value(value)?;
184
185                    let tuple_len = 0 $(+ from_js_impls!(@one $type))*;
186                    let array_len = array.len();
187                    tuple_match_size(array_len, tuple_len)?;
188
189                    Ok(List((
190                        $(array.get::<$type>(from_js_impls!(@idx $type))?,)*
191                    )))
192                }
193            }
194        )*
195    };
196
197    // for list-like Rust types
198    (list: $($(#[$meta:meta])* $type:ident $({$param:ident: $($pguard:tt)*})* $(($($guard:tt)*))*,)*) => {
199        $(
200            $(#[$meta])*
201            impl<'js, T $(,$param)*> FromJs<'js> for $type<T $(,$param)*>
202            where
203                T: FromJs<'js> $(+ $($guard)*)*,
204                $($param: $($pguard)*,)*
205            {
206                fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
207                    let array = Array::from_value(value)?;
208                    array.iter().collect::<Result<_>>()
209                }
210            }
211        )*
212    };
213
214    // for map-like Rust types
215    (map: $($(#[$meta:meta])* $type:ident $({$param:ident: $($pguard:tt)*})* $(($($guard:tt)*))*,)*) => {
216        $(
217            $(#[$meta])*
218            impl<'js, K, V $(,$param)*> FromJs<'js> for $type<K, V $(,$param)*>
219            where
220                K: FromAtom<'js> $(+ $($guard)*)*,
221                V: FromJs<'js>,
222                $($param: $($pguard)*,)*
223            {
224                fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
225                    let object = Object::from_value(value)?;
226                    object.props().collect::<Result<_>>()
227                }
228            }
229        )*
230    };
231
232    // for basic primitive types (int and float)
233    // (ex. f64 => Float as_float Int as_int)
234    (val: $($type:ty => $($jstype:ident $getfn:ident)*,)*) => {
235        $(
236            impl<'js> FromJs<'js> for $type {
237                fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
238                    let type_ = value.type_of();
239                    match type_ {
240                        $(Type::$jstype => Ok(unsafe { value.$getfn() } as _),)*
241                        _ => Err(Error::new_from_js(type_.as_str(), stringify!($type))),
242                    }
243                }
244            }
245        )*
246    };
247
248    // for other primitive types
249    (val: $($base:ident: $($type:ident)*,)*) => {
250        $(
251            $(
252                impl<'js> FromJs<'js> for $type {
253                    fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
254                        let num = <$base>::from_js(ctx, value)?;
255                        number_match_range(num, $type::MIN as $base, $type::MAX as $base, stringify!($base), stringify!($type))?;
256                        Ok(num as $type)
257                    }
258                }
259            )*
260        )*
261    };
262
263    (@one $($t:tt)*) => { 1 };
264
265    (@idx A) => { 0 };
266    (@idx B) => { 1 };
267    (@idx C) => { 2 };
268    (@idx D) => { 3 };
269    (@idx E) => { 4 };
270    (@idx F) => { 5 };
271    (@idx G) => { 6 };
272    (@idx H) => { 7 };
273    (@idx I) => { 8 };
274    (@idx J) => { 9 };
275    (@idx K) => { 10 };
276    (@idx L) => { 11 };
277    (@idx M) => { 12 };
278    (@idx N) => { 13 };
279    (@idx O) => { 14 };
280    (@idx P) => { 15 };
281}
282
283from_js_impls! {
284    val:
285    i32: i8 u8 i16 u16,
286    f64: u32 u64 i64 usize isize,
287}
288
289from_js_impls! {
290    val:
291    bool => Bool get_bool,
292    i32 => Float get_float Int get_int,
293    f64 => Float get_float Int get_int,
294}
295
296from_js_impls! {
297    ref:
298    Box,
299    Rc,
300    Arc,
301    Cell,
302    RefCell,
303    #[cfg(feature = "std")]
304    Mutex,
305    #[cfg(feature = "std")]
306    RwLock,
307}
308
309from_js_impls! {
310    tup:
311    A,
312    A B,
313    A B C,
314    A B C D,
315    A B C D E,
316    A B C D E F,
317    A B C D E F G,
318    A B C D E F G H,
319    A B C D E F G H I,
320    A B C D E F G H I J,
321    A B C D E F G H I J K,
322    A B C D E F G H I J K L,
323    A B C D E F G H I J K L M,
324    A B C D E F G H I J K L M N,
325    A B C D E F G H I J K L M N O,
326    A B C D E F G H I J K L M N O P,
327}
328
329from_js_impls! {
330    list:
331    /// Convert from JS array to Rust vector
332    Vec,
333    /// Convert from JS array to Rust vector deque
334    VecDeque,
335    /// Convert from JS array to Rust linked list
336    LinkedList,
337    /// Convert from JS array to Rust hash set
338    #[cfg(feature = "std")]
339    HashSet {S: Default + BuildHasher} (Eq + Hash),
340    /// Convert from JS array to hashbrown hash set
341    HashbrownSet {S: Default + BuildHasher} (Eq + Hash),
342    /// Convert from JS array to Rust btree set
343    BTreeSet (Eq + Ord),
344    /// Convert from JS array to Rust index set
345    #[cfg(feature = "indexmap")]
346    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "indexmap")))]
347    IndexSet {S: Default + BuildHasher} (Eq + Hash),
348}
349
350from_js_impls! {
351    map:
352    /// Convert from JS object to Rust hash map
353    #[cfg(feature = "std")]
354    HashMap {S: Default + BuildHasher} (Eq + Hash),
355    /// Convert from JS object to hashbrown hash map
356    HashbrownMap {S: Default + BuildHasher} (Eq + Hash),
357    /// Convert from JS object to Rust btree map
358    BTreeMap (Eq + Ord),
359    /// Convert from JS object to Rust index map
360    #[cfg(feature = "indexmap")]
361    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "indexmap")))]
362    IndexMap {S: Default + BuildHasher} (Eq + Hash),
363}
364
365impl<'js> FromJs<'js> for f32 {
366    fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
367        f64::from_js(ctx, value).map(|value| value as _)
368    }
369}
370
371#[allow(dead_code)]
372fn date_to_millis<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result<i64> {
373    let global = ctx.globals();
374    let date_ctor: Object = global.get("Date")?;
375
376    let value = Object::from_value(value)?;
377
378    if !value.is_instance_of(&date_ctor) {
379        return Err(Error::new_from_js("Object", "Date"));
380    }
381
382    let get_time_fn: crate::Function = value.get("getTime")?;
383
384    get_time_fn.call((crate::function::This(value),))
385}
386
387#[cfg(feature = "std")]
388impl<'js> FromJs<'js> for SystemTime {
389    fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<SystemTime> {
390        let millis = date_to_millis(ctx, value)?;
391
392        if millis >= 0 {
393            // since unix epoch
394            SystemTime::UNIX_EPOCH
395                .checked_add(Duration::from_millis(millis as _))
396                .ok_or_else(|| {
397                    Error::new_from_js_message("Date", "SystemTime", "Timestamp too big")
398                })
399        } else {
400            // before unix epoch
401            SystemTime::UNIX_EPOCH
402                .checked_sub(Duration::from_millis((-millis) as _))
403                .ok_or_else(|| {
404                    Error::new_from_js_message("Date", "SystemTime", "Timestamp too small")
405                })
406        }
407    }
408}
409
410macro_rules! chrono_from_js_impls {
411    ($($type:ident;)+) => {
412        $(
413            #[cfg(feature = "chrono")]
414            impl<'js> FromJs<'js> for chrono::DateTime<chrono::$type> {
415                fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<chrono::DateTime<chrono::$type>> {
416                    use chrono::TimeZone;
417
418                    let millis = date_to_millis(ctx, value)?;
419
420                    chrono::$type.timestamp_millis_opt(millis).single()
421                        .ok_or_else(|| {
422                            Error::new_from_js_message("Date", "chrono::DateTime", "Invalid timestamp")
423                        })
424                }
425            }
426        )+
427    };
428}
429
430chrono_from_js_impls! {
431    Utc;
432    Local;
433}
434
435#[cfg(test)]
436mod test {
437    #[cfg(target_arch = "wasm32")]
438    use super::Error;
439
440    #[test]
441    fn js_to_system_time() {
442        use crate::{Context, Runtime};
443        use std::time::{Duration, SystemTime};
444
445        let runtime = Runtime::new().unwrap();
446        let ctx = Context::full(&runtime).unwrap();
447
448        ctx.with(|ctx| {
449            let res: SystemTime = ctx.eval("new Date(123456789)").unwrap();
450            assert_eq!(
451                Duration::from_millis(123456789),
452                res.duration_since(SystemTime::UNIX_EPOCH).unwrap()
453            );
454
455            #[cfg(not(target_arch = "wasm32"))]
456            {
457                let res: SystemTime = ctx.eval("new Date(-123456789)").unwrap();
458                assert_eq!(
459                    Duration::from_millis(123456789),
460                    SystemTime::UNIX_EPOCH.duration_since(res).unwrap()
461                );
462            }
463
464            // wasm32-wasip1 and wasm32-wasip2 do not support SystemTime before the Unix Epoch
465            #[cfg(target_arch = "wasm32")]
466            {
467                let res: Error = ctx
468                    .eval::<SystemTime, &str>("new Date(-123456789)")
469                    .unwrap_err();
470                assert_eq!(
471                    "Error converting from js 'Date' into type 'SystemTime': Timestamp too small",
472                    res.to_string()
473                );
474            }
475        });
476    }
477
478    #[cfg(feature = "chrono")]
479    #[test]
480    fn js_to_chrono() {
481        use crate::{Context, Runtime};
482        use chrono::{DateTime, Utc};
483
484        let runtime = Runtime::new().unwrap();
485        let ctx = Context::full(&runtime).unwrap();
486
487        ctx.with(|ctx| {
488            let res: DateTime<Utc> = ctx.eval("new Date(123456789)").unwrap();
489            assert_eq!(123456789, res.timestamp_millis());
490        });
491
492        ctx.with(|ctx| {
493            let res: DateTime<Utc> = ctx
494                .eval("new Date('Fri Jun 03 2022 23:16:50 GMT+0300')")
495                .unwrap();
496            assert_eq!(1654287410000, res.timestamp_millis());
497        });
498
499        ctx.with(|ctx| {
500            let res: DateTime<Utc> = ctx
501                .eval("new Date('Fri Jun 03 2022 23:16:50 GMT-0300')")
502                .unwrap();
503            assert_eq!(1654309010000, res.timestamp_millis());
504        });
505    }
506}