Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Json stringify replacer #402

Merged
merged 31 commits into from
May 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ad49560
remove undefined values from objects
n14little May 16, 2020
38987ca
remove function values from objects
n14little May 17, 2020
414cc30
json stringify "whitelist" replacer
n14little May 12, 2020
c9b3ff6
run rust format
n14little May 14, 2020
1ff51d9
prefer if let over single case match
n14little May 14, 2020
3585ed7
exit early when replacer and space arguments are ommitted
n14little May 14, 2020
c94cb36
add test for array of numbers (works with existing implementation)
n14little May 15, 2020
32f4e39
WIP: to_json returns undefined to null
n14little May 15, 2020
5756eec
fix rust format
n14little May 18, 2020
cd66600
add test to remove symbol values from objects
n14little May 18, 2020
a14aa73
ignore json stringify tests with symbols (there is a bug)
n14little May 18, 2020
d17a455
handle json stringification for arrays
n14little May 19, 2020
d58ad96
exit early
n14little May 19, 2020
8538005
refactor JSON.stringify function
n14little May 19, 2020
0320e17
add test that needs to be addressed once symbols are fixed
n14little May 19, 2020
30b0bf9
refactor JSON.stringify
n14little May 20, 2020
0eba162
use option in function replacer
n14little May 20, 2020
1399110
remove some comments and unused imports
n14little May 20, 2020
44ed70f
address PR comments
n14little May 23, 2020
235d629
use new get_field
n14little May 25, 2020
fadb75c
use filter map
n14little May 26, 2020
600e560
propogate errors
n14little May 26, 2020
19e8632
format
n14little May 26, 2020
c39ba92
assert that something is thrown
n14little May 26, 2020
766c7dc
return undefined when no argument passed in to JSON.stringify
n14little May 28, 2020
0c9b795
use pattern matching to get replacer / exit early
n14little May 28, 2020
f25778a
use if let instead of match
n14little May 29, 2020
d39a397
cover other conditions where JSON.stringify should return undefined
n14little May 29, 2020
f6cdd1c
run formatter
n14little May 29, 2020
a61a567
mark code as unreachable
n14little May 29, 2020
5a32e02
use option mapping over if let
n14little May 30, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 62 additions & 4 deletions boa/src/builtins/json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

use crate::builtins::{
function::make_builtin_fn,
object::ObjectKind,
property::Property,
value::{ResultValue, Value},
};
use crate::exec::Interpreter;
Expand Down Expand Up @@ -65,10 +67,66 @@ pub fn parse(_: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue
///
/// [spec]: https://tc39.es/ecma262/#sec-json.stringify
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
pub fn stringify(_: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
let obj = args.get(0).expect("cannot get argument for JSON.stringify");
let json = obj.to_json().to_string();
Ok(Value::from(json))
pub fn stringify(_: &mut Value, args: &[Value], interpreter: &mut Interpreter) -> ResultValue {
let object = match args.get(0) {
Razican marked this conversation as resolved.
Show resolved Hide resolved
Some(obj) if obj.is_symbol() || obj.is_function() => return Ok(Value::undefined()),
None => return Ok(Value::undefined()),
Some(obj) => obj,
};
let replacer = match args.get(1) {
Some(replacer) if replacer.is_object() => replacer,
_ => return Ok(Value::from(object.to_json().to_string())),
};

let replacer_as_object = replacer
.as_object()
.expect("JSON.stringify replacer was an object");
if replacer_as_object.is_callable() {
object
.as_object()
.map(|obj| {
let object_to_return = Value::new_object(None);
for (key, val) in obj
.properties
.iter()
.filter_map(|(k, v)| v.value.as_ref().map(|value| (k, value)))
{
let mut this_arg = object.clone();
object_to_return.set_property(
key.to_owned(),
Property::default().value(interpreter.call(
replacer,
&mut this_arg,
&[Value::string(key), val.clone()],
)?),
);
}
Ok(Value::from(object_to_return.to_json().to_string()))
})
.ok_or_else(Value::undefined)?
} else if replacer_as_object.kind == ObjectKind::Array {
let mut obj_to_return =
serde_json::Map::with_capacity(replacer_as_object.properties.len() - 1);
let fields = replacer_as_object.properties.keys().filter_map(|key| {
if key == "length" {
None
} else {
Some(replacer.get_field(key.to_string()))
}
});
for field in fields {
if let Some(value) = object
.get_property(&field.to_string())
.map(|prop| prop.value.as_ref().map(|v| v.to_json()))
.flatten()
{
obj_to_return.insert(field.to_string(), value);
}
}
Ok(Value::from(JSONValue::Object(obj_to_return).to_string()))
} else {
Ok(Value::from(object.to_json().to_string()))
}
}

/// Create a new `JSON` object.
Expand Down
173 changes: 173 additions & 0 deletions boa/src/builtins/json/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,176 @@ fn json_sanity() {
"true"
);
}

#[test]
fn json_stringify_remove_undefined_values_from_objects() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);

let actual = forward(
&mut engine,
r#"JSON.stringify({ aaa: undefined, bbb: 'ccc' })"#,
);
let expected = r#"{"bbb":"ccc"}"#;

assert_eq!(actual, expected);
}

#[test]
fn json_stringify_remove_function_values_from_objects() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);

let actual = forward(
&mut engine,
r#"JSON.stringify({ aaa: () => {}, bbb: 'ccc' })"#,
);
let expected = r#"{"bbb":"ccc"}"#;

assert_eq!(actual, expected);
}

#[test]
#[ignore]
// there is a bug for setting a symbol as a field's value
fn json_stringify_remove_symbols_from_objects() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);

