1use crate::error::{Result, SqzError};
17
18const TOON_PREFIX: &str = "TOON:";
19
20pub struct ToonEncoder;
21
22impl ToonEncoder {
23 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 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 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
57fn encode_value(v: &serde_json::Value, buf: &mut String) {
62 encode_value_in(v, buf, true);
63}
64
65fn 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 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
113fn 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
175fn 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
196fn 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
216struct 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 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 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 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 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 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 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 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 if self.peek() == Some(b'-') {
519 self.advance();
520 }
521 while matches!(self.peek(), Some(b'0'..=b'9')) {
523 self.advance();
524 }
525 if self.peek() == Some(b'.') {
527 self.advance();
528 while matches!(self.peek(), Some(b'0'..=b'9')) {
529 self.advance();
530 }
531 }
532 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 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, 64, 8, |inner| {
581 prop_oneof![
582 prop::collection::vec(inner.clone(), 0..8)
584 .prop_map(serde_json::Value::Array),
585 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 #[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 fn arb_large_json_object() -> impl Strategy<Value = serde_json::Value> {
626 let arb_leaf_string = "[a-z]{4,12}".prop_map(serde_json::Value::String);
628
629 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 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 #[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 prop_assume!(pretty.len() >= 100);
674
675 let encoded = ToonEncoder.encode(&v).expect("encode should not fail");
676
677 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 #[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 #[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 #[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 #[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 proptest! {
923 #[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 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 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 #[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 assert!(ToonEncoder.decode("TOON:42garbage").is_err());
975 }
976}