1use std::fmt::Write;
2
3use crate::JsonValue;
4
5#[must_use]
8pub fn json_stringify_lines(value: &JsonValue, indent: usize) -> Vec<String> {
9 let estimated_size = estimate_json_size(value, indent);
11 let mut buf = String::with_capacity(estimated_size);
12 stringify_value_to_buf(value, 0, indent, &mut buf);
13 vec![buf]
14}
15
16fn estimate_json_size(value: &JsonValue, indent: usize) -> usize {
18 match value {
19 JsonValue::Primitive(p) => match p {
20 crate::StringOrNumberOrBoolOrNull::Null => 4,
21 crate::StringOrNumberOrBoolOrNull::Bool(_) => 5,
22 crate::StringOrNumberOrBoolOrNull::Number(_) => 20,
23 crate::StringOrNumberOrBoolOrNull::String(s) => s.len() + 10,
24 },
25 JsonValue::Array(items) => {
26 let base = items
27 .iter()
28 .map(|v| estimate_json_size(v, indent))
29 .sum::<usize>();
30 base + items.len() * (2 + indent) + 4
31 }
32 JsonValue::Object(entries) => {
33 let base: usize = entries
34 .iter()
35 .map(|(k, v)| k.len() + 4 + estimate_json_size(v, indent))
36 .sum();
37 base + entries.len() * (2 + indent) + 4
38 }
39 }
40}
41
42fn stringify_value_to_buf(value: &JsonValue, depth: usize, indent: usize, buf: &mut String) {
43 match value {
44 JsonValue::Primitive(primitive) => {
45 stringify_primitive_to_buf(primitive, buf);
46 }
47 JsonValue::Array(values) => stringify_array_to_buf(values, depth, indent, buf),
48 JsonValue::Object(entries) => stringify_object_to_buf(entries, depth, indent, buf),
49 }
50}
51
52fn stringify_array_to_buf(values: &[JsonValue], depth: usize, indent: usize, buf: &mut String) {
53 if values.is_empty() {
54 buf.push_str("[]");
55 return;
56 }
57
58 buf.push('[');
59
60 if indent > 0 {
61 for (idx, value) in values.iter().enumerate() {
62 buf.push('\n');
63 push_indent(buf, (depth + 1) * indent);
64 stringify_value_to_buf(value, depth + 1, indent, buf);
65 if idx + 1 < values.len() {
66 buf.push(',');
67 }
68 }
69 buf.push('\n');
70 push_indent(buf, depth * indent);
71 } else {
72 for (idx, value) in values.iter().enumerate() {
73 stringify_value_to_buf(value, depth + 1, indent, buf);
74 if idx + 1 < values.len() {
75 buf.push(',');
76 }
77 }
78 }
79 buf.push(']');
80}
81
82fn stringify_object_to_buf(
83 entries: &[(String, JsonValue)],
84 depth: usize,
85 indent: usize,
86 buf: &mut String,
87) {
88 if entries.is_empty() {
89 buf.push_str("{}");
90 return;
91 }
92
93 buf.push('{');
94
95 if indent > 0 {
96 for (idx, (key, value)) in entries.iter().enumerate() {
97 buf.push('\n');
98 push_indent(buf, (depth + 1) * indent);
99 push_json_string(buf, key);
101 buf.push_str(": ");
102 stringify_value_to_buf(value, depth + 1, indent, buf);
103 if idx + 1 < entries.len() {
104 buf.push(',');
105 }
106 }
107 buf.push('\n');
108 push_indent(buf, depth * indent);
109 } else {
110 for (idx, (key, value)) in entries.iter().enumerate() {
111 push_json_string(buf, key);
112 buf.push(':');
113 stringify_value_to_buf(value, depth + 1, indent, buf);
114 if idx + 1 < entries.len() {
115 buf.push(',');
116 }
117 }
118 }
119 buf.push('}');
120}
121
122fn stringify_primitive_to_buf(value: &crate::JsonPrimitive, buf: &mut String) {
123 match value {
124 crate::StringOrNumberOrBoolOrNull::Null => buf.push_str("null"),
125 crate::StringOrNumberOrBoolOrNull::Bool(true) => buf.push_str("true"),
126 crate::StringOrNumberOrBoolOrNull::Bool(false) => buf.push_str("false"),
127 crate::StringOrNumberOrBoolOrNull::Number(n) => {
128 if let Some(num) = serde_json::Number::from_f64(*n) {
129 buf.push_str(&num.to_string());
130 } else {
131 buf.push_str("null");
132 }
133 }
134 crate::StringOrNumberOrBoolOrNull::String(s) => {
135 push_json_string(buf, s);
136 }
137 }
138}
139
140#[inline]
142fn push_indent(buf: &mut String, count: usize) {
143 for _ in 0..count {
144 buf.push(' ');
145 }
146}
147
148fn push_json_string(buf: &mut String, s: &str) {
150 buf.push('"');
151 for c in s.chars() {
152 match c {
153 '"' => buf.push_str("\\\""),
154 '\\' => buf.push_str("\\\\"),
155 '\n' => buf.push_str("\\n"),
156 '\r' => buf.push_str("\\r"),
157 '\t' => buf.push_str("\\t"),
158 c if c.is_control() => {
159 let _ = write!(buf, "\\u{:04x}", c as u32);
161 }
162 c => buf.push(c),
163 }
164 }
165 buf.push('"');
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::StringOrNumberOrBoolOrNull;
172
173 fn stringify(value: &JsonValue, indent: usize) -> String {
174 let lines = json_stringify_lines(value, indent);
175 assert_eq!(lines.len(), 1);
176 lines.into_iter().next().unwrap()
177 }
178
179 fn s(v: &str) -> JsonValue {
180 JsonValue::Primitive(StringOrNumberOrBoolOrNull::String(v.to_string()))
181 }
182
183 fn n(v: f64) -> JsonValue {
184 JsonValue::Primitive(StringOrNumberOrBoolOrNull::Number(v))
185 }
186
187 fn b(v: bool) -> JsonValue {
188 JsonValue::Primitive(StringOrNumberOrBoolOrNull::Bool(v))
189 }
190
191 fn null() -> JsonValue {
192 JsonValue::Primitive(StringOrNumberOrBoolOrNull::Null)
193 }
194
195 #[test]
196 fn primitive_null() {
197 assert_eq!(stringify(&null(), 0), "null");
198 }
199
200 #[test]
201 fn primitive_booleans() {
202 assert_eq!(stringify(&b(true), 0), "true");
203 assert_eq!(stringify(&b(false), 0), "false");
204 }
205
206 #[test]
207 fn primitive_number_integer_like() {
208 assert_eq!(stringify(&n(42.0), 0), "42.0");
209 }
210
211 #[test]
212 fn primitive_number_zero() {
213 assert_eq!(stringify(&n(0.0), 0), "0.0");
214 }
215
216 #[test]
217 fn primitive_number_nan_becomes_null() {
218 assert_eq!(stringify(&n(f64::NAN), 0), "null");
219 }
220
221 #[test]
222 fn primitive_number_infinity_becomes_null() {
223 assert_eq!(stringify(&n(f64::INFINITY), 0), "null");
224 }
225
226 #[test]
227 fn primitive_empty_string() {
228 assert_eq!(stringify(&s(""), 0), "\"\"");
229 }
230
231 #[test]
232 fn primitive_simple_string() {
233 assert_eq!(stringify(&s("hello"), 0), "\"hello\"");
234 }
235
236 #[test]
237 fn primitive_string_escapes_quote_and_backslash() {
238 assert_eq!(stringify(&s("a\"b\\c"), 0), "\"a\\\"b\\\\c\"");
239 }
240
241 #[test]
242 fn primitive_string_escapes_whitespace_controls() {
243 assert_eq!(stringify(&s("a\nb\rc\td"), 0), "\"a\\nb\\rc\\td\"");
244 }
245
246 #[test]
247 fn primitive_string_escapes_unicode_control() {
248 assert_eq!(stringify(&s("\u{0001}x"), 0), "\"\\u0001x\"");
250 }
251
252 #[test]
253 fn empty_array_is_compact() {
254 let v = JsonValue::Array(vec![]);
255 assert_eq!(stringify(&v, 0), "[]");
256 assert_eq!(stringify(&v, 2), "[]");
257 }
258
259 #[test]
260 fn empty_object_is_compact() {
261 let v = JsonValue::Object(vec![]);
262 assert_eq!(stringify(&v, 0), "{}");
263 assert_eq!(stringify(&v, 2), "{}");
264 }
265
266 #[test]
267 fn array_no_indent() {
268 let v = JsonValue::Array(vec![n(1.0), n(2.0), n(3.0)]);
269 assert_eq!(stringify(&v, 0), "[1.0,2.0,3.0]");
270 }
271
272 #[test]
273 fn array_with_indent() {
274 let v = JsonValue::Array(vec![n(1.0), n(2.0)]);
275 assert_eq!(stringify(&v, 2), "[\n 1.0,\n 2.0\n]");
276 }
277
278 #[test]
279 fn object_no_indent() {
280 let v = JsonValue::Object(vec![("a".to_string(), n(1.0)), ("b".to_string(), b(true))]);
281 assert_eq!(stringify(&v, 0), "{\"a\":1.0,\"b\":true}");
282 }
283
284 #[test]
285 fn object_with_indent() {
286 let v = JsonValue::Object(vec![("a".to_string(), n(1.0))]);
287 assert_eq!(stringify(&v, 2), "{\n \"a\": 1.0\n}");
288 }
289
290 #[test]
291 fn nested_object_array() {
292 let v = JsonValue::Object(vec![(
293 "items".to_string(),
294 JsonValue::Array(vec![s("x"), s("y")]),
295 )]);
296 let out = stringify(&v, 2);
297 assert!(out.contains("\"items\":"));
298 assert!(out.contains("\"x\""));
299 assert!(out.contains("\"y\""));
300 }
301
302 #[test]
303 fn object_key_with_special_chars_is_escaped() {
304 let v = JsonValue::Object(vec![("a\"b".to_string(), n(1.0))]);
305 let out = stringify(&v, 0);
306 assert!(out.contains("\"a\\\"b\""));
307 }
308
309 #[test]
310 fn estimate_size_is_nonzero() {
311 let v = JsonValue::Object(vec![
312 ("a".to_string(), n(1.0)),
313 ("b".to_string(), s("hello")),
314 ]);
315 assert!(estimate_json_size(&v, 0) > 0);
316 assert!(estimate_json_size(&v, 2) >= estimate_json_size(&v, 0));
317 }
318
319 #[test]
320 fn round_trip_via_serde_json_for_objects() {
321 let v = JsonValue::Object(vec![
322 ("name".to_string(), s("Alice")),
323 ("age".to_string(), n(30.0)),
324 ("active".to_string(), b(true)),
325 ("nothing".to_string(), null()),
326 ]);
327 let out = stringify(&v, 0);
328 let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
329 assert_eq!(parsed["name"], "Alice");
330 assert_eq!(parsed["age"], 30.0);
331 assert_eq!(parsed["active"], true);
332 assert!(parsed["nothing"].is_null());
333 }
334}