let actual = forward(
&mut engine,
r#"JSON.stringify({ aaa: Symbol(), bbb: 'ccc' })"#,
);
let expected = r#"{"bbb":"ccc"}"#;

assert_eq!(actual, expected);
}

#[test]
fn json_stringify_replacer_array_strings() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let actual = forward(
&mut engine,
r#"JSON.stringify({aaa: 'bbb', bbb: 'ccc', ccc: 'ddd'}, ['aaa', 'bbb'])"#,
);
let expected = forward(&mut engine, r#"'{"aaa":"bbb","bbb":"ccc"}'"#);
assert_eq!(actual, expected);
}

#[test]
fn json_stringify_replacer_array_numbers() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let actual = forward(
&mut engine,
r#"JSON.stringify({ 0: 'aaa', 1: 'bbb', 2: 'ccc'}, [1, 2])"#,
);
let expected = forward(&mut engine, r#"'{"1":"bbb","2":"ccc"}'"#);
assert_eq!(actual, expected);
}

#[test]
fn json_stringify_replacer_function() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let actual = forward(
&mut engine,
r#"JSON.stringify({ aaa: 1, bbb: 2}, (key, value) => {
if (key === 'aaa') {
return undefined;
}

return value;
})"#,
);
let expected = forward(&mut engine, r#"'{"bbb":2}'"#);
assert_eq!(actual, expected);
}

#[test]
fn json_stringify_arrays() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let actual = forward(&mut engine, r#"JSON.stringify(['a', 'b'])"#);
let expected = forward(&mut engine, r#"'["a","b"]'"#);

assert_eq!(actual, expected);
}

#[test]
fn json_stringify_object_array() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let actual = forward(&mut engine, r#"JSON.stringify([{a: 'b'}, {b: 'c'}])"#);
let expected = forward(&mut engine, r#"'[{"a":"b"},{"b":"c"}]'"#);

assert_eq!(actual, expected);
}

#[test]
fn json_stringify_array_converts_undefined_to_null() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let actual = forward(&mut engine, r#"JSON.stringify([undefined])"#);
let expected = forward(&mut engine, r#"'[null]'"#);

assert_eq!(actual, expected);
}

#[test]
fn json_stringify_array_converts_function_to_null() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let actual = forward(&mut engine, r#"JSON.stringify([() => {}])"#);
let expected = forward(&mut engine, r#"'[null]'"#);

assert_eq!(actual, expected);
}

#[test]
#[ignore]
fn json_stringify_array_converts_symbol_to_null() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let actual = forward(&mut engine, r#"JSON.stringify([Symbol()])"#);
let expected = forward(&mut engine, r#"'[null]'"#);

assert_eq!(actual, expected);
}
#[test]
fn json_stringify_function_replacer_propogate_error() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);

let actual = forward(
&mut engine,
r#"
let thrown = 0;
try {
JSON.stringify({x: 1}, (key, value) => { throw 1 })
} catch (err) {
thrown = err;
}
thrown
"#,
);
let expected = forward(&mut engine, r#"1"#);

assert_eq!(actual, expected);
}
n14little marked this conversation as resolved.
Show resolved Hide resolved

#[test]
fn json_stringify_return_undefined() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);

let actual_no_args = forward(&mut engine, r#"JSON.stringify()"#);
let actual_function = forward(&mut engine, r#"JSON.stringify(() => {})"#);
let actual_symbol = forward(&mut engine, r#"JSON.stringify(Symbol())"#);
let expected = forward(&mut engine, r#"undefined"#);

assert_eq!(actual_no_args, expected);
assert_eq!(actual_function, expected);
assert_eq!(actual_symbol, expected);
}
36 changes: 28 additions & 8 deletions boa/src/builtins/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,16 +717,33 @@ impl ValueData {
/// Conversts the `Value` to `JSON`.
pub fn to_json(&self) -> JSONValue {
match *self {
Self::Null | Self::Symbol(_) | Self::Undefined => JSONValue::Null,
Self::Null => JSONValue::Null,
Self::Boolean(b) => JSONValue::Bool(b),
Self::Object(ref obj) => {
let new_obj = obj
.borrow()
.properties
.iter()
.map(|(k, _)| (k.clone(), self.get_field(k.as_str()).to_json()))
.collect::<Map<String, JSONValue>>();
JSONValue::Object(new_obj)
if obj.borrow().kind == ObjectKind::Array {
let mut arr: Vec<JSONValue> = Vec::new();
obj.borrow().properties.keys().for_each(|k| {
if k != "length" {
let value = self.get_field(k.to_string());
if value.is_undefined() || value.is_function() {
arr.push(JSONValue::Null);
} else {
arr.push(self.get_field(k.to_string()).to_json());
}
}
});
JSONValue::Array(arr)
} else {
let mut new_obj = Map::new();
obj.borrow().properties.keys().for_each(|k| {
let key = k.clone();
let value = self.get_field(k.to_string());
if !value.is_undefined() && !value.is_function() {
new_obj.insert(key, value.to_json());
}
});
JSONValue::Object(new_obj)
}
}
Self::String(ref str) => JSONValue::String(str.clone()),
Self::Rational(num) => JSONValue::Number(
Expand All @@ -737,6 +754,9 @@ impl ValueData {
// TODO: throw TypeError
panic!("TypeError: \"BigInt value can't be serialized in JSON\"");
}
Self::Symbol(_) | Self::Undefined => {
unreachable!("Symbols and Undefined JSON Values depend on parent type");
}
}
}

Expand Down