Skip to content

Commit 2428f7a

Browse files
Fix nested object replacer
1 parent f859364 commit 2428f7a

File tree

3 files changed

+112
-92
lines changed

3 files changed

+112
-92
lines changed

libs/llrt_json/benches/json.rs

+53-51
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,68 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
#![allow(dead_code)]
5-
use criterion::{black_box, criterion_group, criterion_main, Criterion};
5+
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
66
use llrt_json::{parse::json_parse, stringify::json_stringify};
77
use rquickjs::{Context, Runtime};
88

99
static JSON: &str = r#"{"organization":{"name":"TechCorp","founding_year":2000,"departments":[{"name":"Engineering","head":{"name":"Alice Smith","title":"VP of Engineering","contact":{"email":"alice.smith@techcorp.com","phone":"+1 (555) 123-4567"}},"employees":[{"id":101,"name":"Bob Johnson","position":"Software Engineer","contact":{"email":"bob.johnson@techcorp.com","phone":"+1 (555) 234-5678"},"projects":[{"project_id":"P001","name":"Project A","status":"In Progress","description":"Developing a revolutionary software solution for clients.","start_date":"2023-01-15","end_date":null,"team":[{"id":201,"name":"Sara Davis","role":"UI/UX Designer"},{"id":202,"name":"Charlie Brown","role":"Quality Assurance Engineer"}]},{"project_id":"P002","name":"Project B","status":"Completed","description":"Upgrading existing systems to enhance performance.","start_date":"2022-05-01","end_date":"2022-11-30","team":[{"id":203,"name":"Emily White","role":"Systems Architect"},{"id":204,"name":"James Green","role":"Database Administrator"}]}]},{"id":102,"name":"Carol Williams","position":"Senior Software Engineer","contact":{"email":"carol.williams@techcorp.com","phone":"+1 (555) 345-6789"},"projects":[{"project_id":"P001","name":"Project A","status":"In Progress","description":"Working on the backend development of Project A.","start_date":"2023-01-15","end_date":null,"team":[{"id":205,"name":"Alex Turner","role":"DevOps Engineer"},{"id":206,"name":"Mia Garcia","role":"Software Developer"}]},{"project_id":"P003","name":"Project C","status":"Planning","description":"Researching and planning for a future project.","start_date":null,"end_date":null,"team":[]}]}]},{"name":"Marketing","head":{"name":"David Brown","title":"VP of Marketing","contact":{"email":"david.brown@techcorp.com","phone":"+1 (555) 456-7890"}},"employees":[{"id":201,"name":"Eva Miller","position":"Marketing Specialist","contact":{"email":"eva.miller@techcorp.com","phone":"+1 (555) 567-8901"},"campaigns":[{"campaign_id":"C001","name":"Product Launch","status":"Upcoming","description":"Planning for the launch of a new product line.","start_date":"2023-03-01","end_date":null,"team":[{"id":301,"name":"Oliver Martinez","role":"Graphic Designer"},{"id":302,"name":"Sophie Johnson","role":"Content Writer"}]},{"campaign_id":"C002","name":"Brand Awareness","status":"Ongoing","description":"Executing strategies to increase brand visibility.","start_date":"2022-11-15","end_date":"2023-01-31","team":[{"id":303,"name":"Liam Taylor","role":"Social Media Manager"},{"id":304,"name":"Ava Clark","role":"Marketing Analyst"}]}]}]}]}}"#;
1010

1111
static JSON_MIN: &str = r#"{"glossary":{"title":"example glossary","GlossDiv":{"title":"S","GlossList":{"GlossEntry":{"ID":"SGML","SortAs":"SGML","GlossTerm":"Standard Generalized Markup Language","Acronym":"SGML","Abbrev":"ISO 8879:1986","GlossDef":{"para":"A meta-markup language, used to create markup languages such as DocBook.","GlossSeeAlso":["GML","XML"]},"GlossSee":"markup"}}}}}"#;
1212

13-
// fn generate_json(child_json: &str, size: usize) -> String {
14-
// let mut json = String::with_capacity(child_json.len() * size);
15-
// json.push('{');
16-
// for i in 0..size {
17-
// json.push_str("\"obj");
18-
// json.push_str(&i.to_string());
19-
// json.push_str("\":");
20-
// json.push_str(child_json);
21-
// json.push(',');
22-
// }
23-
// json.push_str("\"array\":[");
24-
// for i in 0..size {
25-
// json.push_str(child_json);
26-
// if i < size - 1 {
27-
// json.push(',');
28-
// }
29-
// }
30-
31-
// json.push_str("]}");
32-
// json
33-
// }
13+
fn generate_json(child_json: &str, size: usize) -> String {
14+
let mut json = String::with_capacity(child_json.len() * size);
15+
json.push('{');
16+
for i in 0..size {
17+
json.push_str("\"obj");
18+
json.push_str(&i.to_string());
19+
json.push_str("\":");
20+
json.push_str(child_json);
21+
json.push(',');
22+
}
23+
json.push_str("\"array\":[");
24+
for i in 0..size {
25+
json.push_str(child_json);
26+
if i < size - 1 {
27+
json.push(',');
28+
}
29+
}
30+
31+
json.push_str("]}");
32+
json
33+
}
3434

