1use crate::WriteError;
52use crate::document::{Doc, Value};
53use crate::error::{OmnistError, ParseError};
54use crate::formats::float_fmt;
55use crate::formats::string_escape::{JSON_ESCAPES, write_quoted};
56use crate::formats::textpos::line_col_bytes;
57use crate::limits::{IntGuard, Limits, Resolved};
58use crate::report::{Severity, WriteReport};
59use indexmap::IndexMap;
60
61pub fn read_json(text: &str) -> Result<Doc, OmnistError> {
74 read_json_resolved(text, Resolved::DEFAULT)
75}
76
77pub fn read_json_with(text: &str, limits: &Limits) -> Result<Doc, OmnistError> {
85 read_json_resolved(text, limits.resolve()?)
86}
87
88fn read_json_resolved(text: &str, limits: Resolved) -> Result<Doc, OmnistError> {
89 limits.check_input_size(text.len())?;
91 let text = crate::bom::strip_leading_bom(text)
94 .map_err(|_| ParseError::codec_syntax(1, 1, crate::bom::DOUBLED_BOM_MESSAGE))?;
95 let mut p = Parser::new(text, &limits);
96 p.skip_ws();
97 let value = p.parse_value().map_err(|e| limits.lift_parse_error(e))?;
98 p.skip_ws();
99 if p.pos < p.n {
100 return Err(p
101 .error_at(
102 p.pos,
103 "unexpected trailing data after JSON value".to_string(),
104 )
105 .into());
106 }
107 let doc = Doc::of_resolved(&value, limits).map_err(|e| limits.whole_document(e))?;
108 p.guard.finish(&limits)?;
111 Ok(doc)
112}
113
114pub fn write_json(
127 doc: &Doc,
128 indent: Option<usize>,
129 strict: bool,
130 report: Option<&mut WriteReport>,
131) -> Result<String, WriteError> {
132 let grouped = doc.to_grouped();
133 if let Some((path, x)) = find_special_float(&grouped) {
134 return Err(crate::report::unsupported_value_error(
135 &path,
136 format!("{x} has no JSON token (JSON's grammar has no NaN/Infinity literal)"),
137 ));
138 }
139 let mut rep = check_json_grouped(&grouped);
140 add_interleaving_diagnostic(doc, &mut rep);
141 let mut out = String::new();
142 write_value(&grouped, indent, 0, &mut out);
143 crate::report::finish_write(out, rep, strict, report)
144}
145
146fn find_special_float(grouped: &Value) -> Option<(String, f64)> {
150 let mut path = String::from("$");
151 let mut found: Option<(String, f64)> = None;
152 crate::formats::visit_grouped(grouped, &mut path, &mut |visited, path| {
153 if found.is_some() {
154 return;
155 }
156 let crate::formats::Visited::Node { value } = visited else {
157 return;
158 };
159 if let Value::Float(x) = value
160 && (x.is_nan() || x.is_infinite())
161 {
162 found = Some((path.to_string(), *x));
163 }
164 });
165 found
166}
167
168fn add_interleaving_diagnostic(doc: &Doc, rep: &mut WriteReport) {
174 if doc.has_interleaving_loss() {
175 rep.add(
176 "$",
177 "format.interleaving-lost",
178 "cross-label interleaving could not be written; same-label edges were grouped",
179 Severity::Warning,
180 );
181 }
182}
183
184pub fn check_json(doc: &Doc) -> WriteReport {
192 let grouped = doc.to_grouped();
193 let mut rep = check_json_grouped(&grouped);
194 add_interleaving_diagnostic(doc, &mut rep);
195 rep
196}
197
198fn check_json_grouped(grouped: &Value) -> WriteReport {
199 let mut rep = WriteReport::new();
200 let mut path = String::from("$");
201 crate::formats::visit_grouped(grouped, &mut path, &mut |visited, path| {
202 let crate::formats::Visited::Node { value } = visited else {
203 return;
204 };
205 match value {
206 Value::Float(x) if x.is_nan() || x.is_infinite() => {
212 rep.add(
213 path,
214 "write.unsupported-value",
215 format!("{x} has no JSON token (JSON's grammar has no NaN/Infinity literal)"),
216 Severity::Error,
217 );
218 }
219 Value::Date(_) | Value::Time(_) | Value::Datetime(_) => {
225 rep.add(
226 path,
227 "format.temporal-stringified",
228 "date/time/datetime has no native JSON literal; wrote as an ISO-8601 string"
229 .to_string(),
230 Severity::Warning,
231 );
232 }
233 _ => {}
234 }
235 });
236 rep
237}
238
239pub(crate) struct Json;
246
247impl crate::formats::Codec for Json {
248 const NAME: &'static str = "json";
249
250 fn read(text: &str) -> Result<Doc, OmnistError> {
251 read_json(text)
252 }
253
254 fn write(doc: &Doc) -> Result<String, OmnistError> {
255 write_json(doc, None, false, None).map_err(Into::into)
256 }
257
258 fn check(doc: &Doc) -> WriteReport {
259 check_json(doc)
260 }
261}
262
263fn write_value(v: &Value, indent: Option<usize>, level: usize, out: &mut String) {
264 match v {
265 Value::Null => out.push_str("null"),
266 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
267 Value::Int(i) => out.push_str(&i.to_string()),
268 Value::Float(x) => write_float(*x, out),
269 Value::Str(s) | Value::Date(s) => write_json_string(s, out),
274 Value::Time(s) => write_json_string(&super::temporal_text(s, false), out),
275 Value::Datetime(s) => write_json_string(&super::temporal_text(s, true), out),
276 Value::Array(items) => write_seq(items.iter(), '[', ']', indent, level, out, write_value),
277 Value::Object(map) => write_seq(
278 map.iter(),
279 '{',
280 '}',
281 indent,
282 level,
283 out,
284 |(k, val), indent, level, out| {
285 write_json_string(k, out);
286 out.push_str(": ");
287 write_value(val, indent, level, out);
288 },
289 ),
290 }
291}
292
293fn write_seq<I, T>(
294 items: I,
295 open: char,
296 close: char,
297 indent: Option<usize>,
298 level: usize,
299 out: &mut String,
300 mut write_item: impl FnMut(T, Option<usize>, usize, &mut String),
301) where
302 I: ExactSizeIterator<Item = T>,
303{
304 if items.len() == 0 {
305 out.push(open);
306 out.push(close);
307 return;
308 }
309 out.push(open);
310 let child_level = level + 1;
311 let mut first = true;
312 for item in items {
313 if !first {
314 out.push(',');
315 if indent.is_none() {
316 out.push(' ');
317 }
318 }
319 first = false;
320 if let Some(n) = indent {
321 out.push('\n');
322 out.push_str(&" ".repeat(n * child_level));
323 }
324 write_item(item, indent, child_level, out);
325 }
326 if let Some(n) = indent {
327 out.push('\n');
328 out.push_str(&" ".repeat(n * level));
329 }
330 out.push(close);
331}
332
333fn write_float(x: f64, out: &mut String) {
343 float_fmt::write_float(x, "NaN", "Infinity", "-Infinity", out);
344}
345
346fn write_json_string(s: &str, out: &mut String) {
347 write_quoted(s, &JSON_ESCAPES, out);
348}
349
350struct Parser<'a> {
353 text: &'a str,
354 n: usize,
355 pos: usize,
356 depth: usize,
357 max_depth: usize,
358 guard: IntGuard,
359}
360
361impl<'a> Parser<'a> {
362 fn new(text: &'a str, limits: &Resolved) -> Self {
363 let n = text.len();
371 Parser {
372 text,
373 n,
374 pos: 0,
375 depth: 0,
376 max_depth: limits.max_depth,
377 guard: IntGuard::new(limits.max_int_digits),
378 }
379 }
380
381 fn enter(&mut self) -> Result<(), ParseError> {
386 self.depth += 1;
387 if self.depth > self.max_depth {
388 return Err(self.limit_error_at(
389 self.pos,
390 "document.limit.depth",
391 format!("nesting exceeds the maximum depth ({})", self.max_depth),
392 ));
393 }
394 Ok(())
395 }
396
397 fn error_at(&self, pos: usize, msg: String) -> ParseError {
398 let (line, col) = line_col_bytes(self.text, pos);
399 ParseError::codec_syntax(line, col, format!("invalid JSON: {msg}"))
400 }
401
402 fn limit_error_at(&self, pos: usize, code: &str, msg: String) -> ParseError {
406 let (line, col) = line_col_bytes(self.text, pos);
407 ParseError::new(line, col, code, format!("invalid JSON: {msg}"))
408 }
409
410 fn char_at(&self, at: usize) -> Option<char> {
415 self.text.get(at..)?.chars().next()
416 }
417
418 fn peek(&self) -> Option<char> {
419 self.char_at(self.pos)
420 }
421
422 fn skip_ws(&mut self) {
423 while matches!(
424 self.peek(),
425 Some(' ') | Some('\t') | Some('\n') | Some('\r')
426 ) {
427 self.pos += 1;
428 }
429 }
430
431 fn expect(&mut self, c: char) -> Result<(), ParseError> {
432 if self.peek() == Some(c) {
433 self.pos += c.len_utf8();
434 Ok(())
435 } else {
436 Err(self.error_at(self.pos, format!("expected {c:?}")))
437 }
438 }
439
440 fn matches_word(&self, word: &str) -> bool {
445 debug_assert!(
446 word.is_ascii(),
447 "matches_word is only used with ASCII keywords"
448 );
449 let bytes = self.text.as_bytes();
450 word.bytes()
451 .enumerate()
452 .all(|(i, b)| bytes.get(self.pos + i) == Some(&b))
453 }
454
455 fn parse_value(&mut self) -> Result<Value, ParseError> {
456 self.skip_ws();
457 match self.peek() {
458 None => Err(self.error_at(self.pos, "unexpected end of input".to_string())),
459 Some('{') => self.parse_object(),
460 Some('[') => self.parse_array(),
461 Some('"') => Ok(Value::Str(self.parse_string()?)),
462 Some('t') if self.matches_word("true") => {
463 self.pos += 4;
464 Ok(Value::Bool(true))
465 }
466 Some('f') if self.matches_word("false") => {
467 self.pos += 5;
468 Ok(Value::Bool(false))
469 }
470 Some('n') if self.matches_word("null") => {
471 self.pos += 4;
472 Ok(Value::Null)
473 }
474 Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
475 Some(c) => Err(self.error_at(self.pos, format!("unexpected character {c:?}"))),
476 }
477 }
478
479 fn parse_object(&mut self) -> Result<Value, ParseError> {
480 self.enter()?;
481 self.expect('{')?;
482 let mut map: IndexMap<String, Value> = IndexMap::new();
483 self.skip_ws();
484 if self.peek() == Some('}') {
485 self.pos += 1;
486 self.depth -= 1;
487 return Ok(Value::Object(map));
488 }
489 loop {
490 self.skip_ws();
491 if self.peek() != Some('"') {
492 return Err(self.error_at(self.pos, "expected string key".to_string()));
493 }
494 let key = self.parse_string()?;
495 self.skip_ws();
496 self.expect(':')?;
497 let value = self.parse_value()?;
498 map.insert(key, value);
503 self.skip_ws();
504 match self.peek() {
505 Some(',') => {
506 self.pos += 1;
507 }
508 Some('}') => {
509 self.pos += 1;
510 break;
511 }
512 _ => return Err(self.error_at(self.pos, "expected ',' or '}'".to_string())),
513 }
514 }
515 self.depth -= 1;
516 Ok(Value::Object(map))
517 }
518
519 fn parse_array(&mut self) -> Result<Value, ParseError> {
520 self.enter()?;
521 self.expect('[')?;
522 let mut items = Vec::new();
523 self.skip_ws();
524 if self.peek() == Some(']') {
525 self.pos += 1;
526 self.depth -= 1;
527 return Ok(Value::Array(items));
528 }
529 loop {
530 let v = self.parse_value()?;
531 items.push(v);
532 self.skip_ws();
533 match self.peek() {
534 Some(',') => {
535 self.pos += 1;
536 }
537 Some(']') => {
538 self.pos += 1;
539 break;
540 }
541 _ => return Err(self.error_at(self.pos, "expected ',' or ']'".to_string())),
542 }
543 }
544 self.depth -= 1;
545 Ok(Value::Array(items))
546 }
547
548 fn parse_string(&mut self) -> Result<String, ParseError> {
549 self.expect('"')?;
550 let mut s = String::new();
551 loop {
552 match self.peek() {
553 None => return Err(self.error_at(self.pos, "unterminated string".to_string())),
554 Some('"') => {
555 self.pos += 1;
556 break;
557 }
558 Some('\\') => {
559 self.pos += 1;
560 match self.peek() {
561 Some('"') => {
562 s.push('"');
563 self.pos += 1;
564 }
565 Some('\\') => {
566 s.push('\\');
567 self.pos += 1;
568 }
569 Some('/') => {
570 s.push('/');
571 self.pos += 1;
572 }
573 Some('b') => {
574 s.push('\u{08}');
575 self.pos += 1;
576 }
577 Some('f') => {
578 s.push('\u{0c}');
579 self.pos += 1;
580 }
581 Some('n') => {
582 s.push('\n');
583 self.pos += 1;
584 }
585 Some('r') => {
586 s.push('\r');
587 self.pos += 1;
588 }
589 Some('t') => {
590 s.push('\t');
591 self.pos += 1;
592 }
593 Some('u') => {
594 self.pos += 1;
595 let hi = self.parse_hex4()?;
596 if (0xD800..=0xDBFF).contains(&hi) {
597 if self.peek() == Some('\\')
598 && self.char_at(self.pos + 1) == Some('u')
599 {
600 self.pos += 2;
601 let lo = self.parse_hex4()?;
602 if (0xDC00..=0xDFFF).contains(&lo) {
603 let c = 0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00);
604 s.push(char::from_u32(c).expect(
617 "a well-formed UTF-16 surrogate pair always \
618 combines to a valid supplementary-plane char",
619 ));
620 } else {
621 return Err(self.error_at(
622 self.pos,
623 "invalid low surrogate".to_string(),
624 ));
625 }
626 } else {
627 return Err(self.error_at(
628 self.pos,
629 "unpaired high surrogate".to_string(),
630 ));
631 }
632 } else if (0xDC00..=0xDFFF).contains(&hi) {
633 return Err(
634 self.error_at(self.pos, "unpaired low surrogate".to_string())
635 );
636 } else {
637 s.push(char::from_u32(hi).expect(
648 "a 4-hex-digit \\u escape outside the surrogate range is \
649 always a valid BMP char",
650 ));
651 }
652 }
653 _ => return Err(self.error_at(self.pos, "invalid escape".to_string())),
654 }
655 }
656 Some(c) if (c as u32) < 0x20 => {
657 return Err(self.error_at(self.pos, "control character in string".to_string()));
658 }
659 Some(c) => {
660 s.push(c);
661 self.pos += c.len_utf8();
662 }
663 }
664 }
665 Ok(s)
666 }
667
668 fn parse_hex4(&mut self) -> Result<u32, ParseError> {
669 let mut v: u32 = 0;
670 for _ in 0..4 {
671 let c = self.peek().ok_or_else(|| {
672 self.error_at(self.pos, "unterminated unicode escape".to_string())
673 })?;
674 let d = c.to_digit(16).ok_or_else(|| {
675 self.error_at(self.pos, "invalid hex digit in unicode escape".to_string())
676 })?;
677 v = v * 16 + d;
678 self.pos += 1;
682 }
683 Ok(v)
684 }
685
686 fn parse_number(&mut self) -> Result<Value, ParseError> {
691 let start = self.pos;
692 if self.peek() == Some('-') {
693 self.pos += 1;
694 }
695 let int_start = self.pos;
696 if self.peek() == Some('0') {
697 self.pos += 1;
698 } else if self.peek().is_some_and(|c| c.is_ascii_digit()) {
699 while self.peek().is_some_and(|c| c.is_ascii_digit()) {
700 self.pos += 1;
701 }
702 } else {
703 return Err(self.error_at(self.pos, "invalid number literal".to_string()));
704 }
705 debug_assert!(
706 self.pos > int_start,
707 "the '0' and digit-run branches above both advance pos by at least 1"
708 );
709 let bytes = self.text.as_bytes();
713 let byte_at = |p: usize| bytes.get(p).copied();
714 let mut is_float = false;
715 if self.peek() == Some('.') {
716 let frac_start = self.pos + 1;
717 let mut p = frac_start;
718 while byte_at(p).is_some_and(|b| b.is_ascii_digit()) {
719 p += 1;
720 }
721 if p > frac_start {
722 is_float = true;
723 self.pos = p;
724 }
725 }
726 if matches!(self.peek(), Some('e') | Some('E')) {
727 let mut p = self.pos + 1;
728 if matches!(byte_at(p), Some(b'+') | Some(b'-')) {
729 p += 1;
730 }
731 let exp_start = p;
732 while byte_at(p).is_some_and(|b| b.is_ascii_digit()) {
733 p += 1;
734 }
735 if p > exp_start {
736 is_float = true;
737 self.pos = p;
738 }
739 }
740 let text: &str = &self.text[start..self.pos];
741 if is_float {
742 let v: f64 = text
743 .parse()
744 .expect("scanner only emits number-shaped text, which f64::from_str always parses");
745 Ok(Value::Float(v))
746 } else {
747 let digits = &text[if text.starts_with('-') { 1 } else { 0 }..];
748 if digits.len() > self.guard.max() {
749 return Ok(Value::Int(self.guard.placeholder()));
750 }
751 let v = num_bigint::BigInt::parse_bytes(text.as_bytes(), 10).expect(
755 "scanner only emits digit-shaped text, which BigInt::parse_bytes always parses",
756 );
757 Ok(Value::Int(v))
758 }
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use crate::document::{Doc, Scalar, Value};
766 use crate::formats::int_cap::MAX_INT_DIGITS;
767 use crate::report::Severity;
768
769 fn obj(pairs: Vec<(&str, Value)>) -> Value {
770 Value::Object(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
771 }
772
773 #[test]
776 fn reads_object_with_scalars() {
777 let doc = read_json(r#"{"a": 1, "b": "s", "c": true, "d": null, "e": 1.5}"#).unwrap();
778 let root = doc.root();
779 assert_eq!(
780 *root.get_one("a").unwrap().value().unwrap(),
781 Scalar::Int((1).into())
782 );
783 assert_eq!(
784 *root.get_one("b").unwrap().value().unwrap(),
785 Scalar::Str("s".to_string())
786 );
787 assert_eq!(
788 *root.get_one("c").unwrap().value().unwrap(),
789 Scalar::Bool(true)
790 );
791 assert_eq!(*root.get_one("d").unwrap().value().unwrap(), Scalar::Null);
792 assert_eq!(
793 *root.get_one("e").unwrap().value().unwrap(),
794 Scalar::Float(1.5)
795 );
796 }
797
798 #[test]
799 fn reads_false_literal() {
800 let doc = read_json(r#"{"c": false}"#).unwrap();
801 assert_eq!(
802 *doc.root().get_one("c").unwrap().value().unwrap(),
803 Scalar::Bool(false)
804 );
805 }
806
807 #[test]
808 fn reads_array_as_repeated_edges() {
809 let doc = read_json(r#"{"m": [1, 2, 3]}"#).unwrap();
810 let root = doc.root();
811 let ms = root.get("m");
812 assert_eq!(ms.len(), 3);
813 assert_eq!(*ms[0].value().unwrap(), Scalar::Int((1).into()));
814 assert_eq!(*ms[2].value().unwrap(), Scalar::Int((3).into()));
815 }
816
817 #[test]
818 fn reads_empty_array_literal_as_no_edges() {
819 let doc = read_json(r#"{"m": [], "n": 1}"#).unwrap();
820 assert!(doc.root().get("m").is_empty());
821 }
822
823 #[test]
824 fn reads_nested_object() {
825 let doc = read_json(r#"{"a": {"b": {"c": 1}}}"#).unwrap();
826 let root = doc.root();
827 let a = root.get_one("a").unwrap();
828 let b = a.get_one("b").unwrap();
829 assert_eq!(
830 *b.get_one("c").unwrap().value().unwrap(),
831 Scalar::Int((1).into())
832 );
833 }
834
835 #[test]
836 fn bare_top_level_array_is_a_document_error_not_a_parse_error() {
837 let err = read_json("[1, 2, 3]").unwrap_err();
838 assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
839 }
840
841 #[test]
842 fn array_of_arrays_is_a_document_error() {
843 let err = read_json(r#"{"m": [[1, 2]]}"#).unwrap_err();
844 assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
845 }
846
847 #[test]
848 fn invalid_json_syntax_is_a_parse_error() {
849 let err = read_json("{not json}").unwrap_err();
850 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
851 }
852
853 #[test]
854 fn trailing_data_after_a_value_is_a_parse_error() {
855 let err = read_json("1 2").unwrap_err();
856 assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
857 }
858
859 #[test]
860 fn nesting_past_max_depth_is_a_document_limit_error_at_the_root() {
861 let mut text = String::new();
862 for _ in 0..=crate::document::MAX_DEPTH {
863 text.push_str(r#"{"a":"#);
864 }
865 text.push('1');
866 for _ in 0..=crate::document::MAX_DEPTH {
867 text.push('}');
868 }
869 let err = read_json(&text).unwrap_err();
870 assert!(
871 matches!(&err, OmnistError::Document(e)
872 if e.code.as_deref() == Some("document.limit.depth") && e.path == "$"),
873 "got {err:?}"
874 );
875 assert!(err.to_string().contains("maximum depth"));
876 }
877
878 #[test]
879 fn duplicate_object_keys_last_value_wins_first_position_kept() {
880 let doc = read_json(r#"{"a": 1, "b": 2, "a": 3}"#).unwrap();
881 let root = doc.root();
882 assert_eq!(root.labels(), vec!["a".to_string(), "b".to_string()]);
883 assert_eq!(
884 *root.get_one("a").unwrap().value().unwrap(),
885 Scalar::Int((3).into())
886 );
887 }
888
889 #[test]
890 fn reads_string_escapes_and_unicode_escape() {
891 let doc = read_json(r#"{"s": "a\n\r\t\"\\é"}"#).unwrap();
892 let v = doc.root().get_one("s").unwrap();
893 assert_eq!(
894 *v.value().unwrap(),
895 Scalar::Str("a\n\r\t\"\\\u{e9}".to_string())
896 );
897 }
898
899 #[test]
900 fn reads_plain_unicode_escape() {
901 const BSL: char = '\u{5c}';
902 let input = format!("{{\"s\": \"{BSL}u0041\"}}");
903 let doc = read_json(&input).unwrap();
904 let v = doc.root().get_one("s").unwrap();
905 assert_eq!(*v.value().unwrap(), Scalar::Str("A".to_string()));
906 }
907
908 #[test]
909 fn reads_surrogate_pair_escape() {
910 let doc = read_json(r#"{"s": "😀"}"#).unwrap();
914 let v = doc.root().get_one("s").unwrap();
915 assert_eq!(*v.value().unwrap(), Scalar::Str("\u{1F600}".to_string()));
916 }
917
918 #[test]
919 fn reads_surrogate_pair_written_as_two_u_escapes() {
920 const BSL: char = '\u{5c}';
924 let input = format!("{{\"s\": \"{BSL}ud83d{BSL}ude00\"}}");
925 let doc = read_json(&input).unwrap();
926 let v = doc.root().get_one("s").unwrap();
927 assert_eq!(*v.value().unwrap(), Scalar::Str("\u{1F600}".to_string()));
928 }
929
930 #[test]
931 fn rejects_bare_nan_and_infinity_tokens() {
932 for src in [r#"{"a": NaN}"#, r#"{"a": Infinity}"#, r#"{"a": -Infinity}"#] {
935 assert!(read_json(src).is_err(), "{src} must be refused");
936 }
937 }
938
939 #[test]
940 fn integer_literal_under_digit_cap_but_over_i64_range_parses() {
941 let text = format!(r#"{{"a": {}}}"#, "9".repeat(20));
946 let doc = read_json(&text).unwrap();
947 let value = doc.root().child("a").unwrap().value().unwrap();
948 assert_eq!(
949 value,
950 &Scalar::Int(num_bigint::BigInt::parse_bytes(b"99999999999999999999", 10).unwrap())
951 );
952 }
953
954 #[test]
955 fn integer_literal_over_digit_cap_is_rejected_before_range_check() {
956 let text = format!(r#"{{"a": {}}}"#, "9".repeat(MAX_INT_DIGITS + 1));
957 let err = read_json(&text).unwrap_err();
958 assert!(
959 matches!(&err, OmnistError::Document(e)
960 if e.code.as_deref() == Some("document.limit.int-digits")
961 && e.path == "$.a"
962 && e.message.contains("4300-digit")),
963 "got {err:?}"
964 )
965 }
966
967 #[test]
968 fn integer_literal_exactly_at_digit_cap_parses_not_digit_cap_error() {
969 let text = format!(r#"{{"a": {}}}"#, "9".repeat(MAX_INT_DIGITS));
973 let doc = read_json(&text).unwrap();
974 let value = doc.root().child("a").unwrap().value().unwrap();
975 assert!(
976 matches!(value, Scalar::Int(i) if i.to_string().len() == MAX_INT_DIGITS),
977 "got {value:?}"
978 );
979 }
980
981 #[test]
982 fn whitespace_and_negative_zero_and_exponent_numbers_read() {
983 let doc = read_json(" {\n\"a\" : 1e3,\n\"b\": -0.5, \"c\": 2E-2\t} ").unwrap();
984 let root = doc.root();
985 assert_eq!(
986 *root.get_one("a").unwrap().value().unwrap(),
987 Scalar::Float(1000.0)
988 );
989 assert_eq!(
990 *root.get_one("b").unwrap().value().unwrap(),
991 Scalar::Float(-0.5)
992 );
993 assert_eq!(
994 *root.get_one("c").unwrap().value().unwrap(),
995 Scalar::Float(0.02)
996 );
997 }
998
999 #[test]
1000 fn error_position_after_multibyte_content_reports_correct_line() {
1001 let err = read_json("{\"s\": \"café \u{1F600}\"}\n@").unwrap_err();
1008 assert!(
1009 matches!(&err, OmnistError::Parse(e) if e.line == 2),
1010 "got {err:?}"
1011 );
1012 }
1013
1014 #[test]
1015 fn empty_object_and_array_read() {
1016 let doc = read_json(r#"{"a": {}}"#).unwrap();
1017 assert!(doc.root().get_one("a").unwrap().edges().unwrap().is_empty());
1018 }
1019
1020 #[test]
1021 fn empty_input_is_unexpected_end_of_input_error() {
1022 let err = read_json("").unwrap_err();
1023 assert!(
1024 matches!(&err, OmnistError::Parse(e) if e.message.contains("unexpected end of input")),
1025 "got {err:?}"
1026 )
1027 }
1028
1029 #[test]
1030 fn unrecognized_character_is_a_parse_error() {
1031 let err = read_json("@").unwrap_err();
1032 assert!(
1033 matches!(&err, OmnistError::Parse(e) if e.message.contains("unexpected character")),
1034 "got {err:?}"
1035 )
1036 }
1037
1038 #[test]
1039 fn a_bareword_that_only_partially_matches_a_keyword_is_unexpected_character() {
1040 let err = read_json("tx").unwrap_err();
1043 assert!(
1044 matches!(&err, OmnistError::Parse(e) if e.message.contains("unexpected character")),
1045 "got {err:?}"
1046 )
1047 }
1048
1049 #[test]
1050 fn error_position_reports_the_line_after_a_newline() {
1051 let err = read_json("{\n \"a\": @\n}").unwrap_err();
1052 assert!(
1053 matches!(&err, OmnistError::Parse(e) if e.line == 2),
1054 "got {err:?}"
1055 );
1056 }
1057
1058 #[test]
1059 fn object_missing_colon_is_a_parse_error() {
1060 let err = read_json(r#"{"a" 1}"#).unwrap_err();
1061 assert!(
1062 matches!(&err, OmnistError::Parse(e) if e.message.contains("expected ':'")),
1063 "got {err:?}"
1064 )
1065 }
1066
1067 #[test]
1068 fn object_missing_key_is_a_parse_error() {
1069 let err = read_json(r#"{1: 2}"#).unwrap_err();
1070 assert!(
1071 matches!(&err, OmnistError::Parse(e) if e.message.contains("expected string key")),
1072 "got {err:?}"
1073 )
1074 }
1075
1076 #[test]
1077 fn object_missing_comma_or_brace_is_a_parse_error() {
1078 let err = read_json(r#"{"a": 1 "b": 2}"#).unwrap_err();
1079 assert!(
1080 matches!(&err, OmnistError::Parse(e) if e.message.contains("expected ',' or '}'")),
1081 "got {err:?}"
1082 )
1083 }
1084
1085 #[test]
1086 fn array_missing_comma_or_bracket_is_a_parse_error() {
1087 let err = read_json(r#"{"a": [1 2]}"#).unwrap_err();
1088 assert!(
1089 matches!(&err, OmnistError::Parse(e) if e.message.contains("expected ',' or ']'")),
1090 "got {err:?}"
1091 )
1092 }
1093
1094 #[test]
1095 fn unterminated_string_is_a_parse_error() {
1096 let err = read_json(r#"{"a": "hi}"#).unwrap_err();
1097 assert!(
1098 matches!(&err, OmnistError::Parse(e) if e.message.contains("unterminated string")),
1099 "got {err:?}"
1100 )
1101 }
1102
1103 #[test]
1104 fn control_character_in_string_is_a_parse_error() {
1105 let err = read_json("{\"a\": \"x\u{0007}y\"}").unwrap_err();
1106 assert!(
1107 matches!(&err, OmnistError::Parse(e) if e.message.contains("control character")),
1108 "got {err:?}"
1109 )
1110 }
1111
1112 #[test]
1113 fn every_short_escape_reads_its_control_character() {
1114 let doc = read_json(r#"{"a": "\/\b\f"}"#).unwrap();
1115 let v = doc.root().get_one("a").unwrap();
1116 assert_eq!(
1117 *v.value().unwrap(),
1118 Scalar::Str("/\u{08}\u{0c}".to_string())
1119 );
1120 }
1121
1122 #[test]
1123 fn invalid_escape_character_is_a_parse_error() {
1124 let err = read_json(r#"{"a": "\q"}"#).unwrap_err();
1125 assert!(
1126 matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid escape")),
1127 "got {err:?}"
1128 )
1129 }
1130
1131 #[test]
1132 fn unpaired_high_surrogate_is_a_parse_error() {
1133 let err = read_json(r#"{"a": "\ud800x"}"#).unwrap_err();
1134 assert!(
1135 matches!(&err, OmnistError::Parse(e) if e.message.contains("unpaired high surrogate")),
1136 "got {err:?}"
1137 )
1138 }
1139
1140 #[test]
1141 fn high_surrogate_followed_by_non_low_surrogate_escape_is_a_parse_error() {
1142 const BSL: char = '\u{5c}';
1151 let input = format!("{{\"a\": \"{BSL}ud800{BSL}u0041\"}}");
1152 let err = read_json(&input).unwrap_err();
1153 assert!(
1154 matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid low surrogate")),
1155 "got {err:?}"
1156 )
1157 }
1158
1159 #[test]
1160 fn unpaired_low_surrogate_is_a_parse_error() {
1161 let err = read_json(r#"{"a": "\udc00"}"#).unwrap_err();
1162 assert!(
1163 matches!(&err, OmnistError::Parse(e) if e.message.contains("unpaired low surrogate")),
1164 "got {err:?}"
1165 )
1166 }
1167
1168 #[test]
1169 fn unterminated_unicode_escape_is_a_parse_error() {
1170 let err = read_json(r#"{"a": "\u12"#).unwrap_err();
1171 assert!(
1172 matches!(&err, OmnistError::Parse(e) if e.message.contains("unterminated unicode escape")),
1173 "got {err:?}"
1174 )
1175 }
1176
1177 #[test]
1178 fn invalid_hex_digit_in_unicode_escape_is_a_parse_error() {
1179 let err = read_json(r#"{"a": "\u12zz"}"#).unwrap_err();
1180 assert!(
1181 matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid hex digit")),
1182 "got {err:?}"
1183 )
1184 }
1185
1186 #[test]
1187 fn invalid_number_literal_is_a_parse_error() {
1188 let err = read_json(r#"{"a": -x}"#).unwrap_err();
1189 assert!(
1190 matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid number literal")),
1191 "got {err:?}"
1192 )
1193 }
1194
1195 fn doc_of(v: Value) -> Doc {
1198 Doc::of(&v).unwrap()
1199 }
1200
1201 #[test]
1202 fn round_trips_every_scalar_kind() {
1203 let v = obj(vec![
1204 ("null", Value::Null),
1205 ("bool", Value::Bool(true)),
1206 ("int", Value::Int((42).into())),
1207 ("float", Value::Float(1.5)),
1208 ("str", Value::Str("hi".to_string())),
1209 ]);
1210 let doc = doc_of(v);
1211 let text = write_json(&doc, None, false, None).unwrap();
1212 let back = read_json(&text).unwrap();
1213 assert!(doc.eq_doc(&back));
1214 }
1215
1216 #[test]
1217 fn round_trips_integral_float_at_and_above_1e17_boundary_issue_46() {
1218 for x in [1.0e17, 1.0e18, -1.23e17, 9.9e16_f64] {
1224 let doc = doc_of(obj(vec![("a", Value::Float(x))]));
1225 let text = write_json(&doc, None, false, None).unwrap();
1226 let back = read_json(&text).unwrap();
1227 assert_eq!(
1228 *back.root().get_one("a").unwrap().value().unwrap(),
1229 Scalar::Float(x),
1230 "x={x} text={text}"
1231 );
1232 }
1233 }
1234
1235 #[test]
1236 fn round_trips_temporal_like_strings_since_scalar_has_no_temporal_type() {
1237 let v = obj(vec![("d", Value::Str("2024-01-15".to_string()))]);
1241 let doc = doc_of(v);
1242 let mut rep = WriteReport::new();
1243 let text = write_json(&doc, None, false, Some(&mut rep)).unwrap();
1244 assert!(rep.is_empty());
1245 let back = read_json(&text).unwrap();
1246 assert!(doc.eq_doc(&back));
1247 }
1248
1249 #[test]
1250 fn writes_repeated_labels_as_a_json_array() {
1251 let doc = doc_of(obj(vec![(
1252 "m",
1253 Value::Array(vec![Value::Int((1).into()), Value::Int((2).into())]),
1254 )]));
1255 let text = write_json(&doc, None, false, None).unwrap();
1256 assert_eq!(text, r#"{"m": [1, 2]}"#);
1257 }
1258
1259 #[test]
1260 fn writes_compact_with_comma_space_separators() {
1261 let doc = doc_of(obj(vec![
1262 ("a", Value::Int((1).into())),
1263 ("b", Value::Int((2).into())),
1264 ]));
1265 let text = write_json(&doc, None, false, None).unwrap();
1266 assert_eq!(text, r#"{"a": 1, "b": 2}"#);
1267 }
1268
1269 #[test]
1270 fn writes_indented_multiline() {
1271 let doc = doc_of(obj(vec![("a", Value::Int((1).into()))]));
1272 let text = write_json(&doc, Some(2), false, None).unwrap();
1273 assert_eq!(text, "{\n \"a\": 1\n}");
1274 }
1275
1276 #[test]
1277 fn writes_empty_object_compactly() {
1278 let doc = doc_of(obj(vec![("o", Value::Object(IndexMap::new()))]));
1279 let text = write_json(&doc, Some(2), false, None).unwrap();
1280 assert!(text.contains("\"o\": {}"));
1281 }
1282
1283 #[test]
1284 fn an_empty_array_value_produces_no_edge_at_all() {
1285 let doc = doc_of(obj(vec![
1290 ("a", Value::Array(vec![])),
1291 ("b", Value::Int((1).into())),
1292 ]));
1293 let text = write_json(&doc, None, false, None).unwrap();
1294 assert_eq!(text, r#"{"b": 1}"#);
1295 }
1296
1297 #[test]
1305 fn write_fails_unconditionally_on_nan_and_infinity_lenient() {
1306 let doc = doc_of(obj(vec![
1307 ("a", Value::Float(f64::NAN)),
1308 ("b", Value::Float(f64::INFINITY)),
1309 ]));
1310 let mut rep = WriteReport::new();
1311 let err = write_json(&doc, None, false, Some(&mut rep)).unwrap_err();
1312 assert!(err.to_string().contains("write.unsupported-value"));
1313 assert!(err.to_string().contains("$.a"));
1314 assert!(rep.is_empty());
1318 assert!(err.report().is_none());
1319 }
1320
1321 #[test]
1325 fn write_fails_unconditionally_on_nan_strict() {
1326 let doc = doc_of(obj(vec![("a", Value::Float(f64::NAN))]));
1327 let err = write_json(&doc, None, true, None).unwrap_err();
1328 assert!(err.to_string().contains("write.unsupported-value"));
1329 assert!(err.report().is_none());
1330 }
1331
1332 #[test]
1333 fn strict_write_with_no_adjustments_succeeds() {
1334 let doc = doc_of(obj(vec![("a", Value::Int((1).into()))]));
1335 let text = write_json(&doc, None, true, None).unwrap();
1336 assert_eq!(text, r#"{"a": 1}"#);
1337 }
1338
1339 #[test]
1340 fn check_json_reports_without_producing_output() {
1341 let doc = doc_of(obj(vec![("a", Value::Float(f64::INFINITY))]));
1342 let rep = check_json(&doc);
1343 assert_eq!(rep.len(), 1);
1344 assert_eq!(rep.adjustments()[0].path, "$.a");
1345 assert_eq!(rep.adjustments()[0].code, "write.unsupported-value");
1349 }
1350
1351 #[test]
1352 fn writes_string_escapes() {
1353 let doc = doc_of(obj(vec![("s", Value::Str("a\n\"\\\tb".to_string()))]));
1354 let text = write_json(&doc, None, false, None).unwrap();
1355 assert_eq!(text, r#"{"s": "a\n\"\\\tb"}"#);
1356 }
1357
1358 #[test]
1359 fn writes_unicode_without_escaping_non_ascii() {
1360 let doc = doc_of(obj(vec![("s", Value::Str("caf\u{e9}".to_string()))]));
1362 let text = write_json(&doc, None, false, None).unwrap();
1363 assert_eq!(text, "{\"s\": \"caf\u{e9}\"}");
1364 }
1365
1366 #[test]
1367 fn writes_float_with_trailing_dot_zero_for_integral_values() {
1368 let doc = doc_of(obj(vec![("f", Value::Float(2.0))]));
1369 let text = write_json(&doc, None, false, None).unwrap();
1370 assert_eq!(text, r#"{"f": 2.0}"#);
1371 }
1372
1373 #[test]
1378 fn write_fails_unconditionally_on_negative_infinity() {
1379 let doc = doc_of(obj(vec![("f", Value::Float(f64::NEG_INFINITY))]));
1380 let err = write_json(&doc, None, true, None).unwrap_err();
1381 assert!(err.to_string().contains("write.unsupported-value"));
1382 assert!(err.report().is_none());
1383 }
1384
1385 #[test]
1386 fn write_float_directly_covers_every_branch() {
1387 let mut out = String::new();
1388 write_float(f64::NAN, &mut out);
1389 assert_eq!(out, "NaN");
1390 out.clear();
1391 write_float(f64::INFINITY, &mut out);
1392 assert_eq!(out, "Infinity");
1393 out.clear();
1394 write_float(f64::NEG_INFINITY, &mut out);
1395 assert_eq!(out, "-Infinity");
1396 out.clear();
1397 write_float(1.5, &mut out);
1398 assert_eq!(out, "1.5");
1399 }
1400
1401 #[test]
1402 fn writes_carriage_return_and_control_character_escapes() {
1403 let doc = doc_of(obj(vec![(
1404 "s",
1405 Value::Str("a\rb\u{08}c\u{0c}d\u{01}".to_string()),
1406 )]));
1407 let text = write_json(&doc, None, false, None).unwrap();
1408 const BS: char = '\u{5c}';
1409 let expected = format!("{{\"s\": \"a{BS}rb{BS}bc{BS}fd{BS}u0001\"}}");
1410 assert_eq!(text, expected);
1411 }
1412
1413 #[test]
1414 fn deeply_nested_document_write_reuses_doc_construction_depth_guard() {
1415 let mut v = Value::Int((0).into());
1419 for _ in 0..=crate::document::MAX_DEPTH {
1420 v = obj(vec![("a", v)]);
1421 }
1422 assert!(Doc::of(&v).is_err());
1423 }
1424
1425 #[test]
1426 fn round_trip_via_live_python_equivalent_scalars() {
1427 let doc = doc_of(obj(vec![
1432 ("a", Value::Int((1).into())),
1433 ("b", Value::Str("x".to_string())),
1434 (
1435 "c",
1436 Value::Array(vec![
1437 Value::Int((1).into()),
1438 Value::Int((2).into()),
1439 Value::Int((3).into()),
1440 ]),
1441 ),
1442 ]));
1443 let text = write_json(&doc, None, false, None).unwrap();
1444 assert_eq!(text, r#"{"a": 1, "b": "x", "c": [1, 2, 3]}"#);
1445 }
1446
1447 #[test]
1448 fn test_deeply_nested_json_depth_limit() {
1449 let nested = "[".repeat(50_000) + &"]".repeat(50_000);
1450 let err = read_json(&nested).unwrap_err();
1451 assert!(err.to_string().contains("maximum depth"));
1452 }
1453
1454 fn interleaved_doc() -> Doc {
1462 Doc::from_raw(crate::document::RawNode::Edges(vec![
1464 (
1465 "m".to_string(),
1466 crate::document::RawNode::Leaf(Scalar::Str("A".to_string())),
1467 ),
1468 (
1469 "x".to_string(),
1470 crate::document::RawNode::Leaf(Scalar::Str("X".to_string())),
1471 ),
1472 (
1473 "m".to_string(),
1474 crate::document::RawNode::Leaf(Scalar::Str("B".to_string())),
1475 ),
1476 ]))
1477 .unwrap()
1478 }
1479
1480 fn contiguous_repeat_doc() -> Doc {
1481 Doc::from_raw(crate::document::RawNode::Edges(vec![
1483 (
1484 "m".to_string(),
1485 crate::document::RawNode::Leaf(Scalar::Str("A".to_string())),
1486 ),
1487 (
1488 "m".to_string(),
1489 crate::document::RawNode::Leaf(Scalar::Str("B".to_string())),
1490 ),
1491 (
1492 "x".to_string(),
1493 crate::document::RawNode::Leaf(Scalar::Str("X".to_string())),
1494 ),
1495 ]))
1496 .unwrap()
1497 }
1498
1499 #[test]
1500 fn reports_interleaving_lost_on_write() {
1501 let doc = interleaved_doc();
1502 let mut report = crate::report::WriteReport::new();
1503 let text = write_json(&doc, None, false, Some(&mut report)).unwrap();
1504 assert_eq!(text, r#"{"m": ["A", "B"], "x": "X"}"#);
1505 let adjustments = report.adjustments();
1506 assert_eq!(adjustments.len(), 1);
1507 assert_eq!(adjustments[0].path, "$");
1508 assert_eq!(adjustments[0].code, "format.interleaving-lost");
1509 assert_eq!(adjustments[0].severity, Severity::Warning);
1510 }
1511
1512 #[test]
1513 fn check_json_reports_interleaving_lost() {
1514 let rep = check_json(&interleaved_doc());
1515 assert_eq!(rep.adjustments().len(), 1);
1516 assert_eq!(rep.adjustments()[0].code, "format.interleaving-lost");
1517 }
1518
1519 #[test]
1520 fn contiguous_repeated_label_does_not_report_interleaving_lost() {
1521 let doc = contiguous_repeat_doc();
1522 let mut report = crate::report::WriteReport::new();
1523 let text = write_json(&doc, None, false, Some(&mut report)).unwrap();
1524 assert_eq!(text, r#"{"m": ["A", "B"], "x": "X"}"#);
1525 assert!(report.is_empty());
1526 assert!(check_json(&doc).is_empty());
1527 }
1528}