1use std::cmp::min;
4
5use rquickjs::{
6 atom::PredefinedAtom, function::Opt, prelude::Func, Ctx, IntoJs, Object, Result, Value,
7};
8
9pub mod escape;
10pub mod parse;
11pub mod stringify;
12
13use crate::json::parse::json_parse_string;
14use crate::json::stringify::json_stringify_replacer_space;
15
16pub fn redefine_static_methods(ctx: &Ctx<'_>) -> Result<()> {
17 let globals = ctx.globals();
18 let json_module: Object = globals.get(PredefinedAtom::JSON)?;
19 json_module.set("parse", Func::from(json_parse_string))?;
20 json_module.set(
21 "stringify",
22 Func::from(|ctx, value, replacer, space| {
23 struct StringifyArgs<'js>(Ctx<'js>, Value<'js>, Opt<Value<'js>>, Opt<Value<'js>>);
24 let StringifyArgs(ctx, value, replacer, space) =
25 StringifyArgs(ctx, value, replacer, space);
26
27 let mut space_value = None;
28 let mut replacer_value = None;
29
30 if let Some(replacer) = replacer.0 {
31 if let Some(space) = space.0 {
32 if let Some(space) = space.as_string() {
33 let mut space = space.clone().to_string()?;
34 space.truncate(20);
35 space_value = Some(space);
36 }
37 if let Some(number) = space.as_int() {
38 if number > 0 {
39 space_value = Some(" ".repeat(min(10, number as usize)));
40 }
41 }
42 }
43 replacer_value = Some(replacer);
44 }
45
46 json_stringify_replacer_space(&ctx, value, replacer_value, space_value)
47 .map(|v| v.into_js(&ctx))?
48 }),
49 )?;
50 Ok(())
51}
52
53#[cfg(test)]
54mod tests {
55 use crate::test::test_sync_with;
56 use rquickjs::{prelude::Func, Array, CatchResultExt, IntoJs, Null, Object, Undefined, Value};
57
58 use crate::json::{
59 parse::{json_parse, json_parse_string},
60 stringify::{json_stringify, json_stringify_replacer_space},
61 };
62
63 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"}]}]}]}]}}"#;
64
65 #[tokio::test]
66 async fn json_parser() {
67 test_sync_with(|ctx| {
68 let json_data = [
69 r#"{"aa\"\"aaaaaaaaaaaaaaaa":"a","b":"bbb"}"#,
70 r#"{"a":"aaaaaaaaaaaaaaaaaa","b":"bbb"}"#,
71 r#"{"a":["a","a","aaaa","a"],"b":"b"}"#,
72 r#"{"type":"Buffer","data":[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10]}"#,
73 r#"{"a":[{"object2":{"key1":"value1","key2":123,"key3":false,"nestedObject":{"nestedKey":"nestedValue"}},"string":"Hello, World!","emptyObj":{},"emptyArr":[],"number":42,"boolean":true,"nullValue":null,"array":[1,2,3,"four",5.5,true,null],"object":{"key1":"value1","key2":123,"key3":false,"nestedObject":{"nestedKey":"nestedValue"}}}]}"#,
74 JSON,
75 ];
76
77 for json_str in json_data {
78 let json = json_str.to_string();
79 let json2 = json.clone();
80
81 let value = json_parse(&ctx, json2)?;
82 let new_json = json_stringify_replacer_space(&ctx, value.clone(),None,Some(" ".into()))?.unwrap();
83 let builtin_json = ctx.json_stringify_replacer_space(value,Null," ".to_string())?.unwrap().to_string()?;
84 assert_eq!(new_json, builtin_json);
85 }
86
87 Ok(())
88 })
89 .await;
90 }
91
92 #[tokio::test]
93 async fn json_parse_non_string() {
94 test_sync_with(|ctx| {
95 ctx.globals().set("parse", Func::from(json_parse_string))?;
96
97 let result = ctx.eval::<(), _>("parse({})").catch(&ctx);
98
99 if let Err(err) = result {
100 assert_eq!(
101 err.to_string(),
102 "Error: \"[object Object]\" not valid JSON at index 1 ('o')\n at <eval> (eval_script:1:1)\n"
103 );
104 } else {
105 panic!("expected error")
106 }
107
108 Ok(())
109 })
110 .await;
111 }
112
113 #[tokio::test]
114 async fn json_stringify_undefined() {
115 test_sync_with(|ctx| {
116 let stringified = json_stringify(&ctx, Undefined.into_js(&ctx)?)?;
117 let stringified_2 = ctx
118 .json_stringify(Undefined)?
119 .map(|v| v.to_string().unwrap());
120 assert_eq!(stringified, stringified_2);
121
122 let obj: Value = ctx.eval(
123 r#"let obj = { value: undefined, array: [undefined, null, 1, true, "hello", { [Symbol("sym")]: 1, [undefined]: 2}] };obj;"#,
124 )?;
125
126 let stringified = json_stringify(&ctx, obj.clone())?;
127 let stringified_2 = ctx
128 .json_stringify(obj)?
129 .map(|v| v.to_string().unwrap());
130 assert_eq!(stringified, stringified_2);
131
132 Ok(())
133 })
134 .await;
135 }
136
137 #[tokio::test]
138 async fn json_stringify_objects() {
139 test_sync_with(|ctx| {
140 let date: Value = ctx.eval("let obj = { date: new Date(0) };obj;")?;
141 let stringified = json_stringify(&ctx, date.clone())?.unwrap();
142 let stringified_2 = ctx.json_stringify(date)?.unwrap().to_string()?;
143 assert_eq!(stringified, stringified_2);
144 Ok(())
145 })
146 .await;
147 }
148
149 #[tokio::test]
150 async fn huge_numbers() {
151 test_sync_with(|ctx| {
152
153 let big_int_value = json_parse(&ctx, b"99999999999999999999999999999999999999999999999999999999999999999999999999999999999")?;
154
155 let stringified = json_stringify(&ctx, big_int_value.clone())?.unwrap();
156 let stringified_2 = ctx.json_stringify(big_int_value)?.unwrap().to_string()?.replace("e+", "e");
157 assert_eq!(stringified, stringified_2);
158
159 let big_int_value: Value = ctx.eval("999999999999")?;
160 let stringified = json_stringify(&ctx, big_int_value.clone())?.unwrap();
161 let stringified_2 = ctx.json_stringify(big_int_value)?.unwrap().to_string()?;
162 assert_eq!(stringified, stringified_2);
163
164 Ok(())
165 })
166 .await;
167 }
168
169 #[tokio::test]
170 async fn json_circular_ref() {
171 test_sync_with(|ctx| {
172 let obj1 = Object::new(ctx.clone())?;
173 let obj2 = Object::new(ctx.clone())?;
174 let obj3 = Object::new(ctx.clone())?;
175 let obj4 = Object::new(ctx.clone())?;
176 obj4.set("key", "value")?;
177 obj3.set("sub2", obj4.clone())?;
178 obj2.set("sub1", obj3)?;
179 obj1.set("root1", obj2.clone())?;
180 obj1.set("root2", obj2.clone())?;
181 obj1.set("root3", obj2.clone())?;
182
183 let value = obj1.clone().into_value();
184
185 let stringified = json_stringify(&ctx, value.clone())?.unwrap();
186 let stringified_2 = ctx.json_stringify(value.clone())?.unwrap().to_string()?;
187 assert_eq!(stringified, stringified_2);
188
189 obj4.set("recursive", obj1.clone())?;
190
191 let stringified = json_stringify(&ctx, value.clone());
192
193 if let Err(error_message) = stringified.catch(&ctx) {
194 let error_str = error_message.to_string();
195 assert_eq!(
196 "Error: Circular reference detected at: \"...root1.sub1.sub2.recursive\"\n",
197 error_str
198 )
199 } else {
200 panic!("Expected an error, but got Ok");
201 }
202
203 let array1 = Array::new(ctx.clone())?;
204 let array2 = Array::new(ctx.clone())?;
205 let array3 = Array::new(ctx.clone())?;
206
207 let obj5 = Object::new(ctx.clone())?;
208 obj5.set("key", obj1.clone())?;
209 array3.set(2, obj5)?;
210 array2.set(1, array3)?;
211 array1.set(0, array2)?;
212
213 obj4.remove("recursive")?;
214 obj1.set("recursiveArray", array1)?;
215
216 let stringified = json_stringify(&ctx, value.clone());
217
218 if let Err(error_message) = stringified.catch(&ctx) {
219 let error_str = error_message.to_string();
220 assert_eq!(
221 "Error: Circular reference detected at: \"...recursiveArray[0][1][2].key\"\n",
222 error_str
223 )
224 } else {
225 panic!("Expected an error, but got Ok");
226 }
227
228 Ok(())
229 })
230 .await;
231 }
232}