3535
pub fn criterion_benchmark(c: &mut Criterion) {
36-
// let mut group = c.benchmark_group("Parsing");
37-
38-
// let json = JSON.to_owned();
39-
// for (id, json) in [
40-
// json.clone(),
41-
// generate_json(&json, 1),
42-
// generate_json(&json, 10),
43-
// generate_json(&json, 100),
44-
// ]
45-
// .into_iter()
46-
// .enumerate()
47-
// {
48-
// let runtime = Runtime::new().unwrap();
49-
// runtime.set_max_stack_size(512 * 1024);
50-
51-
// let ctx = Context::full(&runtime).unwrap();
52-
// group.bench_with_input(BenchmarkId::new("Custom", id), &json, |b, json| {
53-
// ctx.with(|ctx| {
54-
// b.iter(|| json_parse(&ctx, json.clone()));
55-
// });
56-
// });
57-
// group.bench_with_input(BenchmarkId::new("Built-in", id), &json, |b, json| {
58-
// ctx.with(|ctx| {
59-
// b.iter(|| ctx.json_parse(json.clone()));
60-
// });
61-
// });
62-
// }
63-
64-
// group.finish();
36+
let mut group = c.benchmark_group("Parsing");
37+
38+
let json = JSON.to_owned();
39+
for (id, json) in [
40+
json.clone(),
41+
generate_json(&json, 1),
42+
generate_json(&json, 10),
43+
generate_json(&json, 100),
44+
generate_json(&json, 1000),
45+
generate_json(&json, 10000),
46+
]
47+
.into_iter()
48+
.enumerate()
49+
{
50+
let runtime = Runtime::new().unwrap();
51+
runtime.set_max_stack_size(512 * 1024);
52+
53+
let ctx = Context::full(&runtime).unwrap();
54+
group.bench_with_input(BenchmarkId::new("Custom", id), &json, |b, json| {
55+
ctx.with(|ctx| {
56+
b.iter(|| json_parse(&ctx, json.clone()));
57+
});
58+
});
59+
group.bench_with_input(BenchmarkId::new("Built-in", id), &json, |b, json| {
60+
ctx.with(|ctx| {
61+
b.iter(|| ctx.json_parse(json.clone()));
62+
});
63+
});
64+
}
65+
66+
group.finish();
6567

6668
c.bench_function("json parse", |b| {
6769
let runtime = Runtime::new().unwrap();

libs/llrt_json/src/stringify.rs

+38-40
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ pub fn json_stringify_replacer_space<'js>(
108108

109109
context.depth += 1;
110110
context.indentation = indentation;
111-
iterate(&mut context)?;
111+
iterate(&mut context, None)?;
112112
Ok(Some(result))
113113
}
114114

@@ -157,10 +157,10 @@ fn run_to_json<'js>(
157157
}
158158

159159
#[derive(PartialEq)]
160-
enum PrimitiveStatus {
160+
enum PrimitiveStatus<'js> {
161161
Written,
162162
Ignored,
163-
Iterate,
163+
Iterate(Option<Value<'js>>),
164164
}
165165

166166
#[inline(always)]
@@ -169,62 +169,57 @@ fn run_replacer<'js>(
169169
context: &mut StringifyContext<'_, 'js>,
170170
replacer_fn: &Function<'js>,
171171
add_comma: bool,
172-
) -> Result<PrimitiveStatus> {
173-
let parent = context.parent;
174-
let ctx = context.ctx;
175-
let value = context.value;
172+
) -> Result<PrimitiveStatus<'js>> {
176173
let key = context.key;
177174
let index = context.index;
178-
let parent = if let Some(parent) = parent {
175+
let value = context.value;
176+
let parent = if let Some(parent) = context.parent {
179177
parent.clone()
180178
} else {
181-
let parent = Object::new(ctx.clone())?;
179+
let parent = Object::new(context.ctx.clone())?;
182180
parent.set("", value.clone())?;
183181
parent
184182
};
185-
let new_value = replacer_fn.call((
183+
let new_value: Value = replacer_fn.call((
186184
This(parent),
187185
get_key_or_index(context.itoa_buffer, key, index),
188186
value,
189187
))?;
190-
write_primitive(
191-
&mut StringifyContext {
192-
ctx,
193-
result: context.result,
194-
value: &new_value,
195-
replacer_fn: None,
196-
key,
197-
index: None,
198-
indentation: context.indentation,
199-
parent: None,
200-
include_keys_replacer: None,
201-
depth: context.depth,
202-
ancestors: context.ancestors,
203-
itoa_buffer: context.itoa_buffer,
204-
ryu_buffer: context.ryu_buffer,
205-
},
206-
add_comma,
207-
)
188+
189+
return write_primitive2(context, add_comma, Some(new_value));
208190
}
209191

210-
fn write_primitive(context: &mut StringifyContext, add_comma: bool) -> Result<PrimitiveStatus> {
192+
fn write_primitive<'a, 'js>(
193+
context: &mut StringifyContext<'a, 'js>,
194+
add_comma: bool,
195+
) -> Result<PrimitiveStatus<'js>> {
211196
if let Some(replacer_fn) = context.replacer_fn {
212197
return run_replacer(context, replacer_fn, add_comma);
213198
}
214199

215-
let include_keys_replacer = context.include_keys_replacer;
216-
let value = context.value;
200+
write_primitive2(context, add_comma, None)
201+
}
202+
203+
fn write_primitive2<'a, 'js>(
204+
context: &mut StringifyContext<'a, 'js>,
205+
add_comma: bool,
206+
new_value: Option<Value<'js>>,
207+
) -> Result<PrimitiveStatus<'js>> {
217208
let key = context.key;
218209
let index = context.index;
210+
let include_keys_replacer = context.include_keys_replacer;
219211
let indentation = context.indentation;
220212
let depth = context.depth;
221213

214+
let value = new_value.as_ref().unwrap_or(context.value);
215+
222216
let type_of = value.type_of();
223217

224-
if matches!(
225-
type_of,
226-
Type::Symbol | Type::Undefined | Type::Function | Type::Constructor
227-
) && context.index.is_none()
218+
if context.index.is_none()
219+
&& matches!(
220+
type_of,
221+
Type::Symbol | Type::Undefined | Type::Function | Type::Constructor
222+
)
228223
{
229224
return Ok(PrimitiveStatus::Ignored);
230225
}
@@ -289,7 +284,7 @@ fn write_primitive(context: &mut StringifyContext, add_comma: bool) -> Result<Pr
289284
context.result,
290285
&unsafe { value.as_string().unwrap_unchecked() }.to_string()?,
291286
),
292-
_ => return Ok(PrimitiveStatus::Iterate),
287+
_ => return Ok(PrimitiveStatus::Iterate(new_value)),
293288
}
294289
Ok(PrimitiveStatus::Written)
295290
}
@@ -374,9 +369,9 @@ fn append_value(context: &mut StringifyContext<'_, '_>, add_comma: bool) -> Resu
374369
match write_primitive(context, add_comma)? {
375370
PrimitiveStatus::Written => Ok(true),
376371
PrimitiveStatus::Ignored => Ok(false),
377-
PrimitiveStatus::Iterate => {
372+
PrimitiveStatus::Iterate(new_value) => {
378373
context.depth += 1;
379-
iterate(context)?;
374+
iterate(context, new_value)?;
380375
Ok(true)
381376
},
382377
}
@@ -419,10 +414,13 @@ fn get_key_or_index<'a>(
419414
key.unwrap_or_else(|| itoa_buffer.format(index.unwrap_or_default()))
420415
}
421416

