1use sonic_rs::{Array, JsonContainerTrait, JsonType, JsonValueTrait, Object, Value};
10use std::fmt::Write as _;
11
12#[derive(Clone, Copy)]
14pub struct Config {
15 pub delimiter: u8,
17 pub key_folding: bool,
19 pub flatten_depth: Option<usize>,
21 pub empty_array_bare: bool,
24 pub escape_controls: bool,
27}
28
29impl Default for Config {
30 fn default() -> Self {
31 Self {
32 delimiter: b',',
33 key_folding: false,
34 flatten_depth: None,
35 empty_array_bare: true,
36 escape_controls: true,
37 }
38 }
39}
40
41pub fn encode(json_bytes: &[u8]) -> Result<String, String> {
42 encode_with(json_bytes, &Config::default())
43}
44
45pub fn encode_with(json_bytes: &[u8], cfg: &Config) -> Result<String, String> {
46 let value: Value =
47 sonic_rs::from_slice(json_bytes).map_err(|e| format!("JSON parse error: {}", e))?;
48 let mut out = String::with_capacity(json_bytes.len());
49 match cfg.delimiter {
50 b',' => write_root::<b','>(&value, cfg, &mut out),
51 b'\t' => write_root::<b'\t'>(&value, cfg, &mut out),
52 b'|' => write_root::<b'|'>(&value, cfg, &mut out),
53 _ => return Err("delimiter must be ',', '\\t', or '|'".to_string()),
54 }
55 Ok(out)
56}
57
58fn write_root<const DELIM: u8>(v: &Value, cfg: &Config, out: &mut String) {
59 match v.get_type() {
60 JsonType::Object => {
61 let m = v.as_object().unwrap();
62 if !m.is_empty() {
63 write_object_body::<DELIM>(m, 0, cfg, cfg.key_folding, out);
66 }
67 }
68 JsonType::Array => {
69 let arr = v.as_array().unwrap();
70 if arr.is_empty() && cfg.empty_array_bare {
72 out.push_str("[]");
73 } else {
74 write_array_suffix::<DELIM>(arr, 0, cfg, out);
75 }
76 }
77 _ => write_scalar::<DELIM>(v, cfg, out),
78 }
79}
80
81fn write_object_body<const DELIM: u8>(
82 m: &Object,
83 indent: usize,
84 cfg: &Config,
85 allow_fold: bool,
86 out: &mut String,
87) {
88 let mut first = true;
89 for (k, v) in m.iter() {
90 if !first {
91 out.push('\n');
92 }
93 first = false;
94 write_indent(indent, out);
95
96 if allow_fold {
97 if let Some((joined, final_v)) = try_fold(k, v, cfg, m) {
98 write_key(&joined, cfg, out);
99 write_value_after_key::<DELIM>(final_v, indent, cfg, out);
100 continue;
101 }
102 }
103
104 write_key(k, cfg, out);
105 write_value_after_key::<DELIM>(v, indent, cfg, out);
106 }
107}
108
109fn try_fold<'a>(k: &'a str, v: &'a Value, cfg: &Config, m: &Object) -> Option<(String, &'a Value)> {
110 let max_depth = cfg.flatten_depth.unwrap_or(usize::MAX);
111 if max_depth < 2 {
112 return None;
113 }
114
115 if key_needs_quoting(k) {
117 return None;
118 }
119
120 let mut cur_v = v;
121 let mut path: Vec<&'a str> = vec![k];
122
123 loop {
124 if path.len() >= max_depth {
125 break;
126 }
127 let obj = match cur_v.get_type() {
128 JsonType::Object => cur_v.as_object().unwrap(),
129 _ => break,
130 };
131 if obj.len() != 1 {
132 break;
133 }
134 let (nk, nv) = obj.iter().next().unwrap();
135 if key_needs_quoting(nk) {
136 break;
137 }
138 path.push(nk);
139 cur_v = nv;
140 }
141
142 if path.len() < 2 {
143 return None;
144 }
145
146 let joined: String = path.join(".");
147
148 if m.get(&joined).is_some() {
149 return None;
150 }
151
152 Some((joined, cur_v))
153}
154
155fn write_value_after_key<const DELIM: u8>(
156 v: &Value,
157 key_indent: usize,
158 cfg: &Config,
159 out: &mut String,
160) {
161 match v.get_type() {
162 JsonType::Object => {
163 let child = v.as_object().unwrap();
164 if child.is_empty() {
165 out.push(':');
166 } else {
167 out.push_str(":\n");
168 let allow = cfg.key_folding && child.len() > 1;
173 write_object_body::<DELIM>(child, key_indent + 1, cfg, allow, out);
174 }
175 }
176 JsonType::Array => {
177 let arr = v.as_array().unwrap();
178 if arr.is_empty() && cfg.empty_array_bare {
180 out.push_str(": []");
181 } else {
182 write_array_suffix::<DELIM>(arr, key_indent, cfg, out);
183 }
184 }
185 _ => {
186 out.push_str(": ");
187 write_scalar::<DELIM>(v, cfg, out);
188 }
189 }
190}
191
192fn write_empty_array_legacy<const DELIM: u8>(out: &mut String) {
195 out.push_str("[0");
196 if DELIM != b',' {
197 out.push(DELIM as char);
198 }
199 out.push_str("]:");
200}
201
202fn write_array_suffix<const DELIM: u8>(arr: &Array, indent: usize, cfg: &Config, out: &mut String) {
203 let _ = cfg;
204 if arr.is_empty() {
205 write_empty_array_legacy::<DELIM>(out);
206 return;
207 }
208
209 write!(out, "[{}", arr.len()).unwrap();
210 if DELIM != b',' {
211 out.push(DELIM as char);
212 }
213 out.push(']');
214
215 if arr.iter().all(is_scalar) {
216 out.push_str(": ");
217 let mut first = true;
218 for v in arr.iter() {
219 if !first {
220 out.push(DELIM as char);
221 }
222 first = false;
223 write_scalar::<DELIM>(v, cfg, out);
224 }
225 return;
226 }
227
228 if let Some((keys, uniform_order)) = table_keys(arr) {
229 out.push('{');
230 for (i, k) in keys.iter().enumerate() {
231 if i > 0 {
232 out.push(DELIM as char);
233 }
234 write_key(k, cfg, out);
235 }
236 out.push_str("}:");
237 if uniform_order {
238 for item in arr.iter() {
239 let m = item.as_object().unwrap();
240 out.push('\n');
241 write_indent(indent + 1, out);
242 let mut first = true;
243 for (_, v) in m.iter() {
244 if !first {
245 out.push(DELIM as char);
246 }
247 first = false;
248 write_scalar::<DELIM>(v, cfg, out);
249 }
250 }
251 } else {
252 for item in arr.iter() {
253 let m = item.as_object().unwrap();
254 out.push('\n');
255 write_indent(indent + 1, out);
256 let mut first = true;
257 for k in &keys {
258 if !first {
259 out.push(DELIM as char);
260 }
261 first = false;
262 write_scalar::<DELIM>(m.get(k).unwrap(), cfg, out);
263 }
264 }
265 }
266 return;
267 }
268
269 out.push(':');
270 for item in arr.iter() {
271 out.push('\n');
272 write_indent(indent + 1, out);
273 out.push('-');
274 write_list_item::<DELIM>(item, indent + 1, cfg, out);
275 }
276}
277
278fn write_list_item<const DELIM: u8>(v: &Value, l: usize, cfg: &Config, out: &mut String) {
279 match v.get_type() {
280 JsonType::Object => {
281 let m = v.as_object().unwrap();
282 if !m.is_empty() {
283 out.push(' ');
284 write_list_item_object::<DELIM>(m, l, cfg, out);
285 }
286 }
287 JsonType::Array => {
288 out.push(' ');
289 write_array_suffix::<DELIM>(v.as_array().unwrap(), l, cfg, out);
290 }
291 _ => {
292 out.push(' ');
293 write_scalar::<DELIM>(v, cfg, out);
294 }
295 }
296}
297
298fn write_list_item_object<const DELIM: u8>(m: &Object, l: usize, cfg: &Config, out: &mut String) {
299 let mut first = true;
300 for (k, v) in m.iter() {
301 if !first {
302 out.push('\n');
303 write_indent(l + 1, out);
304 }
305 first = false;
306 write_key(k, cfg, out);
307 write_value_after_key::<DELIM>(v, l + 1, cfg, out);
308 }
309}
310
311const INDENTS: [&str; 9] = [
314 "",
315 " ",
316 " ",
317 " ",
318 " ",
319 " ",
320 " ",
321 " ",
322 " ",
323];
324
325#[inline]
326fn write_indent(level: usize, out: &mut String) {
327 if level < INDENTS.len() {
328 out.push_str(INDENTS[level]);
329 } else {
330 for _ in 0..(level * 2) {
331 out.push(' ');
332 }
333 }
334}
335
336fn is_scalar(v: &Value) -> bool {
337 !matches!(v.get_type(), JsonType::Object | JsonType::Array)
338}
339
340fn table_keys<'a>(arr: &'a Array) -> Option<(Vec<&'a str>, bool)> {
341 let first_v = arr.iter().next()?;
342 let first = first_v.as_object()?;
343 if first.is_empty() {
344 return None;
345 }
346 if !first.iter().all(|(_, v)| is_scalar(v)) {
347 return None;
348 }
349 let keys: Vec<&'a str> = first.iter().map(|(k, _)| k).collect();
350 let mut uniform_order = true;
351
352 for item in arr.iter().skip(1) {
353 let m = item.as_object()?;
354 if m.len() != keys.len() {
355 return None;
356 }
357 let mut row_iter = m.iter();
358 for k in &keys {
359 let (ik, iv) = row_iter.next()?;
360 if !is_scalar(iv) {
361 return None;
362 }
363 if ik != *k {
364 uniform_order = false;
365 }
366 }
367 if !uniform_order {
368 for k in &keys {
369 match m.get(k) {
370 Some(v) if is_scalar(v) => {}
371 _ => return None,
372 }
373 }
374 }
375 }
376 Some((keys, uniform_order))
377}
378
379#[inline]
382fn write_scalar<const DELIM: u8>(v: &Value, cfg: &Config, out: &mut String) {
383 match v.get_type() {
384 JsonType::Null => out.push_str("null"),
385 JsonType::Boolean => out.push_str(if v.as_bool().unwrap() {
386 "true"
387 } else {
388 "false"
389 }),
390 JsonType::Number => write_number(v, out),
391 JsonType::String => write_string_value::<DELIM>(v.as_str().unwrap(), cfg, out),
392 _ => unreachable!("write_scalar on non-scalar"),
393 }
394}
395
396fn write_number(v: &Value, out: &mut String) {
397 if let Some(i) = v.as_i64() {
398 let mut buf = itoa::Buffer::new();
399 out.push_str(buf.format(i));
400 return;
401 }
402 if let Some(u) = v.as_u64() {
403 let mut buf = itoa::Buffer::new();
404 out.push_str(buf.format(u));
405 return;
406 }
407 if let Some(f) = v.as_f64() {
411 write_float(f, out);
412 } else {
413 out.push_str("null");
414 }
415}
416
417fn write_float(f: f64, out: &mut String) {
418 if !f.is_finite() {
419 out.push_str("null");
420 return;
421 }
422 if f == 0.0 {
423 out.push('0');
424 return;
425 }
426 if f.fract() == 0.0 && f.abs() < 1e16 {
428 let mut buf = itoa::Buffer::new();
429 out.push_str(buf.format(f as i64));
430 return;
431 }
432 write!(out, "{}", f).unwrap();
434}
435
436#[inline]
439fn write_string_value<const DELIM: u8>(s: &str, cfg: &Config, out: &mut String) {
440 if value_needs_quoting::<DELIM>(s, cfg.escape_controls) {
441 write_quoted(s, cfg.escape_controls, out);
442 } else {
443 out.push_str(s);
444 }
445}
446
447fn write_key(k: &str, cfg: &Config, out: &mut String) {
448 if key_needs_quoting(k) {
449 write_quoted(k, cfg.escape_controls, out);
450 } else {
451 out.push_str(k);
452 }
453}
454
455#[inline]
461fn key_needs_quoting(s: &str) -> bool {
462 if s.is_empty() {
463 return true;
464 }
465 let bytes = s.as_bytes();
466 let start = match bytes[0] {
467 b'@' | b'$' | b'#' => {
468 if bytes.len() < 2 {
469 return true; }
471 1
472 }
473 _ => 0,
474 };
475 let first = bytes[start];
476 if !(first.is_ascii_alphabetic() || first == b'_') {
477 return true;
478 }
479 for &b in &bytes[start + 1..] {
480 if !(b.is_ascii_alphanumeric() || b == b'_' || b == b'.') {
481 return true;
482 }
483 }
484 false
485}
486
487#[inline]
488fn value_needs_quoting<const DELIM: u8>(s: &str, escape_controls: bool) -> bool {
489 if s.is_empty() {
490 return true;
491 }
492 let bytes = s.as_bytes();
493 match bytes[0] {
494 b'-' | b'[' | b'{' | b'"' | b'#' | b' ' | b'\t' => return true,
495 _ => {}
496 }
497 match bytes[bytes.len() - 1] {
498 b' ' | b'\t' => return true,
499 _ => {}
500 }
501 for &b in bytes {
505 match b {
506 b':' | b'\n' | b'\r' | b'\t' | b'"' | b'\\' => return true,
507 _ if escape_controls && b < 0x20 => return true,
510 _ if b == DELIM => return true,
511 _ => {}
512 }
513 }
514 if matches!(s, "true" | "false" | "null") {
515 return true;
516 }
517 looks_like_number(bytes)
518}
519
520fn looks_like_number(bytes: &[u8]) -> bool {
521 let mut i = 0;
522 if bytes[0] == b'-' {
523 i = 1;
524 if i == bytes.len() {
525 return false;
526 }
527 }
528 let mut has_digit = false;
529 while i < bytes.len() && bytes[i].is_ascii_digit() {
530 has_digit = true;
531 i += 1;
532 }
533 if !has_digit {
534 return false;
535 }
536 if i < bytes.len() && bytes[i] == b'.' {
537 i += 1;
538 let mut has_frac = false;
539 while i < bytes.len() && bytes[i].is_ascii_digit() {
540 has_frac = true;
541 i += 1;
542 }
543 if !has_frac {
544 return false;
545 }
546 }
547 if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
548 i += 1;
549 if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
550 i += 1;
551 }
552 let mut has_exp_digit = false;
553 while i < bytes.len() && bytes[i].is_ascii_digit() {
554 has_exp_digit = true;
555 i += 1;
556 }
557 if !has_exp_digit {
558 return false;
559 }
560 }
561 i == bytes.len()
562}
563
564#[inline]
566fn hex_lower(n: u8) -> u8 {
567 match n {
568 0..=9 => b'0' + n,
569 _ => b'a' + (n - 10),
570 }
571}
572
573fn write_quoted(s: &str, escape_controls: bool, out: &mut String) {
574 out.push('"');
575 let bytes = s.as_bytes();
576 let mut start = 0;
577 for (i, &b) in bytes.iter().enumerate() {
578 let named = matches!(b, b'\\' | b'"' | b'\n' | b'\r' | b'\t');
581 let other_control = escape_controls && b < 0x20;
582 if named || other_control {
583 if start < i {
584 out.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..i]) });
585 }
586 match b {
587 b'\\' => out.push_str("\\\\"),
588 b'"' => out.push_str("\\\""),
589 b'\n' => out.push_str("\\n"),
590 b'\r' => out.push_str("\\r"),
591 b'\t' => out.push_str("\\t"),
592 _ => {
593 out.push_str("\\u00");
595 out.push(hex_lower(b >> 4) as char);
596 out.push(hex_lower(b & 0x0f) as char);
597 }
598 }
599 start = i + 1;
600 }
601 }
602 if start < bytes.len() {
603 out.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..]) });
604 }
605 out.push('"');
606}
607
608#[cfg(test)]
609mod tests {
610 use super::{encode, encode_with, Config};
611
612 fn enc(json: &str) -> String {
613 encode(json.as_bytes()).unwrap()
614 }
615
616 fn enc_with(json: &str, cfg: &Config) -> String {
617 encode_with(json.as_bytes(), cfg).unwrap()
618 }
619
620 #[test]
625 fn test_write_number_small_exponent_expands_to_decimal() {
626 assert_eq!(enc(r#"{"n":1e-6}"#), "n: 0.000001");
627 assert_eq!(enc(r#"{"n":1e-7}"#), "n: 0.0000001");
628 }
629
630 #[test]
631 fn test_write_number_integer_valued_float_drops_fraction() {
632 assert_eq!(enc(r#"{"n":100.0}"#), "n: 100");
633 assert_eq!(enc(r#"{"n":-2.0}"#), "n: -2");
634 }
635
636 #[test]
637 fn test_write_number_decimal_full_precision_preserved() {
638 assert_eq!(enc(r#"{"n":3.14}"#), "n: 3.14");
639 assert_eq!(enc(r#"{"n":0.3333333333333333}"#), "n: 0.3333333333333333");
640 assert_eq!(enc(r#"{"n":1234567.89}"#), "n: 1234567.89");
641 }
642
643 #[test]
644 fn test_write_number_large_magnitude_float_expands_no_exponent() {
645 assert_eq!(enc(r#"{"n":1e21}"#), "n: 1000000000000000000000");
646 }
647
648 #[test]
649 fn test_write_number_i64_and_u64_fast_paths() {
650 assert_eq!(enc(r#"{"n":42}"#), "n: 42");
651 assert_eq!(
652 enc(r#"{"n":-9223372036854775808}"#),
653 "n: -9223372036854775808"
654 );
655 assert_eq!(
656 enc(r#"{"n":18446744073709551615}"#),
657 "n: 18446744073709551615"
658 );
659 }
660
661 #[test]
662 fn test_write_number_beyond_u64_keeps_expanded_form() {
663 assert_eq!(enc(r#"{"n":1e30}"#), "n: 1000000000000000000000000000000");
666 }
667
668 #[test]
671 fn test_empty_array_root_is_bare_brackets() {
672 assert_eq!(enc("[]"), "[]");
673 }
674
675 #[test]
676 fn test_empty_array_object_field_is_key_bracket() {
677 assert_eq!(enc(r#"{"a":[]}"#), "a: []");
678 assert_eq!(enc(r#"{"x":{"a":[]}}"#), "x:\n a: []");
679 }
680
681 #[test]
682 fn test_empty_array_as_array_element_keeps_legacy_header() {
683 assert_eq!(enc(r#"{"pairs":[[],[]]}"#), "pairs[2]:\n - [0]:\n - [0]:");
685 }
686
687 #[test]
688 fn test_empty_array_legacy_form_when_option_off() {
689 let cfg = Config {
690 empty_array_bare: false,
691 ..Config::default()
692 };
693 assert_eq!(enc_with("[]", &cfg), "[0]:");
694 assert_eq!(enc_with(r#"{"a":[]}"#, &cfg), "a[0]:");
695 }
696
697 #[test]
703 fn test_escape_controls_emits_lowercase_u_escape() {
704 assert_eq!(enc("{\"s\":\"a\\u001fb\"}"), "s: \"a\\u001fb\"");
708 assert_eq!(enc("{\"s\":\"a\\u0000b\"}"), "s: \"a\\u0000b\"");
709 assert_eq!(enc("{\"s\":\"\\u0004\"}"), "s: \"\\u0004\"");
710 }
711
712 #[test]
713 fn test_escape_controls_keeps_named_escapes() {
714 assert_eq!(enc(r#"{"s":"a\nb"}"#), "s: \"a\\nb\"");
715 assert_eq!(enc(r#"{"s":"a\tb"}"#), "s: \"a\\tb\"");
716 assert_eq!(enc(r#"{"s":"a\rb"}"#), "s: \"a\\rb\"");
717 }
718
719 #[test]
720 fn test_escape_controls_off_passes_raw_byte() {
721 let cfg = Config {
722 escape_controls: false,
723 ..Config::default()
724 };
725 assert_eq!(enc_with("{\"s\":\"a\\u001fb\"}", &cfg), "s: a\u{1f}b");
726 }
727
728 #[test]
731 fn test_fold_keys_root_chain() {
732 let cfg = Config {
733 key_folding: true,
734 ..Config::default()
735 };
736 assert_eq!(enc_with(r#"{"a":{"b":{"c":1}}}"#, &cfg), "a.b.c: 1");
737 }
738
739 #[test]
740 fn test_fold_keys_restarts_in_multikey_object_body() {
741 let cfg = Config {
744 key_folding: true,
745 ..Config::default()
746 };
747 assert_eq!(
748 enc_with(r#"{"a":{"x":1,"nested":{"b":{"c":2}}}}"#, &cfg),
749 "a:\n x: 1\n nested.b.c: 2"
750 );
751 }
752
753 #[test]
754 fn test_fold_keys_does_not_refold_past_flatten_depth() {
755 let cfg = Config {
756 key_folding: true,
757 flatten_depth: Some(2),
758 ..Config::default()
759 };
760 assert_eq!(
761 enc_with(r#"{"a":{"b":{"c":{"d":1}}}}"#, &cfg),
762 "a.b:\n c:\n d: 1"
763 );
764 }
765
766 #[test]
767 fn test_fold_keys_skips_sibling_collision_at_any_depth() {
768 let cfg = Config {
770 key_folding: true,
771 ..Config::default()
772 };
773 assert_eq!(
774 enc_with(
775 r#"{"data":{"meta":{"items":[1,2]}},"data.meta.items":"literal"}"#,
776 &cfg
777 ),
778 "data:\n meta:\n items[2]: 1,2\ndata.meta.items: literal"
779 );
780 }
781}