easy_qjs/
value.rs

1use std::fmt;
2
3use crate::JsonValue;
4use itertools::Itertools;
5use js::{Array, Ctx, FromAtom, FromJs, IntoJs, Null, Object};
6use serde::Serialize;
7use serde_json::{json, Value};
8
9impl<'js> FromJs<'js> for JsonValue {
10    fn from_js(_ctx: Ctx<'js>, val: js::Value<'js>) -> Result<Self, js::Error> {
11        let v = match val {
12            val if val.type_name() == "null" => Value::Null,
13            val if val.type_name() == "undefined" => Value::Null,
14            val if val.is_bool() => val.as_bool().expect("checked bool").into(),
15            val if val.is_string() => {
16                match val.into_string().expect("checked string").to_string() {
17                    Ok(v) => Value::String(v),
18                    Err(e) => return Err(e),
19                }
20            }
21            val if val.is_int() => val.as_int().expect("checked int").into(),
22            val if val.is_float() => val.as_float().expect("checked float").into(),
23            val if val.is_array() => {
24                let v = val.as_array().expect("checked array");
25                let mut x = Vec::with_capacity(v.len());
26                for i in v.iter() {
27                    let v = i?;
28                    let v = JsonValue::from_js(_ctx, v)?;
29                    x.push(v.into());
30                }
31                Value::Array(x)
32            }
33            val if val.is_object() => {
34                // Extract the value as an object
35                let v = val.into_object().expect("checked object");
36
37                // Check to see if this object is a function. We don't support it
38                if v.as_function().is_some() {
39                    return Ok(Self::null());
40                }
41                // This object is a normal object
42                let mut x = json!({});
43                for i in v.props() {
44                    let (k, v) = i?;
45                    let k = String::from_atom(k)?;
46                    let v = JsonValue::from_js(_ctx, v)?;
47                    x[k] = v.into();
48                }
49                x
50            }
51            _ => Value::Null,
52        };
53        Ok(v.into())
54    }
55}
56
57impl<'js> IntoJs<'js> for JsonValue {
58    fn into_js(self, ctx: Ctx<'js>) -> Result<js::Value<'js>, js::Error> {
59        match self.0 {
60            Value::Null => Null.into_js(ctx),
61            Value::Bool(v) => Ok(js::Value::new_bool(ctx, v)),
62            Value::Number(num) => {
63                if num.is_f64() {
64                    Ok(js::Value::new_float(
65                        ctx,
66                        num.as_f64().expect("checked f64"),
67                    ))
68                } else if num.is_i64() {
69                    Ok(js::Value::new_number(
70                        ctx,
71                        num.as_i64().expect("checked f64") as _,
72                    ))
73                } else {
74                    Ok(js::Value::new_number(
75                        ctx,
76                        num.as_u64().expect("checked f64") as _,
77                    ))
78                }
79            }
80            Value::String(v) => js::String::from_str(ctx, &v)?.into_js(ctx),
81            Value::Array(v) => {
82                let x = Array::new(ctx)?;
83                for (i, v) in v.into_iter().enumerate() {
84                    x.set(i, JsonValue(v).into_js(ctx)?)?;
85                }
86                x.into_js(ctx)
87            }
88            Value::Object(v) => {
89                let x = Object::new(ctx)?;
90                for (k, v) in v.into_iter() {
91                    x.set(k, JsonValue(v).into_js(ctx)?)?;
92                }
93                x.into_js(ctx)
94            }
95        }
96    }
97}
98
99impl From<Value> for JsonValue {
100    fn from(v: Value) -> Self {
101        Self(v)
102    }
103}
104
105impl From<JsonValue> for Value {
106    fn from(v: JsonValue) -> Self {
107        v.0
108    }
109}
110
111impl Default for JsonValue {
112    fn default() -> Self {
113        Self(json!({}))
114    }
115}
116
117impl JsonValue {
118    pub fn null() -> Self {
119        Self(Value::Null)
120    }
121
122    pub fn array<T: Serialize>(arr: Vec<T>) -> Self {
123        Self(json!(arr))
124    }
125
126    pub fn object<T: Serialize>(obj: T) -> Self {
127        Self(json!(obj))
128    }
129}
130
131impl fmt::Display for JsonValue {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match &self.0 {
134            Value::Null => write!(f, "null"),
135            Value::Bool(v) => write!(f, "{}", v),
136            Value::Number(v) => write!(f, "{}", v),
137            Value::String(v) => write!(f, "{}", v),
138            Value::Array(v) => {
139                write!(f, "[")?;
140                write!(f, "{}", v.iter().join(", "))?;
141                write!(f, "]")
142            }
143            Value::Object(_) => {
144                let s = serde_json::to_string_pretty(&self.0).map_err(|_| fmt::Error)?;
145                write!(f, "{}", s)
146            }
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    #[cfg(feature = "console")]
155    use crate::builtins::con::Console;
156    use crate::JsEngine;
157    use anyhow::Result;
158    use js::Function;
159
160    #[js::bind(object, public)]
161    #[quickjs(bare, rename = "print")]
162    #[allow(unused_variables)]
163    fn print(s: String) {
164        println!("{}", s);
165    }
166
167    #[cfg(not(feature = "dispatcher"))]
168    #[tokio::test]
169    async fn json_value_should_be_converted_to_js() -> Result<()> {
170        let engine = JsEngine::create()?;
171        let _ret: Result<()> = engine.context.with(|ctx| {
172            let v = JsonValue::object(json!({
173              "name": "John",
174              "age": 30,
175              "cars": [
176                "Ford",
177                "BMW",
178                "Fiat"
179              ]
180            }));
181            let js = v.clone().into_js(ctx)?;
182            assert_eq!(js.type_name(), "object");
183            let v1 = JsonValue::from_js(ctx, js)?;
184            assert_eq!(v, v1);
185            Ok(())
186        });
187        Ok(())
188    }
189
190    #[cfg(not(feature = "dispatcher"))]
191    #[tokio::test]
192    async fn js_object_might_be_converted_as_null() -> Result<()> {
193        let engine = JsEngine::create()?;
194        let _ret: Result<()> = engine.context.with(|ctx| {
195            let obj = Object::new(ctx)?;
196            obj.set("name", "John")?;
197            #[cfg(feature = "console")]
198            obj.set("obj", Console)?;
199            obj.set("fun", Function::new(ctx, print))?;
200            let js = obj.into_js(ctx)?;
201            let v = JsonValue::from_js(ctx, js)?;
202            #[cfg(feature = "console")]
203            assert_eq!(v.0, json!({ "name": "John", "obj": {}, "fun": null }));
204            #[cfg(not(feature = "console"))]
205            assert_eq!(v.0, json!({ "name": "John", "fun": null }));
206            Ok(())
207        });
208        Ok(())
209    }
210}