Skip to main content

_etoon/
toon.rs

1//! TOON encoder core (sonic-rs backend).
2//!
3//! Input: JSON bytes (from orjson.dumps on Python side).
4//! Output: TOON string, matching TOON spec v4.1.
5//!
6//! Delimiter is monomorphized via const generics (`DELIM: u8`) so the
7//! byte-match inner loops fold away when emitting default-comma output.
8
9use sonic_rs::{Array, JsonContainerTrait, JsonType, JsonValueTrait, Object, Value};
10use std::fmt::Write as _;
11
12/// Encoder configuration. The spec's only encoder options are `delimiter` and
13/// `indentSize` (§13); the rest are etoon extensions or resource guards.
14#[derive(Clone, Copy)]
15pub struct Config {
16    /// Delimiter between array/tabular values. Must be `,`, `\t`, or `|`.
17    pub delimiter: u8,
18    /// If true, fold single-key object chains into dot-notation keys (safe
19    /// mode). An etoon extension: the spec removed key folding in v4.0, so
20    /// nothing re-nests the output.
21    pub key_folding: bool,
22    /// Max fold depth (segments). None = unlimited. 0 disables folding.
23    pub flatten_depth: Option<usize>,
24    /// If true, emit empty arrays as canonical `[]` / `key: []` instead of the
25    /// legacy `[0]:` / `key[0]:` length-marker form. False emits output the
26    /// spec has forbidden since v3.1.
27    pub empty_array_bare: bool,
28    /// If true, escape control chars U+0000–U+001F (except the named `\n` `\r`
29    /// `\t`) as `\uXXXX` with lowercase hex. False emits output the spec has
30    /// forbidden since v3.1.
31    pub escape_controls: bool,
32    /// Max JSON nesting depth. Input deeper than this is rejected before
33    /// parsing, so neither the sonic-rs DOM parser nor the recursive emitter
34    /// can overflow the stack (both crash the host process near depth ~50k).
35    /// 0 disables the check — use only when the input's depth is already
36    /// bounded by the producer (e.g. orjson output, capped by CPython's
37    /// recursion limit), since the pre-scan is then redundant.
38    pub max_depth: usize,
39    /// Max input size in bytes. 0 disables the check (default). A caller that
40    /// encodes untrusted input can set this to bound peak memory.
41    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    // Reject over-deep input up front: the sonic-rs DOM parser and this
71    // emitter both recurse per nesting level and overflow the stack on
72    // deeply-nested input. This O(n) pre-scan caps depth before either runs.
73    // max_depth == 0 skips it (caller guarantees depth is already bounded).
74    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                // Root keyed tabular header is keyless: `[N:]{fields}:` (§9.5).
100                write_keyed_table::<DELIM>(m, &fields, 0, cfg, out);
101            } else if !m.is_empty() {
102                // Folding is attempted at the top-level object; nested object
103                // bodies re-apply it via write_value_after_key (spec §13.4).
104                write_object_body::<DELIM>(m, 0, cfg, cfg.key_folding, out);
105            }
106        }
107        JsonType::Array => {
108            let arr = v.as_array().unwrap();
109            // Root empty array: canonical bare `[]` (no leading colon).
110            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    // Key segments must match TOON identifier pattern (safe mode).
155    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                // Keyed tabular form replaces the nested object body; the
207                // header attaches directly to the key just written (§9.5).
208                write_keyed_table::<DELIM>(child, &fields, key_indent, cfg, out);
209            } else {
210                out.push_str(":\n");
211                // Folding restarts only at a branch point (multi-key object).
212                // A single-key body is part of a chain whose fold decision was
213                // already made by the parent's try_fold — re-folding it would
214                // wrongly bypass collision/flattenDepth stops (spec §13.4).
215                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            // Object value: canonical `key: []`; legacy `key[0]:` otherwise.
222            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
235/// Emit the legacy empty-array header `[0<delim?>]:` at the current position.
236/// Used in list-item context, where v3.1 keeps this form (SPEC §9.2).
237fn 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
245/// Emit a field list `{f1<delim>f2{sub}<delim>…}` for a tabular or keyed header,
246/// recursing into nested field groups (§9.3).
247fn 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
264/// Emit one row's cells in depth-first pre-order of the field list, so the cell
265/// count equals the header's leaf-field count (§9.3).
266fn 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
290/// Emit the keyed tabular body `[N:<delim?>]{fields}:` plus one entry row per
291/// entry (§9.5). The caller has already written the key, if any — at the root
292/// the header is keyless.
293fn 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    // A keyless fields-bearing header is valid only at the document root (§6),
355    // so an array sitting in list-item position takes list form even when its
356    // elements would otherwise be tabular-eligible (§9.4).
357    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        // Writes the field list inline rather than through write_field_list:
377        // flat tables are the hot path, and routing them through `Field` would
378        // allocate a tree for a list of names that are all leaves.
379        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            // List-item position: no keyless tabular header here (§9.4).
440            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
462// ==================== Depth guard ====================
463
464/// Per-byte structural class for the depth scanner. Most bytes are `Other`
465/// (digits, whitespace, separators, string content) and cost a single table
466/// lookup + skip, so the scan stays close to memory bandwidth.
467const 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
481/// Single linear pass over the raw JSON bytes tracking `{`/`[` nesting depth,
482/// skipping brackets inside string literals. Returns `Some(depth)` with the
483/// first depth that exceeds `max_depth`, or `None` if the input stays within
484/// bounds. No allocation; bails out as soon as the limit is crossed.
485///
486/// String interiors are skipped with `memchr` (SIMD), so quoted content — the
487/// bulk of typical payloads — costs near-zero, and the scalar loop only sees
488/// structural bytes.
489fn 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                // Skip to the closing quote, honoring backslash escapes. Each
508                // memchr2 jumps straight to the next `"` or `\`.
509                i += 1;
510                loop {
511                    // No `"` or `\` left: the string is unterminated, so there
512                    // is no further nesting to find.
513                    let p = memchr::memchr2(b'"', b'\\', &bytes[i..])?;
514                    if bytes[i + p] == b'"' {
515                        i += p + 1;
516                        break;
517                    }
518                    // backslash: skip the escaped byte
519                    i += p + 2;
520                    if i >= n {
521                        return None;
522                    }
523                }
524            }
525            _ => i += 1,
526        }
527    }
528    None
529}
530
531// ==================== Helpers ====================
532
533const 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
560/// One column of a tabular header (spec §9.3): a bare leaf field, or a nested
561/// field group whose sub-columns are themselves leaves or groups. Nesting depth
562/// is unbounded.
563enum Field<'a> {
564    Leaf(&'a str),
565    Group(&'a str, Vec<Field<'a>>),
566}
567
568/// Tabular shape of an array of objects (§9.3).
569enum Table<'a> {
570    /// Every column is uniform-primitive. The flag records whether all rows
571    /// share the first row's key order, letting cells be emitted by iterating
572    /// values in place instead of looking each key up.
573    Flat(Vec<&'a str>, bool),
574    /// At least one nested-uniform column, emitted as a nested field group.
575    Nested(Vec<Field<'a>>),
576}
577
578/// Value of column `k` in `m`. Rows normally share the header's key order, so
579/// try position `idx` first and fall back to a lookup only when it differs.
580#[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/// First-row probe for the §9.3 column rules: an array value or an empty object
589/// disqualifies its column outright, so a mismatch is visible from one object
590/// alone. Callers use it to bail in O(columns) before collecting every row;
591/// `build_fields` re-checks each column itself.
592#[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
602/// Field tree shared by `objs` (§9.3 column classification), or None when any
603/// column is neither uniform-primitive nor nested-uniform. Also used for the
604/// entry values of a keyed tabular object (§9.5).
605fn 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            // Arrays disqualify the column outright; so does any row whose
636            // value at this key is not a primitive.
637            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    // Flat detection bails at the first non-primitive value, but a column of
656    // uniform objects still qualifies as a nested field group (§9.3), so retry
657    // with the recursive walk. Probe the first element before walking all of
658    // them: with no object column there is nothing the flat pass missed, and a
659    // disqualifying value is usually already visible here — that keeps the
660    // common mixed-array case (a tabular-looking array with one list column)
661    // from paying for a full scan on its way to list form.
662    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
678/// Field tree when `m` qualifies for keyed tabular form (§9.5): at least two
679/// entries, every entry value a non-empty object, one shared key set, and every
680/// column uniform-primitive or nested-uniform.
681fn keyed_fields<'a>(m: &'a Object) -> Option<Vec<Field<'a>>> {
682    if m.len() < 2 {
683        return None;
684    }
685    // Cheap reject before allocating: most objects fail on their first entry.
686    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// ==================== Scalar ====================
737
738#[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    // Non-integer or beyond u64: format once via write_float. (The old code
765    // also called v.to_string() first just to probe for a decimal point,
766    // formatting floats twice — dropping that probe is ~3x faster here.)
767    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    // Integer-valued float in i64 range: itoa is faster than float formatting.
784    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    // ryu is ~2.4x faster than std Display for non-integer floats, but emits
790    // scientific notation for very small/large magnitudes (1e-6, 1e21) which
791    // violates TOON's expanded-decimal form. Use ryu when its output has no
792    // exponent (the common LLM-payload case); otherwise fall back to std
793    // Display, which always expands.
794    let mut buf = ryu::Buffer::new();
795    let s = buf.format_finite(f);
796    if s.as_bytes().contains(&b'e') {
797        // std Display gives spec-canonical decimals (expanded, no trailing zeros).
798        write!(out, "{}", f).unwrap();
799    } else {
800        out.push_str(s);
801    }
802}
803
804// ==================== String ====================
805
806#[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/// Keys must match TOON identifier pattern: `[@$#a-zA-Z_][a-zA-Z0-9_.]*`.
824/// Sigil prefixes `@`, `$`, `#` are allowed for ecosystem compatibility:
825/// - `@` : AWS CloudWatch, Elasticsearch, Serilog, XML→JSON
826/// - `$` : MongoDB, JSON Schema, AWS CloudFormation
827/// - `#` : JSON-LD, Azure Resource Manager
828#[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; // bare sigil needs quoting
838            }
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    // DELIM is a compile-time constant, so this match collapses into the
870    // single match arm below when DELIM is in {',', '\t'} (already included),
871    // and stays as a separate branch only for DELIM = '|'.
872    for &b in bytes {
873        match b {
874            // Brackets and braces anywhere in the value, not just at position 0
875            // (spec §7.2) — an unquoted `]` would otherwise close a header the
876            // decoder is scanning.
877            b':' | b'\n' | b'\r' | b'\t' | b'"' | b'\\' | b'[' | b']' | b'{' | b'}' => return true,
878            // Other U+0000–U+001F controls force quoting so write_quoted can
879            // emit `\u00XX` (TOON spec v3.1); only when the option is on.
880            _ 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
891/// Numeric-like per spec §7.2: `^[+-]?[0-9]+(?:\.[0-9]+)?(?:e[+-]?[0-9]+)?$`.
892/// The leading sign includes `+`, so `"+1"` is quoted and survives round-trip.
893fn 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/// Lowercase hex digit for nibble `n` (0–15).
938#[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        // Named escapes always apply; other U+0000–U+001F controls become
952        // `\u00XX` only when escape_controls is on (TOON spec v3.1).
953        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                    // \u00XX, lowercase hex (b < 0x20 so high nibble is 0 or 1)
967                    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    // ── Number formatting (JSON → Rust path; no Python repr to lean on) ──
994    // Pin the decimal canonicalization the to_string-probe removal relies on:
995    // std Display must expand small exponents and drop integer-valued `.0`.
996
997    #[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        // sonic-rs parses this through f64 (precision lost at parse time), but
1037        // the output must stay an expanded integer string, not an exponent.
1038        assert_eq!(enc(r#"{"n":1e30}"#), "n: 1000000000000000000000000000000");
1039    }
1040
1041    // ── Empty arrays (spec v3.1 canonical, default on) ──
1042
1043    #[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        // SPEC §9.2: a bare array element that is itself empty stays `- [0]:`.
1057        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    // ── Control-character escaping (spec v3.1, default on) ──
1071
1072    // Control chars can only enter via JSON `\uXXXX` escapes — strict JSON
1073    // (sonic-rs) rejects raw control bytes in string literals. This mirrors the
1074    // Python path: orjson escapes them before they reach the Rust encoder.
1075    #[test]
1076    fn test_escape_controls_emits_lowercase_u_escape() {
1077        // Control chars enter only via JSON \uXXXX escapes; strict JSON
1078        // (sonic-rs) rejects raw control bytes. Mirrors the Python path where
1079        // orjson escapes them before they reach the Rust encoder.
1080        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    // ── Key folding at depth (spec §13.4) ──
1102
1103    #[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        // The single-key chain nested→b→c sits inside multi-key object `a`, so
1115        // folding restarts there and produces `nested.b.c`.
1116        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        // A top-level literal `data.meta.items` blocks folding the whole chain.
1142        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    // ── Keyed tabular form (spec §9.5) ──
1156    // The happy paths live in tests/fixtures/encode/objects-keyed.json; these
1157    // pin the detection boundaries, where the object must stay nested.
1158
1159    #[test]
1160    fn test_keyed_table_needs_two_entries() {
1161        // A single entry stays nested — the header would cost more than it saves.
1162        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        // Mismatched key sets, a non-object entry, and an array column each
1172        // disqualify the whole object (§9.5 detection).
1173        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        // The `q` column mixes an object with an array, so the array takes list
1190        // form. Its first element is keyed-eligible on its own (two entries,
1191        // one shared key set) but stays nested: array elements are anonymous
1192        // and there is no `- [N:]{…}:` list item (§9.5, §10).
1193        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        // In a tabular column, a keyed-eligible object encodes as a nested
1202        // field group rather than a keyed table (§9.5).
1203        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    // ── Nested field groups (spec §9.3) ──
1210
1211    #[test]
1212    fn test_nested_field_group_rejects_empty_object_column() {
1213        // A column of empty objects has no subfields to declare, so the array
1214        // falls back to list form.
1215        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        // `null` is a primitive, so the column is neither uniform-primitive nor
1221        // nested-uniform (§9.3) and the array takes list form.
1222        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        // Key order may vary per element; cells still follow the header order.
1231        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    // ── String quoting (spec §7.2) ──
1238
1239    #[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        // A plus that does not form a number stays unquoted.
1244        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    // ── Depth guard (P0: prevents sonic-rs/emitter stack overflow) ──
1254
1255    #[test]
1256    fn test_max_depth_rejects_overdeep_input_before_parse() {
1257        // Depth far below the ~50k crash threshold, but past a small limit:
1258        // must return Err, never overflow the stack.
1259        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        // Ordinary nesting (well under default 1000) encodes fine.
1278        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        // Brackets in string literals must not count toward depth.
1284        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    // ── Input-size guard (P1: OOM protection, off by default) ──
1295
1296    #[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        // Default (0) imposes no limit.
1309        assert_eq!(enc(r#"{"a":1}"#), "a: 1");
1310    }
1311
1312    // ── ryu float fast path keeps spec-canonical output ──
1313
1314    #[test]
1315    fn test_ryu_regular_floats_match_spec_form() {
1316        // Common-range floats go through ryu (no exponent) and stay expanded.
1317        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}