422-
fn iterate(context: &mut StringifyContext<'_, '_>) -> Result<()> {
417+
fn iterate<'a, 'js>(
418+
context: &mut StringifyContext<'a, 'js>,
419+
new_value: Option<Value<'js>>,
420+
) -> Result<()> {
423421
let mut add_comma;
424422
let mut value_written;
425-
let elem = context.value;
423+
let elem = new_value.as_ref().unwrap_or(context.value);
426424
let depth = context.depth;
427425
let ctx = context.ctx;
428426
let indentation = context.indentation;

tests/unit/json.test.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ describe("JSON Stringified", () => {
299299
});
300300

301301
it("should stringify and remove objects that are not valid json", () => {
302-
const now = new Date()
302+
const now = new Date();
303303
const dateString = now.toJSON();
304304
const data = {
305305
a: "123",
@@ -327,4 +327,24 @@ describe("JSON Stringified", () => {
327327
})
328328
).toThrow(/Do not know how to serialize a BigInt/);
329329
});
330+
331+
it("should allow replacer that returns new non-primitive objects", () => {
332+
const json = JSON.stringify({ key: "value" });
333+
334+
const obj = {
335+
simple: "text",
336+
nested: json,
337+
};
338+
339+
const replacer = (key: any, value: any) => {
340+
try {
341+
return typeof value === "string" ? JSON.parse(value) : value;
342+
} catch {
343+
return value;
344+
}
345+
};
346+
347+
const result = JSON.stringify(obj, replacer);
348+
expect(result).toEqual('{"simple":"text","nested":{"key":"value"}}');
349+
});
330350
});

0 commit comments

Comments
 (0)