From 8c92a8fa25c1339b639a5acc5c3133ae8df670a2 Mon Sep 17 00:00:00 2001 From: Iban Eguia Date: Tue, 22 Feb 2022 16:10:32 +0000 Subject: [PATCH] Added conversions from and to serde_json's Value type (#1851) This Pull Request closes #1693. It changes the following: - It adds a fallible conversion from `serde_json::Value` to `JsValue`, which requires a context. - It adds a fallible conversion from `JsValue` to `serde_json::Value`, which requires a context. - Added examples to the documentation of both methods. - Removed some duplicate and non-needed code that I found while doing this. Co-authored-by: RageKnify --- boa_engine/src/object/mod.rs | 54 ++----- boa_engine/src/value/conversions.rs | 10 -- boa_engine/src/value/mod.rs | 1 + boa_engine/src/value/serde_json.rs | 211 ++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 53 deletions(-) create mode 100644 boa_engine/src/value/serde_json.rs diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index c05fa4350f..7a100101ea 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -456,17 +456,6 @@ impl Object { ) } - #[inline] - pub fn as_array(&self) -> Option<()> { - match self.data { - ObjectData { - kind: ObjectKind::Array, - .. - } => Some(()), - _ => None, - } - } - /// Checks if it is an `ArrayIterator` object. #[inline] pub fn is_array_iterator(&self) -> bool { @@ -793,17 +782,6 @@ impl Object { ) } - #[inline] - pub fn as_error(&self) -> Option<()> { - match self.data { - ObjectData { - kind: ObjectKind::Error, - .. - } => Some(()), - _ => None, - } - } - /// Checks if it a Boolean object. #[inline] pub fn is_boolean(&self) -> bool { @@ -1159,7 +1137,10 @@ impl Object { &self.properties } - /// Helper function for property insertion. + /// Inserts a field in the object `properties` without checking if it's writable. + /// + /// If a field was already in the object with the same name, then a `Some` is returned + /// with that field's value, otherwise, `None` is returned. #[inline] pub(crate) fn insert(&mut self, key: K, property: P) -> Option where @@ -1174,19 +1155,6 @@ impl Object { pub(crate) fn remove(&mut self, key: &PropertyKey) -> Option { self.properties.remove(key) } - - /// Inserts a field in the object `properties` without checking if it's writable. - /// - /// If a field was already in the object with the same name that a `Some` is returned - /// with that field, otherwise None is returned. - #[inline] - pub fn insert_property(&mut self, key: K, property: P) -> Option - where - K: Into, - P: Into, - { - self.insert(key, property) - } } /// The functions binding. @@ -1406,8 +1374,8 @@ impl<'context> FunctionBuilder<'context> { .writable(false) .enumerable(false) .configurable(true); - object.insert_property("name", property.clone().value(self.name.clone())); - object.insert_property("length", property.value(self.length)); + object.insert("name", property.clone().value(self.name.clone())); + object.insert("length", property.value(self.length)); } } @@ -1473,7 +1441,7 @@ impl<'context> ObjectInitializer<'context> { .constructor(false) .build(); - self.object.borrow_mut().insert_property( + self.object.borrow_mut().insert( binding.binding, PropertyDescriptor::builder() .value(function) @@ -1595,7 +1563,7 @@ impl<'context> ConstructorBuilder<'context> { .constructor(false) .build(); - self.prototype.borrow_mut().insert_property( + self.prototype.borrow_mut().insert( binding.binding, PropertyDescriptor::builder() .value(function) @@ -1624,7 +1592,7 @@ impl<'context> ConstructorBuilder<'context> { .constructor(false) .build(); - self.constructor_object.borrow_mut().insert_property( + self.constructor_object.borrow_mut().insert( binding.binding, PropertyDescriptor::builder() .value(function) @@ -1842,7 +1810,7 @@ impl<'context> ConstructorBuilder<'context> { } if self.constructor_has_prototype { - constructor.insert_property( + constructor.insert( PROTOTYPE, PropertyDescriptor::builder() .value(self.prototype.clone()) @@ -1855,7 +1823,7 @@ impl<'context> ConstructorBuilder<'context> { { let mut prototype = self.prototype.borrow_mut(); - prototype.insert_property( + prototype.insert( "constructor", PropertyDescriptor::builder() .value(self.constructor_object.clone()) diff --git a/boa_engine/src/value/conversions.rs b/boa_engine/src/value/conversions.rs index 40df84a5af..03ff1d8520 100644 --- a/boa_engine/src/value/conversions.rs +++ b/boa_engine/src/value/conversions.rs @@ -127,16 +127,6 @@ impl From for JsValue { } } -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub struct TryFromObjectError; - -impl Display for TryFromObjectError { - #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Could not convert value to an Object type") - } -} - impl From<()> for JsValue { #[inline] fn from(_: ()) -> Self { diff --git a/boa_engine/src/value/mod.rs b/boa_engine/src/value/mod.rs index e91d058c8d..7d69e1080d 100644 --- a/boa_engine/src/value/mod.rs +++ b/boa_engine/src/value/mod.rs @@ -33,6 +33,7 @@ pub(crate) mod display; mod equality; mod hash; mod operations; +mod serde_json; mod r#type; pub use conversions::*; diff --git a/boa_engine/src/value/serde_json.rs b/boa_engine/src/value/serde_json.rs new file mode 100644 index 0000000000..9f8d6a7541 --- /dev/null +++ b/boa_engine/src/value/serde_json.rs @@ -0,0 +1,211 @@ +//! This module implements the conversions from and into [`serde_json::Value`]. + +use super::JsValue; +use crate::{ + builtins::Array, + property::{PropertyDescriptor, PropertyKey}, + Context, JsResult, +}; +use serde_json::{Map, Value}; + +impl JsValue { + /// Converts a [`serde_json::Value`] to a `JsValue`. + /// + /// # Example + /// + /// ``` + /// use boa_engine::{Context, JsValue}; + /// + /// let data = r#" + /// { + /// "name": "John Doe", + /// "age": 43, + /// "phones": [ + /// "+44 1234567", + /// "+44 2345678" + /// ] + /// }"#; + /// + /// let json: serde_json::Value = serde_json::from_str(data).unwrap(); + /// + /// let mut context = Context::default(); + /// let value = JsValue::from_json(&json, &mut context).unwrap(); + /// # + /// # assert_eq!(json, value.to_json(&mut context).unwrap()); + /// ``` + pub fn from_json(json: &Value, context: &mut Context) -> JsResult { + /// Biggest possible integer, as i64. + const MAX_INT: i64 = i32::MAX as i64; + + /// Smallest possible integer, as i64. + const MIN_INT: i64 = i32::MIN as i64; + + match json { + Value::Null => Ok(Self::Null), + Value::Bool(b) => Ok(Self::Boolean(*b)), + Value::Number(num) => num + .as_i64() + .filter(|n| (MIN_INT..=MAX_INT).contains(n)) + .map(|i| Self::Integer(i as i32)) + .or_else(|| num.as_f64().map(Self::Rational)) + .ok_or_else(|| { + context.construct_type_error(format!( + "could not convert JSON number {num} to JsValue" + )) + }), + Value::String(string) => Ok(Self::from(string.as_str())), + Value::Array(vec) => { + let mut arr = Vec::with_capacity(vec.len()); + for val in vec { + arr.push(Self::from_json(val, context)?); + } + Ok(Array::create_array_from_list(arr, context).into()) + } + Value::Object(obj) => { + let js_obj = context.construct_object(); + for (key, value) in obj { + let property = PropertyDescriptor::builder() + .value(Self::from_json(value, context)?) + .writable(true) + .enumerable(true) + .configurable(true); + js_obj.borrow_mut().insert(key.as_str(), property); + } + + Ok(js_obj.into()) + } + } + } + + /// Converts the `JsValue` to a [`serde_json::Value`]. + /// + /// # Example + /// + /// ``` + /// use boa_engine::{Context, JsValue}; + /// + /// let data = r#" + /// { + /// "name": "John Doe", + /// "age": 43, + /// "phones": [ + /// "+44 1234567", + /// "+44 2345678" + /// ] + /// }"#; + /// + /// let json: serde_json::Value = serde_json::from_str(data).unwrap(); + /// + /// let mut context = Context::default(); + /// let value = JsValue::from_json(&json, &mut context).unwrap(); + /// + /// let back_to_json = value.to_json(&mut context).unwrap(); + /// # + /// # assert_eq!(json, back_to_json); + /// ``` + pub fn to_json(&self, context: &mut Context) -> JsResult { + match self { + Self::Null => Ok(Value::Null), + Self::Undefined => todo!("undefined to JSON"), + &Self::Boolean(b) => Ok(b.into()), + Self::String(string) => Ok(string.as_str().into()), + &Self::Rational(rat) => Ok(rat.into()), + &Self::Integer(int) => Ok(int.into()), + Self::BigInt(_bigint) => context.throw_type_error("cannot convert bigint to JSON"), + Self::Object(obj) => { + if obj.is_array() { + let len = obj.length_of_array_like(context)?; + let mut arr = Vec::with_capacity(len); + + let obj = obj.borrow(); + + for k in 0..len as u32 { + let val = obj.properties().get(&k.into()).map_or(Self::Null, |desc| { + desc.value().cloned().unwrap_or(Self::Null) + }); + arr.push(val.to_json(context)?); + } + + Ok(Value::Array(arr)) + } else { + let mut map = Map::new(); + for (key, property) in obj.borrow().properties().iter() { + let key = match &key { + PropertyKey::String(string) => string.as_str().to_owned(), + PropertyKey::Index(i) => i.to_string(), + PropertyKey::Symbol(_sym) => { + return context.throw_type_error("cannot convert Symbol to JSON") + } + }; + + let value = match property.value() { + Some(val) => val.to_json(context)?, + None => Value::Null, + }; + + map.insert(key, value); + } + + Ok(Value::Object(map)) + } + } + Self::Symbol(_sym) => context.throw_type_error("cannot convert Symbol to JSON"), + } + } +} + +#[cfg(test)] +mod tests { + use crate::object::JsArray; + + #[test] + fn ut_json_conversions() { + use crate::{Context, JsValue}; + + let data = r#" + { + "name": "John Doe", + "age": 43, + "minor": false, + "adult": true, + "extra": { + "address": null + }, + "phones": [ + "+44 1234567", + -45, + {}, + true + ] + }"#; + + let json: serde_json::Value = serde_json::from_str(data).unwrap(); + assert!(json.is_object()); + + let mut context = Context::default(); + let value = JsValue::from_json(&json, &mut context).unwrap(); + + let obj = value.as_object().unwrap(); + assert_eq!(obj.get("name", &mut context).unwrap(), "John Doe".into()); + assert_eq!(obj.get("age", &mut context).unwrap(), 43_i32.into()); + assert_eq!(obj.get("minor", &mut context).unwrap(), false.into()); + assert_eq!(obj.get("adult", &mut context).unwrap(), true.into()); + { + let extra = obj.get("extra", &mut context).unwrap(); + let extra = extra.as_object().unwrap(); + assert!(extra.get("address", &mut context).unwrap().is_null()); + } + { + let phones = obj.get("phones", &mut context).unwrap(); + let phones = phones.as_object().unwrap(); + + let arr = JsArray::from_object(phones.clone(), &mut context).unwrap(); + assert_eq!(arr.at(0, &mut context).unwrap(), "+44 1234567".into()); + assert_eq!(arr.at(1, &mut context).unwrap(), JsValue::from(-45_i32)); + assert!(arr.at(2, &mut context).unwrap().is_object()); + assert_eq!(arr.at(3, &mut context).unwrap(), true.into()); + } + + assert_eq!(json, value.to_json(&mut context).unwrap()); + } +}