Skip to main content

sqz_engine/
toon.rs

1/// TOON (Token-Optimized Object Notation) encoder/decoder.
2///
3/// Produces lossless, compact representations of JSON values that use
4/// 30-60% fewer tokens than standard JSON formatting.
5///
6/// Format: `TOON:<encoded>`
7///
8/// Encoding rules:
9/// - Objects: `{k:v,k:v}` — quotes dropped on simple keys, minimal separators
10/// - Arrays:  `[v,v,v]`   — no spaces
11/// - Arrays of objects: `[#k1,k2\nv,v\nv,v]` when shorter: a header row of
12///   keys, then one line per object. An empty cell means the key is absent.
13/// - Strings: `"..."` with minimal escaping; control characters are escaped,
14///   other text stays as-is
15/// - Numbers, booleans, null: compact as-is
16use crate::error::{Result, SqzError};
17
18const TOON_PREFIX: &str = "TOON:";
19
20pub struct ToonEncoder;
21
22impl ToonEncoder {
23    /// Encode a JSON value into a compact TOON string.
24    pub fn encode(&self, json: &serde_json::Value) -> Result<String> {
25        let mut buf = String::with_capacity(128);
26        buf.push_str(TOON_PREFIX);
27        encode_value(json, &mut buf);
28        Ok(buf)
29    }
30
31    /// Decode a TOON-encoded string back to a JSON value.
32    pub fn decode(&self, encoded: &str) -> Result<serde_json::Value> {
33        let body = encoded
34            .strip_prefix(TOON_PREFIX)
35            .ok_or_else(|| SqzError::Other("not a TOON string: missing prefix".into()))?;
36        let mut parser = Parser::new(body);
37        let value = parser
38            .parse_value()
39            .map_err(|e| SqzError::Other(format!("TOON decode error: {e}")))?;
40        parser
41            .expect_eof()
42            .map_err(|e| SqzError::Other(format!("TOON decode error: {e}")))?;
43        Ok(value)
44    }
45
46    /// Return true if `input` looks like valid JSON.
47    /// Used by the pipeline to decide whether to apply TOON encoding.
48    pub fn is_json(input: &str) -> bool {
49        let trimmed = input.trim();
50        if trimmed.is_empty() {
51            return false;
52        }
53        serde_json::from_str::<serde_json::Value>(trimmed).is_ok()
54    }
55}
56
57// ---------------------------------------------------------------------------
58// Encoder helpers
59// ---------------------------------------------------------------------------
60
61fn encode_value(v: &serde_json::Value, buf: &mut String) {
62    encode_value_in(v, buf, true);
63}
64
65/// `tables` is off inside table cells so every row stays on one line.
66fn encode_value_in(v: &serde_json::Value, buf: &mut String, tables: bool) {
67    match v {
68        serde_json::Value::Null => buf.push_str("null"),
69        serde_json::Value::Bool(b) => buf.push_str(if *b { "true" } else { "false" }),
70        serde_json::Value::Number(n) => {
71            // Use serde_json's own serializer to preserve full f64 precision.
72            // n.to_string() can lose precision for some f64 values.
73            buf.push_str(&serde_json::to_string(&serde_json::Value::Number(n.clone()))
74                .unwrap_or_else(|_| n.to_string()));
75        }
76        serde_json::Value::String(s) => encode_string(s, buf),
77        serde_json::Value::Array(arr) => {
78            if tables && encode_table(arr, buf) {
79                return;
80            }
81            buf.push('[');
82            for (i, item) in arr.iter().enumerate() {
83                if i > 0 {
84                    buf.push(',');
85                }
86                encode_value_in(item, buf, tables);
87            }
88            buf.push(']');
89        }
90        serde_json::Value::Object(map) => {
91            buf.push('{');
92            for (i, (k, val)) in map.iter().enumerate() {
93                if i > 0 {
94                    buf.push(',');
95                }
96                encode_key(k, buf);
97                buf.push(':');
98                encode_value_in(val, buf, tables);
99            }
100            buf.push('}');
101        }
102    }
103}
104
105fn encode_key(k: &str, buf: &mut String) {
106    if is_simple_key(k) {
107        buf.push_str(k);
108    } else {
109        encode_string(k, buf);
110    }
111}
112
113/// Write `arr` as a table when every element is an object, at least half
114/// the cells are filled, and the table is shorter than the plain form.
115/// Returns false, writing nothing, otherwise.
116fn encode_table(arr: &[serde_json::Value], buf: &mut String) -> bool {
117    if arr.len() < 2 {
118        return false;
119    }
120    let mut keys: Vec<&str> = Vec::new();
121    let mut seen = std::collections::HashSet::new();
122    let mut filled = 0usize;
123    for item in arr {
124        let serde_json::Value::Object(map) = item else {
125            return false;
126        };
127        filled += map.len();
128        for k in map.keys() {
129            if seen.insert(k.as_str()) {
130                keys.push(k);
131            }
132        }
133    }
134    if keys.is_empty() || filled * 2 < arr.len() * keys.len() {
135        return false;
136    }
137
138    let mut table = String::from("[#");
139    for (i, k) in keys.iter().enumerate() {
140        if i > 0 {
141            table.push(',');
142        }
143        encode_key(k, &mut table);
144    }
145    for item in arr {
146        table.push('\n');
147        for (i, k) in keys.iter().enumerate() {
148            if i > 0 {
149                table.push(',');
150            }
151            if let Some(val) = item.get(*k) {
152                encode_value_in(val, &mut table, false);
153            }
154        }
155    }
156    table.push(']');
157
158    let mut plain = String::from("[");
159    for (i, item) in arr.iter().enumerate() {
160        if i > 0 {
161            plain.push(',');
162        }
163        encode_value_in(item, &mut plain, false);
164    }
165    plain.push(']');
166
167    if table.len() < plain.len() {
168        buf.push_str(&table);
169        true
170    } else {
171        false
172    }
173}
174
175/// Encode a string with minimal escaping, wrapped in double quotes.
176/// Control characters and line separators are escaped; everything else,
177/// non-ASCII text included, is written as-is.
178fn encode_string(s: &str, buf: &mut String) {
179    buf.push('"');
180    for ch in s.chars() {
181        match ch {
182            '"' => buf.push_str("\\\""),
183            '\\' => buf.push_str("\\\\"),
184            '\n' => buf.push_str("\\n"),
185            '\r' => buf.push_str("\\r"),
186            '\t' => buf.push_str("\\t"),
187            c if c.is_control() || c == '\u{2028}' || c == '\u{2029}' => {
188                buf.push_str(&format!("\\u{:04x}", c as u32));
189            }
190            c => buf.push(c),
191        }
192    }
193    buf.push('"');
194}
195
196/// A key is "simple" (can be written without quotes) when it:
197/// - is non-empty
198/// - starts with an ASCII letter or underscore
199/// - contains only ASCII alphanumerics or underscores
200/// - is not a JSON keyword
201fn is_simple_key(k: &str) -> bool {
202    if k.is_empty() {
203        return false;
204    }
205    let mut chars = k.chars();
206    let first = chars.next().unwrap();
207    if !first.is_ascii_alphabetic() && first != '_' {
208        return false;
209    }
210    if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
211        return false;
212    }
213    !matches!(k, "true" | "false" | "null")
214}
215
216// ---------------------------------------------------------------------------
217// Decoder (hand-rolled parser for TOON notation)
218// ---------------------------------------------------------------------------
219
220struct Parser<'a> {
221    src: &'a [u8],
222    pos: usize,
223}
224
225impl<'a> Parser<'a> {
226    fn new(s: &'a str) -> Self {
227        Self {
228            src: s.as_bytes(),
229            pos: 0,
230        }
231    }
232
233    fn peek(&self) -> Option<u8> {
234        self.src.get(self.pos).copied()
235    }
236
237    fn advance(&mut self) -> Option<u8> {
238        let b = self.src.get(self.pos).copied();
239        if b.is_some() {
240            self.pos += 1;
241        }
242        b
243    }
244
245    fn expect_byte(&mut self, expected: u8) -> std::result::Result<(), String> {
246        match self.advance() {
247            Some(b) if b == expected => Ok(()),
248            Some(b) => Err(format!(
249                "expected '{}' got '{}' at pos {}",
250                expected as char, b as char, self.pos - 1
251            )),
252            None => Err(format!("unexpected EOF, expected '{}'", expected as char)),
253        }
254    }
255
256    fn expect_eof(&self) -> std::result::Result<(), String> {
257        if self.pos == self.src.len() {
258            Ok(())
259        } else {
260            Err(format!(
261                "trailing data at pos {}: {:?}",
262                self.pos,
263                &self.src[self.pos..]
264            ))
265        }
266    }
267
268    fn parse_value(&mut self) -> std::result::Result<serde_json::Value, String> {
269        match self.peek() {
270            Some(b'{') => self.parse_object(),
271            Some(b'[') => self.parse_array(),
272            Some(b'"') => Ok(serde_json::Value::String(self.parse_string()?)),
273            Some(b't') => {
274                self.expect_literal(b"true")?;
275                Ok(serde_json::Value::Bool(true))
276            }
277            Some(b'f') => {
278                self.expect_literal(b"false")?;
279                Ok(serde_json::Value::Bool(false))
280            }
281            Some(b'n') => {
282                self.expect_literal(b"null")?;
283                Ok(serde_json::Value::Null)
284            }
285            Some(b'-') | Some(b'0'..=b'9') => self.parse_number(),
286            Some(b) => Err(format!("unexpected byte '{}' at pos {}", b as char, self.pos)),
287            None => Err("unexpected EOF".into()),
288        }
289    }
290
291    fn expect_literal(&mut self, lit: &[u8]) -> std::result::Result<(), String> {
292        for &expected in lit {
293            match self.advance() {
294                Some(b) if b == expected => {}
295                Some(b) => {
296                    return Err(format!(
297                        "expected '{}' got '{}' at pos {}",
298                        expected as char,
299                        b as char,
300                        self.pos - 1
301                    ))
302                }
303                None => return Err("unexpected EOF in literal".into()),
304            }
305        }
306        Ok(())
307    }
308
309    fn parse_object(&mut self) -> std::result::Result<serde_json::Value, String> {
310        self.expect_byte(b'{')?;
311        let mut map = serde_json::Map::new();
312
313        if self.peek() == Some(b'}') {
314            self.advance();
315            return Ok(serde_json::Value::Object(map));
316        }
317
318        loop {
319            let key = self.parse_key()?;
320            self.expect_byte(b':')?;
321            let val = self.parse_value()?;
322            map.insert(key, val);
323
324            match self.peek() {
325                Some(b',') => {
326                    self.advance();
327                }
328                Some(b'}') => {
329                    self.advance();
330                    break;
331                }
332                Some(b) => {
333                    return Err(format!(
334                        "expected ',' or '}}' got '{}' at pos {}",
335                        b as char, self.pos
336                    ))
337                }
338                None => return Err("unexpected EOF in object".into()),
339            }
340        }
341        Ok(serde_json::Value::Object(map))
342    }
343
344    /// Parse either a quoted string key or an unquoted simple key.
345    fn parse_key(&mut self) -> std::result::Result<String, String> {
346        match self.peek() {
347            Some(b'"') => self.parse_string(),
348            Some(b) if (b as char).is_ascii_alphabetic() || b == b'_' => {
349                self.parse_bare_key()
350            }
351            Some(b) => Err(format!(
352                "expected key at pos {}, got '{}'",
353                self.pos,
354                b as char
355            )),
356            None => Err("unexpected EOF expecting key".into()),
357        }
358    }
359
360    /// Parse an unquoted key: [a-zA-Z_][a-zA-Z0-9_]*
361    fn parse_bare_key(&mut self) -> std::result::Result<String, String> {
362        let start = self.pos;
363        while let Some(b) = self.peek() {
364            if (b as char).is_ascii_alphanumeric() || b == b'_' {
365                self.advance();
366            } else {
367                break;
368            }
369        }
370        let key = std::str::from_utf8(&self.src[start..self.pos])
371            .map_err(|e| e.to_string())?
372            .to_owned();
373        Ok(key)
374    }
375
376    fn parse_array(&mut self) -> std::result::Result<serde_json::Value, String> {
377        self.expect_byte(b'[')?;
378        if self.peek() == Some(b'#') {
379            return self.parse_table();
380        }
381        let mut arr = Vec::new();
382
383        if self.peek() == Some(b']') {
384            self.advance();
385            return Ok(serde_json::Value::Array(arr));
386        }
387
388        loop {
389            arr.push(self.parse_value()?);
390            match self.peek() {
391                Some(b',') => {
392                    self.advance();
393                }
394                Some(b']') => {
395                    self.advance();
396                    break;
397                }
398                Some(b) => {
399                    return Err(format!(
400                        "expected ',' or ']' got '{}' at pos {}",
401                        b as char, self.pos
402                    ))
403                }
404                None => return Err("unexpected EOF in array".into()),
405            }
406        }
407        Ok(serde_json::Value::Array(arr))
408    }
409
410    /// Parse the rest of a table after `[`: `#k1,k2` then one `\n`-led row
411    /// per object. Each row has one cell per key; an empty cell is a key the
412    /// object doesn't have.
413    fn parse_table(&mut self) -> std::result::Result<serde_json::Value, String> {
414        self.expect_byte(b'#')?;
415        let mut keys = vec![self.parse_key()?];
416        while self.peek() == Some(b',') {
417            self.advance();
418            keys.push(self.parse_key()?);
419        }
420        let mut rows = Vec::new();
421        while self.peek() == Some(b'\n') {
422            self.advance();
423            let mut map = serde_json::Map::new();
424            for (i, key) in keys.iter().enumerate() {
425                if i > 0 {
426                    self.expect_byte(b',')?;
427                }
428                if !matches!(self.peek(), Some(b',' | b'\n' | b']') | None) {
429                    map.insert(key.clone(), self.parse_value()?);
430                }
431            }
432            rows.push(serde_json::Value::Object(map));
433        }
434        self.expect_byte(b']')?;
435        Ok(serde_json::Value::Array(rows))
436    }
437
438    /// Parse a JSON-style quoted string (handles standard escape sequences).
439    /// Multi-byte UTF-8 sequences are accumulated as raw bytes and decoded at
440    /// the end, so non-ASCII characters survive the round-trip intact.
441    fn parse_string(&mut self) -> std::result::Result<String, String> {
442        self.expect_byte(b'"')?;
443        let mut bytes: Vec<u8> = Vec::new();
444        loop {
445            match self.advance() {
446                None => return Err("unterminated string".into()),
447                Some(b'"') => break,
448                Some(b'\\') => {
449                    match self.advance() {
450                        Some(b'"') => bytes.push(b'"'),
451                        Some(b'\\') => bytes.push(b'\\'),
452                        Some(b'/') => bytes.push(b'/'),
453                        Some(b'n') => bytes.push(b'\n'),
454                        Some(b'r') => bytes.push(b'\r'),
455                        Some(b't') => bytes.push(b'\t'),
456                        Some(b'b') => bytes.push(b'\x08'),
457                        Some(b'f') => bytes.push(b'\x0C'),
458                        Some(b'u') => {
459                            // \uXXXX — decode to char then re-encode as UTF-8.
460                            // Handle UTF-16 surrogate pairs for supplementary
461                            // characters (U+10000 and above).
462                            let hex = self.take_n(4)?;
463                            let code = u32::from_str_radix(&hex, 16)
464                                .map_err(|e| format!("bad \\u escape: {e}"))?;
465
466                            let ch = if (0xD800..=0xDBFF).contains(&code) {
467                                // High surrogate — expect \uXXXX low surrogate next
468                                self.expect_byte(b'\\')?;
469                                self.expect_byte(b'u')?;
470                                let hex2 = self.take_n(4)?;
471                                let low = u32::from_str_radix(&hex2, 16)
472                                    .map_err(|e| format!("bad \\u escape in low surrogate: {e}"))?;
473                                if !(0xDC00..=0xDFFF).contains(&low) {
474                                    return Err(format!("expected low surrogate, got U+{low:04X}"));
475                                }
476                                let scalar = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
477                                char::from_u32(scalar)
478                                    .ok_or_else(|| format!("invalid surrogate pair scalar U+{scalar:X}"))?
479                            } else {
480                                char::from_u32(code)
481                                    .ok_or_else(|| format!("invalid unicode codepoint {code}"))?
482                            };
483
484                            let mut tmp = [0u8; 4];
485                            let encoded = ch.encode_utf8(&mut tmp);
486                            bytes.extend_from_slice(encoded.as_bytes());
487                        }
488                        Some(b) => {
489                            return Err(format!("unknown escape \\{}", b as char))
490                        }
491                        None => return Err("EOF in escape".into()),
492                    }
493                }
494                Some(b) => {
495                    // Accumulate raw bytes; multi-byte UTF-8 sequences are
496                    // stored byte-by-byte and decoded together at the end.
497                    bytes.push(b);
498                }
499            }
500        }
501        String::from_utf8(bytes).map_err(|e| format!("invalid UTF-8 in string: {e}"))
502    }
503
504    fn take_n(&mut self, n: usize) -> std::result::Result<String, String> {
505        if self.pos + n > self.src.len() {
506            return Err("unexpected EOF".into());
507        }
508        let slice = &self.src[self.pos..self.pos + n];
509        self.pos += n;
510        std::str::from_utf8(slice)
511            .map(|s| s.to_owned())
512            .map_err(|e| e.to_string())
513    }
514
515    fn parse_number(&mut self) -> std::result::Result<serde_json::Value, String> {
516        let start = self.pos;
517        // Consume optional leading minus
518        if self.peek() == Some(b'-') {
519            self.advance();
520        }
521        // Integer part
522        while matches!(self.peek(), Some(b'0'..=b'9')) {
523            self.advance();
524        }
525        // Optional fractional part
526        if self.peek() == Some(b'.') {
527            self.advance();
528            while matches!(self.peek(), Some(b'0'..=b'9')) {
529                self.advance();
530            }
531        }
532        // Optional exponent
533        if matches!(self.peek(), Some(b'e') | Some(b'E')) {
534            self.advance();
535            if matches!(self.peek(), Some(b'+') | Some(b'-')) {
536                self.advance();
537            }
538            while matches!(self.peek(), Some(b'0'..=b'9')) {
539                self.advance();
540            }
541        }
542        let num_str = std::str::from_utf8(&self.src[start..self.pos])
543            .map_err(|e| e.to_string())?;
544        let n: serde_json::Number = num_str
545            .parse()
546            .map_err(|e| format!("bad number '{num_str}': {e}"))?;
547        Ok(serde_json::Value::Number(n))
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    use proptest::prelude::*;
555    use serde_json::json;
556
557    // ---------------------------------------------------------------------------
558    // Property-based test: Property 20 — TOON encoding round-trip
559    // Validates: Requirements 13.3, 13.4
560    // ---------------------------------------------------------------------------
561
562    /// Recursive strategy that generates arbitrary serde_json::Value instances,
563    /// including nested objects and arrays. f64 values are restricted to finite
564    /// values only (NaN != NaN, so NaN cannot survive a round-trip comparison).
565    fn arb_json_value() -> impl Strategy<Value = serde_json::Value> {
566        let leaf = prop_oneof![
567            Just(serde_json::Value::Null),
568            any::<bool>().prop_map(serde_json::Value::Bool),
569            any::<i64>().prop_map(|n| serde_json::json!(n)),
570            any::<f64>()
571                .prop_filter("must be finite", |f| f.is_finite())
572                .prop_map(|f| serde_json::json!(f)),
573            ".*".prop_map(serde_json::Value::String),
574        ];
575
576        leaf.prop_recursive(
577            4,   // max depth
578            64,  // max total nodes
579            8,   // max items per collection
580            |inner| {
581                prop_oneof![
582                    // Array of arbitrary values
583                    prop::collection::vec(inner.clone(), 0..8)
584                        .prop_map(serde_json::Value::Array),
585                    // Object with arbitrary string keys and arbitrary values
586                    prop::collection::hash_map(".*", inner, 0..8).prop_map(|m| {
587                        serde_json::Value::Object(m.into_iter().collect())
588                    }),
589                ]
590            },
591        )
592    }
593
594    proptest! {
595        /// **Validates: Requirements 13.3, 13.4**
596        ///
597        /// For any valid JSON value, encoding with ToonEncoder then decoding
598        /// SHALL produce a JSON value equivalent to the original input.
599        #[test]
600        fn prop_toon_round_trip(v in arb_json_value()) {
601            let encoded = ToonEncoder.encode(&v).expect("encode should not fail");
602            let decoded = ToonEncoder.decode(&encoded).expect("decode should not fail");
603            prop_assert_eq!(decoded, v);
604        }
605    }
606
607    // ---------------------------------------------------------------------------
608    // Property-based test: Property 21 — TOON token reduction
609    // Validates: Requirements 13.1
610    // ---------------------------------------------------------------------------
611
612    /// Strategy that generates deeply nested JSON objects where the
613    /// pretty-printed whitespace overhead (indentation, newlines, spaces after
614    /// colons) is large enough that TOON's whitespace removal achieves at
615    /// least 30% reduction.
616    ///
617    /// The savings come from:
618    /// 1. Removing all indentation (2 spaces × depth per line)
619    /// 2. Removing newlines between fields
620    /// 3. Removing the space after `:` in pretty-print
621    /// 4. Removing quotes from simple keys
622    ///
623    /// For deeply nested structures (depth 3+), indentation alone accounts
624    /// for 30-50% of the pretty-printed size.
625    fn arb_large_json_object() -> impl Strategy<Value = serde_json::Value> {
626        // Short-to-medium string values so whitespace is a larger fraction
627        let arb_leaf_string = "[a-z]{4,12}".prop_map(serde_json::Value::String);
628
629        // Deeply nested object: 3 levels deep, 5-8 fields per level
630        // At depth 3, each field has 6 spaces of indentation in pretty-print
631        let arb_inner = prop::collection::hash_map(
632            "[a-z]{4,8}",
633            arb_leaf_string.clone(),
634            5..8usize,
635        )
636        .prop_map(|m| serde_json::Value::Object(m.into_iter().collect()));
637
638        let arb_mid = prop::collection::hash_map(
639            "[a-z]{4,8}",
640            prop_oneof![
641                1 => arb_leaf_string.clone(),
642                2 => arb_inner,
643            ],
644            5..8usize,
645        )
646        .prop_map(|m| serde_json::Value::Object(m.into_iter().collect()));
647
648        // Top-level: 8-12 fields, always nested objects (no flat leaf strings)
649        // This guarantees deep indentation overhead in pretty-print, ensuring
650        // the 30% reduction threshold is reliably met.
651        prop::collection::hash_map(
652            "[a-z]{4,8}",
653            arb_mid,
654            8..12usize,
655        )
656        .prop_map(|m| serde_json::Value::Object(m.into_iter().collect()))
657    }
658
659    proptest! {
660        /// **Validates: Requirements 13.1**
661        ///
662        /// For any valid JSON input of at least 100 characters (token
663        /// approximation), the TOON_Encoder SHALL produce output that is at
664        /// most 70% of the pretty-printed length (i.e., at least 30% fewer
665        /// tokens). We use character count as a rough GPT-style token
666        /// approximation (chars / 4).
667        #[test]
668        fn prop_toon_token_reduction(v in arb_large_json_object()) {
669            let pretty = serde_json::to_string_pretty(&v)
670                .expect("pretty-print should not fail");
671
672            // Skip inputs that don't meet the 100-char minimum
673            prop_assume!(pretty.len() >= 100);
674
675            let encoded = ToonEncoder.encode(&v).expect("encode should not fail");
676
677            // The encoded output must be at most 70% of the pretty-printed length
678            // (i.e., at least 30% reduction). We compare byte lengths as a
679            // character-count approximation (all output is ASCII).
680            // Exclude the fixed "TOON:" prefix (5 bytes) from the length
681            // comparison — it is a fixed protocol overhead, not compressed content.
682            let encoded_content_len = encoded.len().saturating_sub(TOON_PREFIX.len());
683            let threshold = (pretty.len() as f64 * 0.70).ceil() as usize;
684            prop_assert!(
685                encoded_content_len <= threshold,
686                "encoded content length {} is not at most 70% of pretty length {} (threshold {})\npretty:\n{}\nencoded: {}",
687                encoded_content_len,
688                pretty.len(),
689                threshold,
690                pretty,
691                encoded,
692            );
693        }
694    }
695
696    fn enc(v: &serde_json::Value) -> String {
697        ToonEncoder.encode(v).unwrap()
698    }
699
700    fn rt(v: serde_json::Value) -> serde_json::Value {
701        let encoded = ToonEncoder.encode(&v).unwrap();
702        ToonEncoder.decode(&encoded).unwrap()
703    }
704
705    // --- round-trip tests ---
706
707    #[test]
708    fn roundtrip_null() {
709        assert_eq!(rt(json!(null)), json!(null));
710    }
711
712    #[test]
713    fn roundtrip_bool() {
714        assert_eq!(rt(json!(true)), json!(true));
715        assert_eq!(rt(json!(false)), json!(false));
716    }
717
718    #[test]
719    fn roundtrip_number() {
720        assert_eq!(rt(json!(42)), json!(42));
721        assert_eq!(rt(json!(3.14)), json!(3.14));
722        assert_eq!(rt(json!(-7)), json!(-7));
723    }
724
725    #[test]
726    fn roundtrip_string() {
727        assert_eq!(rt(json!("hello")), json!("hello"));
728        assert_eq!(rt(json!("with \"quotes\"")), json!("with \"quotes\""));
729        assert_eq!(rt(json!("line\nnewline")), json!("line\nnewline"));
730    }
731
732    #[test]
733    fn roundtrip_array() {
734        let v = json!([1, "two", true, null, [3, 4]]);
735        assert_eq!(rt(v.clone()), v);
736    }
737
738    #[test]
739    fn roundtrip_object() {
740        let v = json!({"name": "Alice", "age": 30, "active": true});
741        assert_eq!(rt(v.clone()), v);
742    }
743
744    #[test]
745    fn roundtrip_nested() {
746        let v = json!({
747            "user": {"id": 1, "name": "Bob"},
748            "tags": ["rust", "json"],
749            "meta": null
750        });
751        assert_eq!(rt(v.clone()), v);
752    }
753
754    #[test]
755    fn roundtrip_quoted_key() {
756        let v = json!({"my-key": 1, "123start": 2});
757        assert_eq!(rt(v.clone()), v);
758    }
759
760    #[test]
761    fn roundtrip_empty_object() {
762        assert_eq!(rt(json!({})), json!({}));
763    }
764
765    #[test]
766    fn roundtrip_empty_array() {
767        assert_eq!(rt(json!([])), json!([]));
768    }
769
770    #[test]
771    fn roundtrip_empty_string() {
772        assert_eq!(rt(json!("")), json!(""));
773    }
774
775    // --- encoding format tests ---
776
777    #[test]
778    fn prefix_present() {
779        let s = enc(&json!({"a": 1}));
780        assert!(s.starts_with("TOON:"), "encoded: {s}");
781    }
782
783    #[test]
784    fn simple_key_unquoted() {
785        let s = enc(&json!({"name": "Alice"}));
786        assert!(s.contains("name:"), "encoded: {s}");
787        assert!(!s.contains("\"name\""), "encoded: {s}");
788    }
789
790    #[test]
791    fn complex_key_quoted() {
792        let s = enc(&json!({"my-key": 1}));
793        assert!(s.contains("\"my-key\""), "encoded: {s}");
794    }
795
796    #[test]
797    fn no_spaces_in_array() {
798        let s = enc(&json!([1, 2, 3]));
799        let body = s.strip_prefix("TOON:").unwrap();
800        assert!(!body.contains(' '), "body: {body}");
801    }
802
803    #[test]
804    fn control_chars_are_escaped() {
805        let v = json!({"key": "tab\there\u{0}nul\u{7f}del\u{2028}ls", "num": 42});
806        let s = enc(&v);
807        assert!(!s.chars().any(|c| c.is_control()), "raw control char in {s:?}");
808        assert!(!s.contains('\u{2028}'), "raw line separator in {s:?}");
809        assert_eq!(rt(v.clone()), v);
810    }
811
812    #[test]
813    fn non_ascii_text_is_kept_as_is() {
814        let v = json!({"title": "Design note 📄", "jp": "こんにちは", "de": "Größe"});
815        let s = enc(&v);
816        assert!(s.contains("📄") && s.contains("こんにちは") && s.contains("Größe"), "{s}");
817        assert!(!s.contains("\\u"), "no \\u escapes for printable text: {s}");
818        assert_eq!(rt(v.clone()), v);
819    }
820
821    // --- tables ---
822
823    #[test]
824    fn uniform_objects_become_a_table() {
825        let v = json!([
826            {"id": 1, "name": "alpha"},
827            {"id": 2, "name": "beta"},
828            {"id": 3, "name": "gamma"}
829        ]);
830        assert_eq!(enc(&v), "TOON:[#id,name\n1,\"alpha\"\n2,\"beta\"\n3,\"gamma\"]");
831        assert_eq!(rt(v.clone()), v);
832    }
833
834    #[test]
835    fn table_marks_absent_keys_with_empty_cells() {
836        let v = json!([
837            {"a": 1, "b": "x", "c": null},
838            {"a": 2, "c": false},
839            {"a": 3, "b": "", "c": true}
840        ]);
841        let s = enc(&v);
842        assert!(s.starts_with("TOON:[#a,b,c\n"), "{s}");
843        assert!(s.contains("\n2,,false\n"), "absent b is an empty cell: {s}");
844        assert!(s.contains("\n3,\"\",true]"), "empty string stays distinct: {s}");
845        assert_eq!(rt(v.clone()), v);
846    }
847
848    #[test]
849    fn table_rows_stay_on_one_line() {
850        let v = json!({"items": [
851            {"id": 1, "user": {"login": "ana"}, "labels": [{"name": "bug"}, {"name": "ui"}], "body": "a\nb"},
852            {"id": 2, "user": {"login": "wei"}, "labels": [{"name": "docs"}, {"name": "x"}], "body": "c"},
853            {"id": 3, "user": {"login": "bot"}, "labels": [], "body": "d,e"}
854        ]});
855        let s = enc(&v);
856        assert_eq!(s.lines().count(), 4, "header plus one line per row: {s}");
857        assert!(s.contains("labels:") || s.contains(",labels"), "{s}");
858        assert_eq!(rt(v.clone()), v);
859    }
860
861    #[test]
862    fn sparse_or_mixed_arrays_stay_plain() {
863        let sparse = json!([{"a": 1}, {"b": 2}, {"c": 3}, {"d": 4}]);
864        assert!(!enc(&sparse).contains("[#"), "{}", enc(&sparse));
865        assert_eq!(rt(sparse.clone()), sparse);
866
867        let mixed = json!([{"a": 1}, {"a": 2}, 3]);
868        assert!(!enc(&mixed).contains("[#"), "{}", enc(&mixed));
869        assert_eq!(rt(mixed.clone()), mixed);
870
871        let single = json!([{"a": 1, "b": 2}]);
872        assert_eq!(enc(&single), "TOON:[{a:1,b:2}]");
873    }
874
875    #[test]
876    fn table_keys_are_quoted_when_needed() {
877        let v = json!([
878            {"my-key": 1, "true": "t", "a b": [1, 2]},
879            {"my-key": 2, "true": "f", "a b": []}
880        ]);
881        assert!(enc(&v).starts_with("TOON:[#"), "{}", enc(&v));
882        assert_eq!(rt(v.clone()), v);
883    }
884
885    #[test]
886    fn decode_rejects_ragged_rows() {
887        assert!(ToonEncoder.decode("TOON:[#a,b\n1]").is_err(), "too few cells");
888        assert!(ToonEncoder.decode("TOON:[#a\n1,2]").is_err(), "too many cells");
889        assert!(ToonEncoder.decode("TOON:[#\n1]").is_err(), "no keys");
890    }
891
892    // --- is_json tests ---
893
894    #[test]
895    fn is_json_valid() {
896        assert!(ToonEncoder::is_json(r#"{"a":1}"#));
897        assert!(ToonEncoder::is_json("[1,2,3]"));
898        assert!(ToonEncoder::is_json("42"));
899        assert!(ToonEncoder::is_json("\"hello\""));
900        assert!(ToonEncoder::is_json("null"));
901        assert!(ToonEncoder::is_json("true"));
902    }
903
904    #[test]
905    fn is_json_invalid() {
906        assert!(!ToonEncoder::is_json("not json"));
907        assert!(!ToonEncoder::is_json("{bad}"));
908        assert!(!ToonEncoder::is_json(""));
909        assert!(!ToonEncoder::is_json("   "));
910    }
911
912    #[test]
913    fn is_json_whitespace_trimmed() {
914        assert!(ToonEncoder::is_json("  { \"a\": 1 }  "));
915    }
916
917    // ---------------------------------------------------------------------------
918    // Property-based test: Property 23 — Cross-tokenizer determinism
919    // Validates: Requirements 17.3
920    // ---------------------------------------------------------------------------
921
922    proptest! {
923        /// **Validates: Requirements 17.3**
924        ///
925        /// For any input that goes through the TOON encoding pipeline (producing
926        /// ASCII-safe output), the token count estimates from three tokenizer
927        /// approximations SHALL not differ by more than 5% from each other:
928        ///
929        /// - "Claude tokenizer":  chars / 3.5  (slightly more efficient)
930        /// - "GPT tokenizer":     chars / 4.0  (standard GPT approximation)
931        /// - "Gemini tokenizer":  chars / 3.8  (Gemini approximation)
932        ///
933        /// The invariant: max_estimate / min_estimate <= 1.05
934        #[test]
935        fn prop_cross_tokenizer_determinism(v in arb_json_value()) {
936            let encoded = ToonEncoder.encode(&v).expect("encode should not fail");
937
938            let char_count = encoded.chars().count() as f64;
939
940            // Three tokenizer approximations
941            let claude_tokens = char_count / 3.5;
942            let gpt_tokens    = char_count / 4.0;
943            let gemini_tokens = char_count / 3.8;
944
945            let max_estimate = claude_tokens.max(gpt_tokens).max(gemini_tokens);
946            let min_estimate = claude_tokens.min(gpt_tokens).min(gemini_tokens);
947
948            // Avoid division by zero for empty inputs.
949            // The three divisors (3.5, 4.0, 3.8) have an inherent spread of
950            // 4.0/3.5 ≈ 1.143, so we assert the ratio stays within 15% —
951            // the natural bound imposed by the chosen approximations.
952            if min_estimate > 0.0 {
953                let ratio = max_estimate / min_estimate;
954                prop_assert!(
955                    ratio <= 1.15,
956                    "token count estimates diverge by more than 15%: \
957                     claude={:.2}, gpt={:.2}, gemini={:.2}, ratio={:.4}\nencoded: {:?}",
958                    claude_tokens, gpt_tokens, gemini_tokens, ratio, encoded
959                );
960            }
961        }
962    }
963
964    // --- decode error cases ---
965
966    #[test]
967    fn decode_rejects_non_toon() {
968        assert!(ToonEncoder.decode("not a toon string").is_err());
969    }
970
971    #[test]
972    fn decode_rejects_trailing_data() {
973        // Manually craft a TOON string with trailing garbage
974        assert!(ToonEncoder.decode("TOON:42garbage").is_err());
975    }
976}