1use sonic_rs::{Array, JsonContainerTrait, JsonType, JsonValueTrait, Object, Value};
10use std::fmt::Write as _;
11
12#[derive(Clone, Copy)]
15pub struct Config {
16 pub delimiter: u8,
18 pub key_folding: bool,
22 pub flatten_depth: Option<usize>,
24 pub empty_array_bare: bool,
28 pub escape_controls: bool,
32 pub max_depth: usize,
39 pub max_input_bytes: usize,
42}
43
44impl Default for Config {
45 fn default() -> Self {
46 Self {
47 delimiter: b',',
48 key_folding: false,
49 flatten_depth: None,
50 empty_array_bare: true,
51 escape_controls: true,
52 max_depth: 1000,
53 max_input_bytes: 0,
54 }
55 }
56}
57
58pub fn encode(json_bytes: &[u8]) -> Result<String, String> {
59 encode_with(json_bytes, &Config::default())
60}
61
62pub fn encode_with(json_bytes: &[u8], cfg: &Config) -> Result<String, String> {
63 if cfg.max_input_bytes != 0 && json_bytes.len() > cfg.max_input_bytes {
64 return Err(format!(
65 "input exceeds max_input_bytes ({} > {})",
66 json_bytes.len(),
67 cfg.max_input_bytes
68 ));
69 }
70 if cfg.max_depth != 0 {
75 if let Some(depth) = scan_exceeds_depth(json_bytes, cfg.max_depth) {
76 return Err(format!(
77 "input exceeds max_depth ({} > {})",
78 depth, cfg.max_depth
79 ));
80 }
81 }
82 let value: Value =
83 sonic_rs::from_slice(json_bytes).map_err(|e| format!("JSON parse error: {}", e))?;
84 let mut out = String::with_capacity(json_bytes.len());
85 match cfg.delimiter {
86 b',' => write_root::<b','>(&value, cfg, &mut out),
87 b'\t' => write_root::<b'\t'>(&value, cfg, &mut out),
88 b'|' => write_root::<b'|'>(&value, cfg, &mut out),
89 _ => return Err("delimiter must be ',', '\\t', or '|'".to_string()),
90 }
91 Ok(out)
92}
93
94fn write_root<const DELIM: u8>(v: &Value, cfg: &Config, out: &mut String) {
95 match v.get_type() {
96 JsonType::Object => {
97 let m = v.as_object().unwrap();
98 if let Some(fields) = keyed_fields(m) {
99 write_keyed_table::<DELIM>(m, &fields, 0, cfg, out);
101 } else if !m.is_empty() {
102 write_object_body::<DELIM>(m, 0, cfg, cfg.key_folding, out);
105 }
106 }
107 JsonType::Array => {
108 let arr = v.as_array().unwrap();
109 if arr.is_empty() && cfg.empty_array_bare {
111 out.push_str("[]");
112 } else {
113 write_array_suffix::<DELIM>(arr, 0, cfg, true, out);
114 }
115 }
116 _ => write_scalar::<DELIM>(v, cfg, out),
117 }
118}
119
120fn write_object_body<const DELIM: u8>(
121 m: &Object,
122 indent: usize,
123 cfg: &Config,
124 allow_fold: bool,
125 out: &mut String,
126) {
127 let mut first = true;
128 for (k, v) in m.iter() {
129 if !first {
130 out.push('\n');
131 }
132 first = false;
133 write_indent(indent, out);
134
135 if allow_fold {
136 if let Some((joined, final_v)) = try_fold(k, v, cfg, m) {
137 write_key(&joined, cfg, out);
138 write_value_after_key::<DELIM>(final_v, indent, cfg, out);
139 continue;
140 }
141 }
142
143 write_key(k, cfg, out);
144 write_value_after_key::<DELIM>(v, indent, cfg, out);
145 }
146}
147
148fn try_fold<'a>(k: &'a str, v: &'a Value, cfg: &Config, m: &Object) -> Option<(String, &'a Value)> {
149 let max_depth = cfg.flatten_depth.unwrap_or(usize::MAX);
150 if max_depth < 2 {
151 return None;
152 }
153
154 if key_needs_quoting(k) {
156 return None;
157 }
158
159 let mut cur_v = v;
160 let mut path: Vec<&'a str> = vec![k];
161
162 loop {
163 if path.len() >= max_depth {
164 break;
165 }
166 let obj = match cur_v.get_type() {
167 JsonType::Object => cur_v.as_object().unwrap(),
168 _ => break,
169 };
170 if obj.len() != 1 {
171 break;
172 }
173 let (nk, nv) = obj.iter().next().unwrap();
174 if key_needs_quoting(nk) {
175 break;
176 }
177 path.push(nk);
178 cur_v = nv;
179 }
180
181 if path.len() < 2 {
182 return None;
183 }
184
185 let joined: String = path.join(".");
186
187 if m.get(&joined).is_some() {
188 return None;
189 }
190
191 Some((joined, cur_v))
192}
193
194fn write_value_after_key<const DELIM: u8>(
195 v: &Value,
196 key_indent: usize,
197 cfg: &Config,
198 out: &mut String,
199) {
200 match v.get_type() {
201 JsonType::Object => {
202 let child = v.as_object().unwrap();
203 if child.is_empty() {
204 out.push(':');
205 } else if let Some(fields) = keyed_fields(child) {
206 write_keyed_table::<DELIM>(child, &fields, key_indent, cfg, out);
209 } else {
210 out.push_str(":\n");
211 let allow = cfg.key_folding && child.len() > 1;
216 write_object_body::<DELIM>(child, key_indent + 1, cfg, allow, out);
217 }
218 }
219 JsonType::Array => {
220 let arr = v.as_array().unwrap();
221 if arr.is_empty() && cfg.empty_array_bare {
223 out.push_str(": []");
224 } else {
225 write_array_suffix::<DELIM>(arr, key_indent, cfg, true, out);
226 }
227 }
228 _ => {
229 out.push_str(": ");
230 write_scalar::<DELIM>(v, cfg, out);
231 }
232 }
233}
234
235fn write_empty_array_legacy<const DELIM: u8>(out: &mut String) {
238 out.push_str("[0");
239 if DELIM != b',' {
240 out.push(DELIM as char);
241 }
242 out.push_str("]:");
243}
244
245fn write_field_list<const DELIM: u8>(fields: &[Field], cfg: &Config, out: &mut String) {
248 out.push('{');
249 for (i, f) in fields.iter().enumerate() {
250 if i > 0 {
251 out.push(DELIM as char);
252 }
253 match f {
254 Field::Leaf(k) => write_key(k, cfg, out),
255 Field::Group(k, sub) => {
256 write_key(k, cfg, out);
257 write_field_list::<DELIM>(sub, cfg, out);
258 }
259 }
260 }
261 out.push('}');
262}
263
264fn write_row_cells<const DELIM: u8>(
267 m: &Object,
268 fields: &[Field],
269 cfg: &Config,
270 first: &mut bool,
271 out: &mut String,
272) {
273 for (idx, f) in fields.iter().enumerate() {
274 match f {
275 Field::Leaf(k) => {
276 if !*first {
277 out.push(DELIM as char);
278 }
279 *first = false;
280 write_scalar::<DELIM>(column_value(m, idx, k).unwrap(), cfg, out);
281 }
282 Field::Group(k, sub) => {
283 let child = column_value(m, idx, k).unwrap().as_object().unwrap();
284 write_row_cells::<DELIM>(child, sub, cfg, first, out);
285 }
286 }
287 }
288}
289
290fn write_keyed_table<const DELIM: u8>(
294 m: &Object,
295 fields: &[Field],
296 indent: usize,
297 cfg: &Config,
298 out: &mut String,
299) {
300 out.push('[');
301 let mut len_buf = itoa::Buffer::new();
302 out.push_str(len_buf.format(m.len()));
303 out.push(':');
304 if DELIM != b',' {
305 out.push(DELIM as char);
306 }
307 out.push(']');
308 write_field_list::<DELIM>(fields, cfg, out);
309 out.push(':');
310
311 for (k, v) in m.iter() {
312 out.push('\n');
313 write_indent(indent + 1, out);
314 write_key(k, cfg, out);
315 out.push_str(": ");
316 let mut first = true;
317 write_row_cells::<DELIM>(v.as_object().unwrap(), fields, cfg, &mut first, out);
318 }
319}
320
321fn write_array_suffix<const DELIM: u8>(
322 arr: &Array,
323 indent: usize,
324 cfg: &Config,
325 allow_tabular: bool,
326 out: &mut String,
327) {
328 if arr.is_empty() {
329 write_empty_array_legacy::<DELIM>(out);
330 return;
331 }
332
333 out.push('[');
334 let mut len_buf = itoa::Buffer::new();
335 out.push_str(len_buf.format(arr.len()));
336 if DELIM != b',' {
337 out.push(DELIM as char);
338 }
339 out.push(']');
340
341 if arr.iter().all(is_scalar) {
342 out.push_str(": ");
343 let mut first = true;
344 for v in arr.iter() {
345 if !first {
346 out.push(DELIM as char);
347 }
348 first = false;
349 write_scalar::<DELIM>(v, cfg, out);
350 }
351 return;
352 }
353
354 let shape = if allow_tabular {
358 table_shape(arr)
359 } else {
360 None
361 };
362
363 if let Some(Table::Nested(fields)) = &shape {
364 write_field_list::<DELIM>(fields, cfg, out);
365 out.push(':');
366 for item in arr.iter() {
367 out.push('\n');
368 write_indent(indent + 1, out);
369 let mut first = true;
370 write_row_cells::<DELIM>(item.as_object().unwrap(), fields, cfg, &mut first, out);
371 }
372 return;
373 }
374
375 if let Some(Table::Flat(keys, uniform_order)) = shape {
376 out.push('{');
380 for (i, k) in keys.iter().enumerate() {
381 if i > 0 {
382 out.push(DELIM as char);
383 }
384 write_key(k, cfg, out);
385 }
386 out.push_str("}:");
387 if uniform_order {
388 for item in arr.iter() {
389 let m = item.as_object().unwrap();
390 out.push('\n');
391 write_indent(indent + 1, out);
392 let mut first = true;
393 for (_, v) in m.iter() {
394 if !first {
395 out.push(DELIM as char);
396 }
397 first = false;
398 write_scalar::<DELIM>(v, cfg, out);
399 }
400 }
401 } else {
402 for item in arr.iter() {
403 let m = item.as_object().unwrap();
404 out.push('\n');
405 write_indent(indent + 1, out);
406 let mut first = true;
407 for k in &keys {
408 if !first {
409 out.push(DELIM as char);
410 }
411 first = false;
412 write_scalar::<DELIM>(m.get(k).unwrap(), cfg, out);
413 }
414 }
415 }
416 return;
417 }
418
419 out.push(':');
420 for item in arr.iter() {
421 out.push('\n');
422 write_indent(indent + 1, out);
423 out.push('-');
424 write_list_item::<DELIM>(item, indent + 1, cfg, out);
425 }
426}
427
428fn write_list_item<const DELIM: u8>(v: &Value, l: usize, cfg: &Config, out: &mut String) {
429 match v.get_type() {
430 JsonType::Object => {
431 let m = v.as_object().unwrap();
432 if !m.is_empty() {
433 out.push(' ');
434 write_list_item_object::<DELIM>(m, l, cfg, out);
435 }
436 }
437 JsonType::Array => {
438 out.push(' ');
439 write_array_suffix::<DELIM>(v.as_array().unwrap(), l, cfg, false, out);
441 }
442 _ => {
443 out.push(' ');
444 write_scalar::<DELIM>(v, cfg, out);
445 }
446 }
447}
448
449fn write_list_item_object<const DELIM: u8>(m: &Object, l: usize, cfg: &Config, out: &mut String) {
450 let mut first = true;
451 for (k, v) in m.iter() {
452 if !first {
453 out.push('\n');
454 write_indent(l + 1, out);
455 }
456 first = false;
457 write_key(k, cfg, out);
458 write_value_after_key::<DELIM>(v, l + 1, cfg, out);
459 }
460}
461
462const OPEN: u8 = 1;
468const CLOSE: u8 = 2;
469const QUOTE: u8 = 3;
470
471const CLASS: [u8; 256] = {
472 let mut t = [0u8; 256];
473 t[b'{' as usize] = OPEN;
474 t[b'[' as usize] = OPEN;
475 t[b'}' as usize] = CLOSE;
476 t[b']' as usize] = CLOSE;
477 t[b'"' as usize] = QUOTE;
478 t
479};
480
481fn scan_exceeds_depth(bytes: &[u8], max_depth: usize) -> Option<usize> {
490 let mut depth: usize = 0;
491 let mut i = 0;
492 let n = bytes.len();
493 while i < n {
494 match CLASS[bytes[i] as usize] {
495 OPEN => {
496 depth += 1;
497 if depth > max_depth {
498 return Some(depth);
499 }
500 i += 1;
501 }
502 CLOSE => {
503 depth = depth.saturating_sub(1);
504 i += 1;
505 }
506 QUOTE => {
507 i += 1;
510 loop {
511 let p = memchr::memchr2(b'"', b'\\', &bytes[i..])?;
514 if bytes[i + p] == b'"' {
515 i += p + 1;
516 break;
517 }
518 i += p + 2;
520 if i >= n {
521 return None;
522 }
523 }
524 }
525 _ => i += 1,
526 }
527 }
528 None
529}
530
531const INDENTS: [&str; 9] = [
534 "",
535 " ",
536 " ",
537 " ",
538 " ",
539 " ",
540 " ",
541 " ",
542 " ",
543];
544
545#[inline]
546fn write_indent(level: usize, out: &mut String) {
547 if level < INDENTS.len() {
548 out.push_str(INDENTS[level]);
549 } else {
550 for _ in 0..(level * 2) {
551 out.push(' ');
552 }
553 }
554}
555
556fn is_scalar(v: &Value) -> bool {
557 !matches!(v.get_type(), JsonType::Object | JsonType::Array)
558}
559
560enum Field<'a> {
564 Leaf(&'a str),
565 Group(&'a str, Vec<Field<'a>>),
566}
567
568enum Table<'a> {
570 Flat(Vec<&'a str>, bool),
574 Nested(Vec<Field<'a>>),
576}
577
578#[inline]
581fn column_value<'a>(m: &'a Object, idx: usize, k: &str) -> Option<&'a Value> {
582 match m.iter().nth(idx) {
583 Some((ik, iv)) if ik == k => Some(iv),
584 _ => m.get(&k),
585 }
586}
587
588#[inline]
593fn columns_could_be_uniform(first: &Object) -> bool {
594 !first.is_empty()
595 && first.iter().all(|(_, v)| match v.get_type() {
596 JsonType::Array => false,
597 JsonType::Object => !v.as_object().unwrap().is_empty(),
598 _ => true,
599 })
600}
601
602fn build_fields<'a>(objs: &[&'a Object]) -> Option<Vec<Field<'a>>> {
606 let first = *objs.first()?;
607 if first.is_empty() {
608 return None;
609 }
610 for m in &objs[1..] {
611 if m.len() != first.len() {
612 return None;
613 }
614 }
615
616 let mut fields = Vec::with_capacity(first.len());
617 for (idx, (k, v0)) in first.iter().enumerate() {
618 match v0.get_type() {
619 JsonType::Object => {
620 let sub0 = v0.as_object().unwrap();
621 if sub0.is_empty() {
622 return None;
623 }
624 let mut subs = Vec::with_capacity(objs.len());
625 subs.push(sub0);
626 for m in &objs[1..] {
627 let sub = column_value(m, idx, k)?.as_object()?;
628 if sub.is_empty() {
629 return None;
630 }
631 subs.push(sub);
632 }
633 fields.push(Field::Group(k, build_fields(&subs)?));
634 }
635 JsonType::Array => return None,
638 _ => {
639 for m in &objs[1..] {
640 if !is_scalar(column_value(m, idx, k)?) {
641 return None;
642 }
643 }
644 fields.push(Field::Leaf(k));
645 }
646 }
647 }
648 Some(fields)
649}
650
651fn table_shape<'a>(arr: &'a Array) -> Option<Table<'a>> {
652 if let Some((keys, uniform_order)) = table_keys(arr) {
653 return Some(Table::Flat(keys, uniform_order));
654 }
655 let probe = arr.iter().next()?.as_object()?;
663 if !columns_could_be_uniform(probe)
664 || !probe
665 .iter()
666 .any(|(_, v)| matches!(v.get_type(), JsonType::Object))
667 {
668 return None;
669 }
670
671 let mut objs = Vec::with_capacity(arr.len());
672 for v in arr.iter() {
673 objs.push(v.as_object()?);
674 }
675 Some(Table::Nested(build_fields(&objs)?))
676}
677
678fn keyed_fields<'a>(m: &'a Object) -> Option<Vec<Field<'a>>> {
682 if m.len() < 2 {
683 return None;
684 }
685 let probe = m.iter().next()?.1.as_object()?;
687 if !columns_could_be_uniform(probe) {
688 return None;
689 }
690 let mut objs = Vec::with_capacity(m.len());
691 for (_, v) in m.iter() {
692 objs.push(v.as_object()?);
693 }
694 build_fields(&objs)
695}
696
697fn table_keys<'a>(arr: &'a Array) -> Option<(Vec<&'a str>, bool)> {
698 let first_v = arr.iter().next()?;
699 let first = first_v.as_object()?;
700 if first.is_empty() {
701 return None;
702 }
703 if !first.iter().all(|(_, v)| is_scalar(v)) {
704 return None;
705 }
706 let keys: Vec<&'a str> = first.iter().map(|(k, _)| k).collect();
707 let mut uniform_order = true;
708
709 for item in arr.iter().skip(1) {
710 let m = item.as_object()?;
711 if m.len() != keys.len() {
712 return None;
713 }
714 let mut row_iter = m.iter();
715 for k in &keys {
716 let (ik, iv) = row_iter.next()?;
717 if !is_scalar(iv) {
718 return None;
719 }
720 if ik != *k {
721 uniform_order = false;
722 }
723 }
724 if !uniform_order {
725 for k in &keys {
726 match m.get(k) {
727 Some(v) if is_scalar(v) => {}
728 _ => return None,
729 }
730 }
731 }
732 }
733 Some((keys, uniform_order))
734}
735
736#[inline]
739fn write_scalar<const DELIM: u8>(v: &Value, cfg: &Config, out: &mut String) {
740 match v.get_type() {
741 JsonType::Null => out.push_str("null"),
742 JsonType::Boolean => out.push_str(if v.as_bool().unwrap() {
743 "true"
744 } else {
745 "false"
746 }),
747 JsonType::Number => write_number(v, out),
748 JsonType::String => write_string_value::<DELIM>(v.as_str().unwrap(), cfg, out),
749 _ => unreachable!("write_scalar on non-scalar"),
750 }
751}
752
753fn write_number(v: &Value, out: &mut String) {
754 if let Some(i) = v.as_i64() {
755 let mut buf = itoa::Buffer::new();
756 out.push_str(buf.format(i));
757 return;
758 }
759 if let Some(u) = v.as_u64() {
760 let mut buf = itoa::Buffer::new();
761 out.push_str(buf.format(u));
762 return;
763 }
764 if let Some(f) = v.as_f64() {
768 write_float(f, out);
769 } else {
770 out.push_str("null");
771 }
772}
773
774fn write_float(f: f64, out: &mut String) {
775 if !f.is_finite() {
776 out.push_str("null");
777 return;
778 }
779 if f == 0.0 {
780 out.push('0');
781 return;
782 }
783 if f.fract() == 0.0 && f.abs() < 1e16 {
785 let mut buf = itoa::Buffer::new();
786 out.push_str(buf.format(f as i64));
787 return;
788 }
789 let mut buf = ryu::Buffer::new();
795 let s = buf.format_finite(f);
796 if s.as_bytes().contains(&b'e') {
797 write!(out, "{}", f).unwrap();
799 } else {
800 out.push_str(s);
801 }
802}
803
804#[inline]
807fn write_string_value<const DELIM: u8>(s: &str, cfg: &Config, out: &mut String) {
808 if value_needs_quoting::<DELIM>(s, cfg.escape_controls) {
809 write_quoted(s, cfg.escape_controls, out);
810 } else {
811 out.push_str(s);
812 }
813}
814
815fn write_key(k: &str, cfg: &Config, out: &mut String) {
816 if key_needs_quoting(k) {
817 write_quoted(k, cfg.escape_controls, out);
818 } else {
819 out.push_str(k);
820 }
821}
822
823#[inline]
829fn key_needs_quoting(s: &str) -> bool {
830 if s.is_empty() {
831 return true;
832 }
833 let bytes = s.as_bytes();
834 let start = match bytes[0] {
835 b'@' | b'$' | b'#' => {
836 if bytes.len() < 2 {
837 return true; }
839 1
840 }
841 _ => 0,
842 };
843 let first = bytes[start];
844 if !(first.is_ascii_alphabetic() || first == b'_') {
845 return true;
846 }
847 for &b in &bytes[start + 1..] {
848 if !(b.is_ascii_alphanumeric() || b == b'_' || b == b'.') {
849 return true;
850 }
851 }
852 false
853}
854
855#[inline]
856fn value_needs_quoting<const DELIM: u8>(s: &str, escape_controls: bool) -> bool {
857 if s.is_empty() {
858 return true;
859 }
860 let bytes = s.as_bytes();
861 match bytes[0] {
862 b'-' | b'#' | b' ' | b'\t' => return true,
863 _ => {}
864 }
865 match bytes[bytes.len() - 1] {
866 b' ' | b'\t' => return true,
867 _ => {}
868 }
869 for &b in bytes {
873 match b {
874 b':' | b'\n' | b'\r' | b'\t' | b'"' | b'\\' | b'[' | b']' | b'{' | b'}' => return true,
878 _ if escape_controls && b < 0x20 => return true,
881 _ if b == DELIM => return true,
882 _ => {}
883 }
884 }
885 if matches!(s, "true" | "false" | "null") {
886 return true;
887 }
888 looks_like_number(bytes)
889}
890
891fn looks_like_number(bytes: &[u8]) -> bool {
894 let mut i = 0;
895 if matches!(bytes[0], b'-' | b'+') {
896 i = 1;
897 if i == bytes.len() {
898 return false;
899 }
900 }
901 let mut has_digit = false;
902 while i < bytes.len() && bytes[i].is_ascii_digit() {
903 has_digit = true;
904 i += 1;
905 }
906 if !has_digit {
907 return false;
908 }
909 if i < bytes.len() && bytes[i] == b'.' {
910 i += 1;
911 let mut has_frac = false;
912 while i < bytes.len() && bytes[i].is_ascii_digit() {
913 has_frac = true;
914 i += 1;
915 }
916 if !has_frac {
917 return false;
918 }
919 }
920 if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
921 i += 1;
922 if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
923 i += 1;
924 }
925 let mut has_exp_digit = false;
926 while i < bytes.len() && bytes[i].is_ascii_digit() {
927 has_exp_digit = true;
928 i += 1;
929 }
930 if !has_exp_digit {
931 return false;
932 }
933 }
934 i == bytes.len()
935}
936
937#[inline]
939fn hex_lower(n: u8) -> u8 {
940 match n {
941 0..=9 => b'0' + n,
942 _ => b'a' + (n - 10),
943 }
944}
945
946fn write_quoted(s: &str, escape_controls: bool, out: &mut String) {
947 out.push('"');
948 let bytes = s.as_bytes();
949 let mut start = 0;
950 for (i, &b) in bytes.iter().enumerate() {
951 let named = matches!(b, b'\\' | b'"' | b'\n' | b'\r' | b'\t');
954 let other_control = escape_controls && b < 0x20;
955 if named || other_control {
956 if start < i {
957 out.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..i]) });
958 }
959 match b {
960 b'\\' => out.push_str("\\\\"),
961 b'"' => out.push_str("\\\""),
962 b'\n' => out.push_str("\\n"),
963 b'\r' => out.push_str("\\r"),
964 b'\t' => out.push_str("\\t"),
965 _ => {
966 out.push_str("\\u00");
968 out.push(hex_lower(b >> 4) as char);
969 out.push(hex_lower(b & 0x0f) as char);
970 }
971 }
972 start = i + 1;
973 }
974 }
975 if start < bytes.len() {
976 out.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..]) });
977 }
978 out.push('"');
979}
980
981#[cfg(test)]
982mod tests {
983 use super::{encode, encode_with, Config};
984
985 fn enc(json: &str) -> String {
986 encode(json.as_bytes()).unwrap()
987 }
988
989 fn enc_with(json: &str, cfg: &Config) -> String {
990 encode_with(json.as_bytes(), cfg).unwrap()
991 }
992
993 #[test]
998 fn test_write_number_small_exponent_expands_to_decimal() {
999 assert_eq!(enc(r#"{"n":1e-6}"#), "n: 0.000001");
1000 assert_eq!(enc(r#"{"n":1e-7}"#), "n: 0.0000001");
1001 }
1002
1003 #[test]
1004 fn test_write_number_integer_valued_float_drops_fraction() {
1005 assert_eq!(enc(r#"{"n":100.0}"#), "n: 100");
1006 assert_eq!(enc(r#"{"n":-2.0}"#), "n: -2");
1007 }
1008
1009 #[test]
1010 fn test_write_number_decimal_full_precision_preserved() {
1011 assert_eq!(enc(r#"{"n":3.14}"#), "n: 3.14");
1012 assert_eq!(enc(r#"{"n":0.3333333333333333}"#), "n: 0.3333333333333333");
1013 assert_eq!(enc(r#"{"n":1234567.89}"#), "n: 1234567.89");
1014 }
1015
1016 #[test]
1017 fn test_write_number_large_magnitude_float_expands_no_exponent() {
1018 assert_eq!(enc(r#"{"n":1e21}"#), "n: 1000000000000000000000");
1019 }
1020
1021 #[test]
1022 fn test_write_number_i64_and_u64_fast_paths() {
1023 assert_eq!(enc(r#"{"n":42}"#), "n: 42");
1024 assert_eq!(
1025 enc(r#"{"n":-9223372036854775808}"#),
1026 "n: -9223372036854775808"
1027 );
1028 assert_eq!(
1029 enc(r#"{"n":18446744073709551615}"#),
1030 "n: 18446744073709551615"
1031 );
1032 }
1033
1034 #[test]
1035 fn test_write_number_beyond_u64_keeps_expanded_form() {
1036 assert_eq!(enc(r#"{"n":1e30}"#), "n: 1000000000000000000000000000000");
1039 }
1040
1041 #[test]
1044 fn test_empty_array_root_is_bare_brackets() {
1045 assert_eq!(enc("[]"), "[]");
1046 }
1047
1048 #[test]
1049 fn test_empty_array_object_field_is_key_bracket() {
1050 assert_eq!(enc(r#"{"a":[]}"#), "a: []");
1051 assert_eq!(enc(r#"{"x":{"a":[]}}"#), "x:\n a: []");
1052 }
1053
1054 #[test]
1055 fn test_empty_array_as_array_element_keeps_legacy_header() {
1056 assert_eq!(enc(r#"{"pairs":[[],[]]}"#), "pairs[2]:\n - [0]:\n - [0]:");
1058 }
1059
1060 #[test]
1061 fn test_empty_array_legacy_form_when_option_off() {
1062 let cfg = Config {
1063 empty_array_bare: false,
1064 ..Config::default()
1065 };
1066 assert_eq!(enc_with("[]", &cfg), "[0]:");
1067 assert_eq!(enc_with(r#"{"a":[]}"#, &cfg), "a[0]:");
1068 }
1069
1070 #[test]
1076 fn test_escape_controls_emits_lowercase_u_escape() {
1077 assert_eq!(enc("{\"s\":\"a\\u001fb\"}"), "s: \"a\\u001fb\"");
1081 assert_eq!(enc("{\"s\":\"a\\u0000b\"}"), "s: \"a\\u0000b\"");
1082 assert_eq!(enc("{\"s\":\"\\u0004\"}"), "s: \"\\u0004\"");
1083 }
1084
1085 #[test]
1086 fn test_escape_controls_keeps_named_escapes() {
1087 assert_eq!(enc(r#"{"s":"a\nb"}"#), "s: \"a\\nb\"");
1088 assert_eq!(enc(r#"{"s":"a\tb"}"#), "s: \"a\\tb\"");
1089 assert_eq!(enc(r#"{"s":"a\rb"}"#), "s: \"a\\rb\"");
1090 }
1091
1092 #[test]
1093 fn test_escape_controls_off_passes_raw_byte() {
1094 let cfg = Config {
1095 escape_controls: false,
1096 ..Config::default()
1097 };
1098 assert_eq!(enc_with("{\"s\":\"a\\u001fb\"}", &cfg), "s: a\u{1f}b");
1099 }
1100
1101 #[test]
1104 fn test_fold_keys_root_chain() {
1105 let cfg = Config {
1106 key_folding: true,
1107 ..Config::default()
1108 };
1109 assert_eq!(enc_with(r#"{"a":{"b":{"c":1}}}"#, &cfg), "a.b.c: 1");
1110 }
1111
1112 #[test]
1113 fn test_fold_keys_restarts_in_multikey_object_body() {
1114 let cfg = Config {
1117 key_folding: true,
1118 ..Config::default()
1119 };
1120 assert_eq!(
1121 enc_with(r#"{"a":{"x":1,"nested":{"b":{"c":2}}}}"#, &cfg),
1122 "a:\n x: 1\n nested.b.c: 2"
1123 );
1124 }
1125
1126 #[test]
1127 fn test_fold_keys_does_not_refold_past_flatten_depth() {
1128 let cfg = Config {
1129 key_folding: true,
1130 flatten_depth: Some(2),
1131 ..Config::default()
1132 };
1133 assert_eq!(
1134 enc_with(r#"{"a":{"b":{"c":{"d":1}}}}"#, &cfg),
1135 "a.b:\n c:\n d: 1"
1136 );
1137 }
1138
1139 #[test]
1140 fn test_fold_keys_skips_sibling_collision_at_any_depth() {
1141 let cfg = Config {
1143 key_folding: true,
1144 ..Config::default()
1145 };
1146 assert_eq!(
1147 enc_with(
1148 r#"{"data":{"meta":{"items":[1,2]}},"data.meta.items":"literal"}"#,
1149 &cfg
1150 ),
1151 "data:\n meta:\n items[2]: 1,2\ndata.meta.items: literal"
1152 );
1153 }
1154
1155 #[test]
1160 fn test_keyed_table_needs_two_entries() {
1161 assert_eq!(enc(r#"{"m":{"a":{"x":1}}}"#), "m:\n a:\n x: 1");
1163 assert_eq!(
1164 enc(r#"{"m":{"a":{"x":1},"b":{"x":2}}}"#),
1165 "m[2:]{x}:\n a: 1\n b: 2"
1166 );
1167 }
1168
1169 #[test]
1170 fn test_keyed_table_rejects_non_uniform_columns() {
1171 assert_eq!(
1174 enc(r#"{"m":{"a":{"x":1},"b":{"y":2}}}"#),
1175 "m:\n a:\n x: 1\n b:\n y: 2"
1176 );
1177 assert_eq!(
1178 enc(r#"{"m":{"a":{"x":1},"b":7}}"#),
1179 "m:\n a:\n x: 1\n b: 7"
1180 );
1181 assert_eq!(
1182 enc(r#"{"m":{"a":{"x":[1]},"b":{"x":[2]}}}"#),
1183 "m:\n a:\n x[1]: 1\n b:\n x[1]: 2"
1184 );
1185 }
1186
1187 #[test]
1188 fn test_keyed_table_not_used_for_array_elements() {
1189 assert_eq!(
1194 enc(r#"{"a":[{"p":{"x":1},"q":{"x":2}},{"p":{"x":3},"q":[9]}]}"#),
1195 "a[2]:\n - p:\n x: 1\n q:\n x: 2\n - p:\n x: 3\n q[1]: 9"
1196 );
1197 }
1198
1199 #[test]
1200 fn test_keyed_eligible_column_becomes_nested_field_group() {
1201 assert_eq!(
1204 enc(r#"{"a":[{"p":{"x":1},"q":{"x":2}}]}"#),
1205 "a[1]{p{x},q{x}}:\n 1,2"
1206 );
1207 }
1208
1209 #[test]
1212 fn test_nested_field_group_rejects_empty_object_column() {
1213 assert_eq!(enc(r#"{"a":[{"n":{}},{"n":{}}]}"#), "a[2]:\n - n:\n - n:");
1216 }
1217
1218 #[test]
1219 fn test_nested_field_group_rejects_mixed_null_and_object_column() {
1220 assert_eq!(
1223 enc(r#"{"a":[{"n":{"x":1}},{"n":null}]}"#),
1224 "a[2]:\n - n:\n x: 1\n - n: null"
1225 );
1226 }
1227
1228 #[test]
1229 fn test_nested_field_group_tolerates_row_key_reordering() {
1230 assert_eq!(
1232 enc(r#"{"a":[{"id":1,"g":{"x":1,"y":2}},{"g":{"y":4,"x":3},"id":2}]}"#),
1233 "a[2]{id,g{x,y}}:\n 1,1,2\n 2,3,4"
1234 );
1235 }
1236
1237 #[test]
1240 fn test_quotes_leading_plus_numeric_like_string() {
1241 assert_eq!(enc(r#"{"a":"+1"}"#), r#"a: "+1""#);
1242 assert_eq!(enc(r#"{"a":"+1.5e-3"}"#), r#"a: "+1.5e-3""#);
1243 assert_eq!(enc(r#"{"a":"+x"}"#), "a: +x");
1245 }
1246
1247 #[test]
1248 fn test_quotes_brackets_and_braces_anywhere_in_value() {
1249 assert_eq!(enc(r#"{"a":"x[1]"}"#), r#"a: "x[1]""#);
1250 assert_eq!(enc(r#"{"a":"a}b"}"#), r#"a: "a}b""#);
1251 }
1252
1253 #[test]
1256 fn test_max_depth_rejects_overdeep_input_before_parse() {
1257 let deep: Vec<u8> = b"["
1260 .iter()
1261 .cycle()
1262 .take(100)
1263 .chain(b"1".iter())
1264 .chain(b"]".iter().cycle().take(100))
1265 .copied()
1266 .collect();
1267 let cfg = Config {
1268 max_depth: 10,
1269 ..Config::default()
1270 };
1271 let err = encode_with(&deep, &cfg).unwrap_err();
1272 assert!(err.contains("max_depth"), "got: {err}");
1273 }
1274
1275 #[test]
1276 fn test_max_depth_default_allows_normal_nesting() {
1277 assert_eq!(enc(r#"{"a":{"b":{"c":1}}}"#), "a:\n b:\n c: 1");
1279 }
1280
1281 #[test]
1282 fn test_max_depth_ignores_brackets_inside_strings() {
1283 let cfg = Config {
1285 max_depth: 2,
1286 ..Config::default()
1287 };
1288 assert_eq!(
1289 enc_with(r#"{"s":"[[[[[deep]]]]]"}"#, &cfg),
1290 r#"s: "[[[[[deep]]]]]""#
1291 );
1292 }
1293
1294 #[test]
1297 fn test_max_input_bytes_rejects_oversize_input() {
1298 let cfg = Config {
1299 max_input_bytes: 4,
1300 ..Config::default()
1301 };
1302 let err = encode_with(br#"{"a":1}"#, &cfg).unwrap_err();
1303 assert!(err.contains("max_input_bytes"), "got: {err}");
1304 }
1305
1306 #[test]
1307 fn test_max_input_bytes_zero_disables_check() {
1308 assert_eq!(enc(r#"{"a":1}"#), "a: 1");
1310 }
1311
1312 #[test]
1315 fn test_ryu_regular_floats_match_spec_form() {
1316 assert_eq!(enc(r#"{"n":2.5}"#), "n: 2.5");
1318 assert_eq!(enc(r#"{"n":99.99}"#), "n: 99.99");
1319 assert_eq!(enc(r#"{"n":0.1}"#), "n: 0.1");
1320 assert_eq!(enc(r#"{"n":-0.0625}"#), "n: -0.0625");
1321 }
1322}