1use serde_json::{Map, Value};
2
3use crate::Result;
4
5use super::Envelope;
6
7pub fn render_toon(envelope: &Envelope) -> Result<String> {
9 envelope.serialization_result()?;
10 let clean = serde_json::to_value(envelope)?;
11 Ok(encode_value(&clean))
12}
13
14fn encode_value(value: &Value) -> String {
15 if is_primitive(value) {
16 return encode_primitive(value);
17 }
18 let mut lines = Vec::new();
19 match value {
20 Value::Array(items) => encode_array("", items, &mut lines, 0),
21 Value::Object(map) => encode_object(map, &mut lines, 0),
22 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
23 }
24 lines.join("\n")
25}
26
27fn encode_object(map: &Map<String, Value>, lines: &mut Vec<String>, depth: usize) {
28 let mut keys = map.keys().collect::<Vec<_>>();
29 keys.sort();
30 for key in keys {
31 encode_key_value_pair(key, &map[key], lines, depth);
32 }
33}
34
35fn encode_key_value_pair(key: &str, value: &Value, lines: &mut Vec<String>, depth: usize) {
36 let encoded_key = encode_key(key);
37 match value {
38 value if is_primitive(value) => push_line(
39 lines,
40 depth,
41 format!("{encoded_key}: {}", encode_primitive(value)),
42 ),
43 Value::Array(items) => encode_array(key, items, lines, depth),
44 Value::Object(map) if map.is_empty() => push_line(lines, depth, format!("{encoded_key}:")),
45 Value::Object(map) => {
46 push_line(lines, depth, format!("{encoded_key}:"));
47 encode_object(map, lines, depth + 1);
48 }
49 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
50 }
51}
52
53fn encode_array(key: &str, items: &[Value], lines: &mut Vec<String>, depth: usize) {
54 if items.is_empty() {
55 push_line(lines, depth, format_header(key, 0, &[]));
56 return;
57 }
58
59 if items.iter().all(is_primitive) {
60 push_line(lines, depth, format_inline_array(key, items));
61 return;
62 }
63
64 if items
65 .iter()
66 .all(|item| matches!(item, Value::Array(values) if values.iter().all(is_primitive)))
67 {
68 push_line(lines, depth, format_header(key, items.len(), &[]));
69 for item in items {
70 if let Value::Array(values) = item {
71 push_line(
72 lines,
73 depth + 1,
74 format!("- {}", format_inline_array("", values)),
75 );
76 }
77 }
78 return;
79 }
80
81 if let Some(header) = detect_tabular_header(items) {
82 push_line(lines, depth, format_header(key, items.len(), &header));
83 for item in items {
84 let Value::Object(map) = item else {
85 continue;
86 };
87 let values = header.iter().map(|key| &map[key]).collect::<Vec<_>>();
88 push_line(lines, depth + 1, join_encoded_values(&values));
89 }
90 return;
91 }
92
93 push_line(lines, depth, format_header(key, items.len(), &[]));
94 for item in items {
95 match item {
96 value if is_primitive(value) => {
97 push_line(lines, depth + 1, format!("- {}", encode_primitive(value)));
98 }
99 Value::Array(values) if values.iter().all(is_primitive) => {
100 push_line(
101 lines,
102 depth + 1,
103 format!("- {}", format_inline_array("", values)),
104 );
105 }
106 Value::Object(map) => encode_object_as_list_item(map, lines, depth + 1),
107 Value::Array(_)
108 | Value::Null
109 | Value::Bool(_)
110 | Value::Number(_)
111 | Value::String(_) => {}
112 }
113 }
114}
115
116fn detect_tabular_header(items: &[Value]) -> Option<Vec<String>> {
117 let Value::Object(first) = items.first()? else {
118 return None;
119 };
120 if first.is_empty() {
121 return None;
122 }
123 let mut header = first.keys().cloned().collect::<Vec<_>>();
124 header.sort();
125 for item in items {
126 let Value::Object(map) = item else {
127 return None;
128 };
129 if map.len() != header.len() {
130 return None;
131 }
132 for key in &header {
133 if !map.get(key).is_some_and(is_primitive) {
134 return None;
135 }
136 }
137 }
138 Some(header)
139}
140
141fn encode_object_as_list_item(map: &Map<String, Value>, lines: &mut Vec<String>, depth: usize) {
142 let mut keys = map.keys().collect::<Vec<_>>();
143 keys.sort();
144 let Some(first_key) = keys.first() else {
145 push_line(lines, depth, "-".to_owned());
146 return;
147 };
148 let first_value = &map[*first_key];
149 match first_value {
150 value if is_primitive(value) => push_line(
151 lines,
152 depth,
153 format!("- {}: {}", encode_key(first_key), encode_primitive(value)),
154 ),
155 Value::Array(values) if values.iter().all(is_primitive) => push_line(
156 lines,
157 depth,
158 format!("- {}", format_inline_array(first_key, values)),
159 ),
160 Value::Object(nested) if nested.is_empty() => {
161 push_line(lines, depth, format!("- {}:", encode_key(first_key)));
162 }
163 Value::Object(nested) => {
164 push_line(lines, depth, format!("- {}:", encode_key(first_key)));
165 encode_object(nested, lines, depth + 2);
166 }
167 Value::Array(values) => {
168 push_line(
169 lines,
170 depth,
171 format!("- {}[{}]:", encode_key(first_key), values.len()),
172 );
173 for item in values {
174 match item {
175 value if is_primitive(value) => {
176 push_line(lines, depth + 1, format!("- {}", encode_primitive(value)));
177 }
178 Value::Array(nested) if nested.iter().all(is_primitive) => {
179 push_line(
180 lines,
181 depth + 1,
182 format!("- {}", format_inline_array("", nested)),
183 );
184 }
185 Value::Object(nested) => encode_object_as_list_item(nested, lines, depth + 1),
186 Value::Array(_)
187 | Value::Null
188 | Value::Bool(_)
189 | Value::Number(_)
190 | Value::String(_) => {}
191 }
192 }
193 }
194 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
195 }
196
197 for key in keys.into_iter().skip(1) {
198 encode_key_value_pair(key, &map[key], lines, depth + 1);
199 }
200}
201
202fn format_header(key: &str, len: usize, fields: &[String]) -> String {
203 let mut out = String::new();
204 if !key.is_empty() {
205 out.push_str(&encode_key(key));
206 }
207 out.push('[');
208 out.push_str(&len.to_string());
209 out.push(']');
210 if !fields.is_empty() {
211 out.push('{');
212 out.push_str(
213 &fields
214 .iter()
215 .map(|field| encode_key(field))
216 .collect::<Vec<_>>()
217 .join(","),
218 );
219 out.push('}');
220 }
221 out.push(':');
222 out
223}
224
225fn format_inline_array(key: &str, values: &[Value]) -> String {
226 let header = format_header(key, values.len(), &[]);
227 if values.is_empty() {
228 header
229 } else {
230 let refs = values.iter().collect::<Vec<_>>();
231 format!("{header} {}", join_encoded_values(&refs))
232 }
233}
234
235fn join_encoded_values(values: &[&Value]) -> String {
236 values
237 .iter()
238 .map(|value| encode_primitive(value))
239 .collect::<Vec<_>>()
240 .join(",")
241}
242
243fn encode_primitive(value: &Value) -> String {
244 match value {
245 Value::Bool(true) => "true".to_owned(),
246 Value::Bool(false) => "false".to_owned(),
247 Value::Number(number) => number.to_string(),
248 Value::String(value) => encode_string_literal(value),
249 Value::Null | Value::Array(_) | Value::Object(_) => "null".to_owned(),
250 }
251}
252
253fn encode_string_literal(value: &str) -> String {
254 if is_safe_unquoted(value) {
255 value.to_owned()
256 } else {
257 format!("\"{}\"", escape_string(value))
258 }
259}
260
261fn encode_key(key: &str) -> String {
262 if is_valid_unquoted_key(key) {
263 key.to_owned()
264 } else {
265 format!("\"{}\"", escape_string(key))
266 }
267}
268
269fn is_primitive(value: &Value) -> bool {
270 matches!(
271 value,
272 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
273 )
274}
275
276fn is_safe_unquoted(value: &str) -> bool {
277 !value.is_empty()
278 && value.trim() == value
279 && !matches!(value, "true" | "false" | "null")
280 && !is_numeric_like(value)
281 && !value.contains(':')
282 && !value.contains('"')
283 && !value.contains('\\')
284 && !value.contains(',')
285 && !value.contains(['[', ']', '{', '}'])
286 && !value.contains(['\n', '\r', '\t'])
287 && !value.starts_with('-')
288}
289
290fn is_numeric_like(value: &str) -> bool {
291 if value.starts_with('0') && value.len() > 1 && value.chars().all(|ch| ch.is_ascii_digit()) {
292 return true;
293 }
294 value.parse::<f64>().is_ok()
295}
296
297fn is_valid_unquoted_key(key: &str) -> bool {
298 let mut chars = key.chars();
299 let Some(first) = chars.next() else {
300 return false;
301 };
302 (first.is_ascii_alphabetic() || first == '_')
303 && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '.')
304}
305
306fn escape_string(value: &str) -> String {
307 value
308 .replace('\\', "\\\\")
309 .replace('"', "\\\"")
310 .replace('\n', "\\n")
311 .replace('\r', "\\r")
312 .replace('\t', "\\t")
313}
314
315fn push_line(lines: &mut Vec<String>, depth: usize, line: String) {
316 lines.push(format!("{}{line}", " ".repeat(depth)));
317}