Skip to main content

toon/cli/
json_stringify.rs

1use std::fmt::Write;
2
3use crate::JsonValue;
4
5/// Stream JSON stringification chunks for a `JsonValue`.
6/// Returns a Vec with a single string (optimized to avoid many small allocations).
7#[must_use]
8pub fn json_stringify_lines(value: &JsonValue, indent: usize) -> Vec<String> {
9    // Estimate size: rough guess based on value complexity
10    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
16/// Estimate the JSON output size for pre-allocation
17fn 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            // Escape key inline
100            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/// Push spaces for indentation
141#[inline]
142fn push_indent(buf: &mut String, count: usize) {
143    for _ in 0..count {
144        buf.push(' ');
145    }
146}
147
148/// Push a JSON-escaped string (with quotes) directly to buffer
149fn 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                // Use \uXXXX format for control characters
160                let _ = write!(buf, "\\u{:04x}", c as u32);
161            }
162            c => buf.push(c),
163        }
164    }
165    buf.push('"');
166}