quick_js/value/
mod.rs

1#[cfg(feature = "bigint")]
2pub(crate) mod bigint;
3
4use std::convert::{TryFrom, TryInto};
5use std::{collections::HashMap, error, fmt};
6
7#[cfg(feature = "bigint")]
8pub use bigint::BigInt;
9
10/// A value that can be (de)serialized to/from the quickjs runtime.
11#[derive(PartialEq, Clone, Debug)]
12#[allow(missing_docs)]
13pub enum JsValue {
14    Undefined,
15    Null,
16    Bool(bool),
17    Int(i32),
18    Float(f64),
19    String(String),
20    Array(Vec<JsValue>),
21    Object(HashMap<String, JsValue>),
22    /// chrono::Datetime<Utc> / JS Date integration.
23    /// Only available with the optional `chrono` feature.
24    #[cfg(feature = "chrono")]
25    Date(chrono::DateTime<chrono::Utc>),
26    /// num_bigint::BigInt / JS BigInt integration
27    /// Only available with the optional `bigint` feature
28    #[cfg(feature = "bigint")]
29    BigInt(crate::BigInt),
30    #[doc(hidden)]
31    __NonExhaustive,
32}
33
34impl JsValue {
35    /// Cast value to a str.
36    ///
37    /// Returns `Some(&str)` if value is a `JsValue::String`, None otherwise.
38    pub fn as_str(&self) -> Option<&str> {
39        match self {
40            JsValue::String(ref s) => Some(s.as_str()),
41            _ => None,
42        }
43    }
44
45    /// Convert to `String`.
46    pub fn into_string(self) -> Option<String> {
47        match self {
48            JsValue::String(s) => Some(s),
49            _ => None,
50        }
51    }
52}
53
54macro_rules! value_impl_from {
55    (
56        (
57            $(  $t1:ty => $var1:ident, )*
58        )
59        (
60            $( $t2:ty => |$exprname:ident| $expr:expr => $var2:ident, )*
61        )
62    ) => {
63        $(
64            impl From<$t1> for JsValue {
65                fn from(value: $t1) -> Self {
66                    JsValue::$var1(value)
67                }
68            }
69
70            impl std::convert::TryFrom<JsValue> for $t1 {
71                type Error = ValueError;
72
73                fn try_from(value: JsValue) -> Result<Self, Self::Error> {
74                    match value {
75                        JsValue::$var1(inner) => Ok(inner),
76                        _ => Err(ValueError::UnexpectedType)
77                    }
78
79                }
80            }
81        )*
82        $(
83            impl From<$t2> for JsValue {
84                fn from(value: $t2) -> Self {
85                    let $exprname = value;
86                    let inner = $expr;
87                    JsValue::$var2(inner)
88                }
89            }
90        )*
91    }
92}
93
94value_impl_from! {
95    (
96        bool => Bool,
97        i32 => Int,
98        f64 => Float,
99        String => String,
100    )
101    (
102        i8 => |x| i32::from(x) => Int,
103        i16 => |x| i32::from(x) => Int,
104        u8 => |x| i32::from(x) => Int,
105        u16 => |x| i32::from(x) => Int,
106        u32 => |x| f64::from(x) => Float,
107    )
108}
109
110#[cfg(feature = "bigint")]
111value_impl_from! {
112    ()
113    (
114        i64 => |x| x.into() => BigInt,
115        u64 => |x| num_bigint::BigInt::from(x).into() => BigInt,
116        i128 => |x| num_bigint::BigInt::from(x).into() => BigInt,
117        u128 => |x| num_bigint::BigInt::from(x).into() => BigInt,
118        num_bigint::BigInt => |x| x.into() => BigInt,
119    )
120}
121
122#[cfg(feature = "bigint")]
123impl std::convert::TryFrom<JsValue> for i64 {
124    type Error = ValueError;
125
126    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
127        match value {
128            JsValue::Int(int) => Ok(int as i64),
129            JsValue::BigInt(bigint) => bigint.as_i64().ok_or(ValueError::UnexpectedType),
130            _ => Err(ValueError::UnexpectedType),
131        }
132    }
133}
134
135#[cfg(feature = "bigint")]
136macro_rules! value_bigint_impl_tryfrom {
137    (
138        ($($t:ty => $to_type:ident, )*)
139    ) => {
140        $(
141            impl std::convert::TryFrom<JsValue> for $t {
142                type Error = ValueError;
143
144                fn try_from(value: JsValue) -> Result<Self, Self::Error> {
145                    use num_traits::ToPrimitive;
146
147                    match value {
148                        JsValue::Int(int) => Ok(int as $t),
149                        JsValue::BigInt(bigint) => bigint
150                            .into_bigint()
151                            .$to_type()
152                            .ok_or(ValueError::UnexpectedType),
153                        _ => Err(ValueError::UnexpectedType),
154                    }
155                }
156            }
157        )*
158    }
159}
160
161#[cfg(feature = "bigint")]
162value_bigint_impl_tryfrom! {
163    (
164        u64 => to_u64,
165        i128 => to_i128,
166        u128 => to_u128,
167    )
168}
169
170#[cfg(feature = "bigint")]
171impl std::convert::TryFrom<JsValue> for num_bigint::BigInt {
172    type Error = ValueError;
173
174    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
175        match value {
176            JsValue::Int(int) => Ok(num_bigint::BigInt::from(int)),
177            JsValue::BigInt(bigint) => Ok(bigint.into_bigint()),
178            _ => Err(ValueError::UnexpectedType),
179        }
180    }
181}
182
183impl<T> From<Vec<T>> for JsValue
184where
185    T: Into<JsValue>,
186{
187    fn from(values: Vec<T>) -> Self {
188        let items = values.into_iter().map(|x| x.into()).collect();
189        JsValue::Array(items)
190    }
191}
192
193impl<T> TryFrom<JsValue> for Vec<T>
194where
195    T: TryFrom<JsValue>,
196{
197    type Error = ValueError;
198
199    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
200        match value {
201            JsValue::Array(items) => items
202                .into_iter()
203                .map(|item| item.try_into().map_err(|_| ValueError::UnexpectedType))
204                .collect(),
205            _ => Err(ValueError::UnexpectedType),
206        }
207    }
208}
209
210impl<'a> From<&'a str> for JsValue {
211    fn from(val: &'a str) -> Self {
212        JsValue::String(val.into())
213    }
214}
215
216impl<T> From<Option<T>> for JsValue
217where
218    T: Into<JsValue>,
219{
220    fn from(opt: Option<T>) -> Self {
221        if let Some(value) = opt {
222            value.into()
223        } else {
224            JsValue::Null
225        }
226    }
227}
228
229impl<K, V> From<HashMap<K, V>> for JsValue
230where
231    K: Into<String>,
232    V: Into<JsValue>,
233{
234    fn from(map: HashMap<K, V>) -> Self {
235        let new_map = map.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
236        JsValue::Object(new_map)
237    }
238}
239
240impl<V> TryFrom<JsValue> for HashMap<String, V>
241where
242    V: TryFrom<JsValue>,
243{
244    type Error = ValueError;
245
246    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
247        match value {
248            JsValue::Object(object) => object
249                .into_iter()
250                .map(|(k, v)| match v.try_into() {
251                    Ok(v) => Ok((k, v)),
252                    Err(_) => Err(ValueError::UnexpectedType),
253                })
254                .collect(),
255            _ => Err(ValueError::UnexpectedType),
256        }
257    }
258}
259
260/// Error during value conversion.
261#[derive(PartialEq, Eq, Debug)]
262#[non_exhaustive]
263pub enum ValueError {
264    /// Invalid non-utf8 string.
265    InvalidString(std::str::Utf8Error),
266    /// Encountered string with \0 bytes.
267    StringWithZeroBytes(std::ffi::NulError),
268    /// Internal error.
269    Internal(String),
270    /// Received an unexpected type that could not be converted.
271    UnexpectedType,
272}
273
274// TODO: remove this once either the Never type get's stabilized or the compiler
275// can properly handle Infallible.
276impl From<std::convert::Infallible> for ValueError {
277    fn from(_: std::convert::Infallible) -> Self {
278        unreachable!()
279    }
280}
281
282impl fmt::Display for ValueError {
283    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
284        use ValueError::*;
285        match self {
286            InvalidString(e) => write!(f, "Value conversion failed - invalid non-utf8 string: {e}"),
287            StringWithZeroBytes(_) => write!(f, "String contains \\0 bytes",),
288            Internal(e) => write!(f, "Value conversion failed - internal error: {e}"),
289            UnexpectedType => write!(f, "Could not convert - received unexpected type"),
290        }
291    }
292}
293
294impl error::Error for ValueError {}
295
296#[cfg(test)]
297mod tests {
298    #[allow(unused_imports)]
299    use super::*;
300
301    #[cfg(feature = "bigint")]
302    #[test]
303    fn test_bigint_from_i64() {
304        let int = 1234i64;
305        let value = JsValue::from(int);
306        if let JsValue::BigInt(value) = value {
307            assert_eq!(value.as_i64(), Some(int));
308        } else {
309            panic!("Expected JsValue::BigInt");
310        }
311    }
312
313    #[cfg(feature = "bigint")]
314    #[test]
315    fn test_bigint_from_bigint() {
316        let bigint = num_bigint::BigInt::from(std::i128::MAX);
317        let value = JsValue::from(bigint.clone());
318        if let JsValue::BigInt(value) = value {
319            assert_eq!(value.into_bigint(), bigint);
320        } else {
321            panic!("Expected JsValue::BigInt");
322        }
323    }
324
325    #[cfg(feature = "bigint")]
326    #[test]
327    fn test_bigint_i64_bigint_eq() {
328        let value_i64 = JsValue::BigInt(1234i64.into());
329        let value_bigint = JsValue::BigInt(num_bigint::BigInt::from(1234i64).into());
330        assert_eq!(value_i64, value_bigint);
331    }
332}