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