use crate::WriteError;
use crate::document::{Doc, Value};
use crate::error::{OmnistError, ParseError};
use crate::formats::float_fmt;
use crate::formats::int_cap::{MAX_INT_DIGITS, over_cap_message};
use crate::formats::string_escape::{JSON_ESCAPES, write_quoted};
use crate::formats::textpos::line_col_bytes;
use crate::report::{Severity, WriteReport};
use indexmap::IndexMap;
pub fn read_json(text: &str) -> Result<Doc, OmnistError> {
let mut p = Parser::new(text);
p.skip_ws();
let value = p.parse_value()?;
p.skip_ws();
if p.pos < p.n {
return Err(p
.error_at(
p.pos,
"unexpected trailing data after JSON value".to_string(),
)
.into());
}
Ok(Doc::of(&value)?)
}
pub fn write_json(
doc: &Doc,
indent: Option<usize>,
strict: bool,
report: Option<&mut WriteReport>,
) -> Result<String, WriteError> {
let grouped = doc.to_grouped();
let rep = check_json_grouped(&grouped);
let prepared = if strict { grouped } else { prepare(grouped) };
let mut out = String::new();
write_value(&prepared, indent, 0, &mut out);
crate::report::finish_write(out, rep, strict, report)
}
pub fn check_json(doc: &Doc) -> WriteReport {
let grouped = doc.to_grouped();
check_json_grouped(&grouped)
}
fn check_json_grouped(grouped: &Value) -> WriteReport {
let mut rep = WriteReport::new();
let mut path = String::from("$");
crate::formats::visit_grouped(grouped, &mut path, &mut |visited, path| {
let crate::formats::Visited::Node { value } = visited else {
return;
};
match value {
Value::Float(x) if x.is_nan() || x.is_infinite() => {
rep.add(
path,
"float.special",
format!("{x} is not valid JSON; wrote null"),
Severity::Error,
);
}
Value::Date(_) | Value::Time(_) | Value::Datetime(_) => {
rep.add(
path,
"format.temporal-stringified",
"date/time/datetime has no native JSON literal; wrote as an ISO-8601 string"
.to_string(),
Severity::Warning,
);
}
_ => {}
}
});
rep
}
pub(crate) struct Json;
impl crate::formats::Codec for Json {
const NAME: &'static str = "json";
fn read(text: &str) -> Result<Doc, OmnistError> {
read_json(text)
}
fn write(doc: &Doc) -> Result<String, OmnistError> {
write_json(doc, None, false, None).map_err(Into::into)
}
fn check(doc: &Doc) -> WriteReport {
check_json(doc)
}
}
fn prepare(node: Value) -> Value {
match node {
Value::Object(map) => Value::Object(
map.into_iter()
.map(|(k, v)| (k, prepare(v)))
.collect::<IndexMap<_, _>>(),
),
Value::Array(items) => Value::Array(items.into_iter().map(prepare).collect()),
Value::Float(x) if x.is_nan() || x.is_infinite() => Value::Null,
other => other,
}
}
fn write_value(v: &Value, indent: Option<usize>, level: usize, out: &mut String) {
match v {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Int(i) => out.push_str(&i.to_string()),
Value::Float(x) => write_float(*x, out),
Value::Str(s) | Value::Date(s) | Value::Time(s) | Value::Datetime(s) => {
write_json_string(s, out)
}
Value::Array(items) => write_seq(items.iter(), '[', ']', indent, level, out, write_value),
Value::Object(map) => write_seq(
map.iter(),
'{',
'}',
indent,
level,
out,
|(k, val), indent, level, out| {
write_json_string(k, out);
out.push_str(": ");
write_value(val, indent, level, out);
},
),
}
}
fn write_seq<I, T>(
items: I,
open: char,
close: char,
indent: Option<usize>,
level: usize,
out: &mut String,
mut write_item: impl FnMut(T, Option<usize>, usize, &mut String),
) where
I: ExactSizeIterator<Item = T>,
{
if items.len() == 0 {
out.push(open);
out.push(close);
return;
}
out.push(open);
let child_level = level + 1;
let mut first = true;
for item in items {
if !first {
out.push(',');
if indent.is_none() {
out.push(' ');
}
}
first = false;
if let Some(n) = indent {
out.push('\n');
out.push_str(&" ".repeat(n * child_level));
}
write_item(item, indent, child_level, out);
}
if let Some(n) = indent {
out.push('\n');
out.push_str(&" ".repeat(n * level));
}
out.push(close);
}
fn write_float(x: f64, out: &mut String) {
float_fmt::write_float(x, "NaN", "Infinity", "-Infinity", out);
}
fn write_json_string(s: &str, out: &mut String) {
write_quoted(s, &JSON_ESCAPES, out);
}
struct Parser<'a> {
text: &'a str,
n: usize,
pos: usize,
depth: usize,
}
impl<'a> Parser<'a> {
fn new(text: &'a str) -> Self {
let n = text.len();
Parser {
text,
n,
pos: 0,
depth: 0,
}
}
fn error_at(&self, pos: usize, msg: String) -> ParseError {
let (line, col) = line_col_bytes(self.text, pos);
ParseError::new(line, col, format!("invalid JSON: {msg}"))
}
fn char_at(&self, at: usize) -> Option<char> {
self.text.get(at..)?.chars().next()
}
fn peek(&self) -> Option<char> {
self.char_at(self.pos)
}
fn skip_ws(&mut self) {
while matches!(
self.peek(),
Some(' ') | Some('\t') | Some('\n') | Some('\r')
) {
self.pos += 1;
}
}
fn expect(&mut self, c: char) -> Result<(), ParseError> {
if self.peek() == Some(c) {
self.pos += c.len_utf8();
Ok(())
} else {
Err(self.error_at(self.pos, format!("expected {c:?}")))
}
}
fn matches_word(&self, word: &str) -> bool {
debug_assert!(
word.is_ascii(),
"matches_word is only used with ASCII keywords"
);
let bytes = self.text.as_bytes();
word.bytes()
.enumerate()
.all(|(i, b)| bytes.get(self.pos + i) == Some(&b))
}
fn parse_value(&mut self) -> Result<Value, ParseError> {
self.skip_ws();
match self.peek() {
None => Err(self.error_at(self.pos, "unexpected end of input".to_string())),
Some('{') => self.parse_object(),
Some('[') => self.parse_array(),
Some('"') => Ok(Value::Str(self.parse_string()?)),
Some('t') if self.matches_word("true") => {
self.pos += 4;
Ok(Value::Bool(true))
}
Some('f') if self.matches_word("false") => {
self.pos += 5;
Ok(Value::Bool(false))
}
Some('n') if self.matches_word("null") => {
self.pos += 4;
Ok(Value::Null)
}
Some('N') if self.matches_word("NaN") => {
self.pos += 3;
Ok(Value::Float(f64::NAN))
}
Some('I') if self.matches_word("Infinity") => {
self.pos += 8;
Ok(Value::Float(f64::INFINITY))
}
Some('-') if self.matches_word("-Infinity") => {
self.pos += 9;
Ok(Value::Float(f64::NEG_INFINITY))
}
Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
Some(c) => Err(self.error_at(self.pos, format!("unexpected character {c:?}"))),
}
}
fn parse_object(&mut self) -> Result<Value, ParseError> {
self.depth += 1;
if self.depth > crate::document::MAX_DEPTH {
return Err(self.error_at(
self.pos,
format!(
"nesting exceeds the maximum depth ({})",
crate::document::MAX_DEPTH
),
));
}
self.expect('{')?;
let mut map: IndexMap<String, Value> = IndexMap::new();
self.skip_ws();
if self.peek() == Some('}') {
self.pos += 1;
self.depth -= 1;
return Ok(Value::Object(map));
}
loop {
self.skip_ws();
if self.peek() != Some('"') {
return Err(self.error_at(self.pos, "expected string key".to_string()));
}
let key = self.parse_string()?;
self.skip_ws();
self.expect(':')?;
let value = self.parse_value()?;
map.insert(key, value);
self.skip_ws();
match self.peek() {
Some(',') => {
self.pos += 1;
}
Some('}') => {
self.pos += 1;
break;
}
_ => return Err(self.error_at(self.pos, "expected ',' or '}'".to_string())),
}
}
self.depth -= 1;
Ok(Value::Object(map))
}
fn parse_array(&mut self) -> Result<Value, ParseError> {
self.depth += 1;
if self.depth > crate::document::MAX_DEPTH {
return Err(self.error_at(
self.pos,
format!(
"nesting exceeds the maximum depth ({})",
crate::document::MAX_DEPTH
),
));
}
self.expect('[')?;
let mut items = Vec::new();
self.skip_ws();
if self.peek() == Some(']') {
self.pos += 1;
self.depth -= 1;
return Ok(Value::Array(items));
}
loop {
let v = self.parse_value()?;
items.push(v);
self.skip_ws();
match self.peek() {
Some(',') => {
self.pos += 1;
}
Some(']') => {
self.pos += 1;
break;
}
_ => return Err(self.error_at(self.pos, "expected ',' or ']'".to_string())),
}
}
self.depth -= 1;
Ok(Value::Array(items))
}
fn parse_string(&mut self) -> Result<String, ParseError> {
self.expect('"')?;
let mut s = String::new();
loop {
match self.peek() {
None => return Err(self.error_at(self.pos, "unterminated string".to_string())),
Some('"') => {
self.pos += 1;
break;
}
Some('\\') => {
self.pos += 1;
match self.peek() {
Some('"') => {
s.push('"');
self.pos += 1;
}
Some('\\') => {
s.push('\\');
self.pos += 1;
}
Some('/') => {
s.push('/');
self.pos += 1;
}
Some('b') => {
s.push('\u{08}');
self.pos += 1;
}
Some('f') => {
s.push('\u{0c}');
self.pos += 1;
}
Some('n') => {
s.push('\n');
self.pos += 1;
}
Some('r') => {
s.push('\r');
self.pos += 1;
}
Some('t') => {
s.push('\t');
self.pos += 1;
}
Some('u') => {
self.pos += 1;
let hi = self.parse_hex4()?;
if (0xD800..=0xDBFF).contains(&hi) {
if self.peek() == Some('\\')
&& self.char_at(self.pos + 1) == Some('u')
{
self.pos += 2;
let lo = self.parse_hex4()?;
if (0xDC00..=0xDFFF).contains(&lo) {
let c = 0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00);
s.push(char::from_u32(c).expect(
"a well-formed UTF-16 surrogate pair always \
combines to a valid supplementary-plane char",
));
} else {
return Err(self.error_at(
self.pos,
"invalid low surrogate".to_string(),
));
}
} else {
return Err(self.error_at(
self.pos,
"unpaired high surrogate".to_string(),
));
}
} else if (0xDC00..=0xDFFF).contains(&hi) {
return Err(
self.error_at(self.pos, "unpaired low surrogate".to_string())
);
} else {
s.push(char::from_u32(hi).expect(
"a 4-hex-digit \\u escape outside the surrogate range is \
always a valid BMP char",
));
}
}
_ => return Err(self.error_at(self.pos, "invalid escape".to_string())),
}
}
Some(c) if (c as u32) < 0x20 => {
return Err(self.error_at(self.pos, "control character in string".to_string()));
}
Some(c) => {
s.push(c);
self.pos += c.len_utf8();
}
}
}
Ok(s)
}
fn parse_hex4(&mut self) -> Result<u32, ParseError> {
let mut v: u32 = 0;
for _ in 0..4 {
let c = self.peek().ok_or_else(|| {
self.error_at(self.pos, "unterminated unicode escape".to_string())
})?;
let d = c.to_digit(16).ok_or_else(|| {
self.error_at(self.pos, "invalid hex digit in unicode escape".to_string())
})?;
v = v * 16 + d;
self.pos += 1;
}
Ok(v)
}
fn parse_number(&mut self) -> Result<Value, ParseError> {
let start = self.pos;
if self.peek() == Some('-') {
self.pos += 1;
}
let int_start = self.pos;
if self.peek() == Some('0') {
self.pos += 1;
} else if self.peek().is_some_and(|c| c.is_ascii_digit()) {
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
self.pos += 1;
}
} else {
return Err(self.error_at(self.pos, "invalid number literal".to_string()));
}
debug_assert!(
self.pos > int_start,
"the '0' and digit-run branches above both advance pos by at least 1"
);
let bytes = self.text.as_bytes();
let byte_at = |p: usize| bytes.get(p).copied();
let mut is_float = false;
if self.peek() == Some('.') {
let frac_start = self.pos + 1;
let mut p = frac_start;
while byte_at(p).is_some_and(|b| b.is_ascii_digit()) {
p += 1;
}
if p > frac_start {
is_float = true;
self.pos = p;
}
}
if matches!(self.peek(), Some('e') | Some('E')) {
let mut p = self.pos + 1;
if matches!(byte_at(p), Some(b'+') | Some(b'-')) {
p += 1;
}
let exp_start = p;
while byte_at(p).is_some_and(|b| b.is_ascii_digit()) {
p += 1;
}
if p > exp_start {
is_float = true;
self.pos = p;
}
}
let text: &str = &self.text[start..self.pos];
if is_float {
let v: f64 = text
.parse()
.expect("scanner only emits number-shaped text, which f64::from_str always parses");
Ok(Value::Float(v))
} else {
let digits = &text[if text.starts_with('-') { 1 } else { 0 }..];
if digits.len() > MAX_INT_DIGITS {
return Err(self.error_at(start, over_cap_message("", digits.len())));
}
let v = num_bigint::BigInt::parse_bytes(text.as_bytes(), 10).expect(
"scanner only emits digit-shaped text, which BigInt::parse_bytes always parses",
);
Ok(Value::Int(v))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::{Doc, Scalar, Value};
use crate::report::Severity;
fn obj(pairs: Vec<(&str, Value)>) -> Value {
Value::Object(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
}
#[test]
fn reads_object_with_scalars() {
let doc = read_json(r#"{"a": 1, "b": "s", "c": true, "d": null, "e": 1.5}"#).unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Str("s".to_string())
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Bool(true)
);
assert_eq!(*root.get_one("d").unwrap().value().unwrap(), Scalar::Null);
assert_eq!(
*root.get_one("e").unwrap().value().unwrap(),
Scalar::Float(1.5)
);
}
#[test]
fn reads_false_literal() {
let doc = read_json(r#"{"c": false}"#).unwrap();
assert_eq!(
*doc.root().get_one("c").unwrap().value().unwrap(),
Scalar::Bool(false)
);
}
#[test]
fn reads_array_as_repeated_edges() {
let doc = read_json(r#"{"m": [1, 2, 3]}"#).unwrap();
let root = doc.root();
let ms = root.get("m");
assert_eq!(ms.len(), 3);
assert_eq!(*ms[0].value().unwrap(), Scalar::Int((1).into()));
assert_eq!(*ms[2].value().unwrap(), Scalar::Int((3).into()));
}
#[test]
fn reads_empty_array_literal_as_no_edges() {
let doc = read_json(r#"{"m": [], "n": 1}"#).unwrap();
assert!(doc.root().get("m").is_empty());
}
#[test]
fn reads_nested_object() {
let doc = read_json(r#"{"a": {"b": {"c": 1}}}"#).unwrap();
let root = doc.root();
let a = root.get_one("a").unwrap();
let b = a.get_one("b").unwrap();
assert_eq!(
*b.get_one("c").unwrap().value().unwrap(),
Scalar::Int((1).into())
);
}
#[test]
fn bare_top_level_array_is_a_document_error_not_a_parse_error() {
let err = read_json("[1, 2, 3]").unwrap_err();
assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
}
#[test]
fn array_of_arrays_is_a_document_error() {
let err = read_json(r#"{"m": [[1, 2]]}"#).unwrap_err();
assert!(matches!(err, OmnistError::Document(_)), "got {err:?}");
}
#[test]
fn invalid_json_syntax_is_a_parse_error() {
let err = read_json("{not json}").unwrap_err();
assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
}
#[test]
fn trailing_data_after_a_value_is_a_parse_error() {
let err = read_json("1 2").unwrap_err();
assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
}
#[test]
fn nesting_past_max_depth_is_a_parse_error() {
let mut text = String::new();
for _ in 0..=crate::document::MAX_DEPTH {
text.push_str(r#"{"a":"#);
}
text.push('1');
for _ in 0..=crate::document::MAX_DEPTH {
text.push('}');
}
let err = read_json(&text).unwrap_err();
assert!(matches!(err, OmnistError::Parse(_)), "got {err:?}");
assert!(err.to_string().contains("maximum depth"));
}
#[test]
fn duplicate_object_keys_last_value_wins_first_position_kept() {
let doc = read_json(r#"{"a": 1, "b": 2, "a": 3}"#).unwrap();
let root = doc.root();
assert_eq!(root.labels(), vec!["a".to_string(), "b".to_string()]);
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Int((3).into())
);
}
#[test]
fn reads_string_escapes_and_unicode_escape() {
let doc = read_json(r#"{"s": "a\n\r\t\"\\é"}"#).unwrap();
let v = doc.root().get_one("s").unwrap();
assert_eq!(
*v.value().unwrap(),
Scalar::Str("a\n\r\t\"\\\u{e9}".to_string())
);
}
#[test]
fn reads_plain_unicode_escape() {
const BSL: char = '\u{5c}';
let input = format!("{{\"s\": \"{BSL}u0041\"}}");
let doc = read_json(&input).unwrap();
let v = doc.root().get_one("s").unwrap();
assert_eq!(*v.value().unwrap(), Scalar::Str("A".to_string()));
}
#[test]
fn reads_surrogate_pair_escape() {
let doc = read_json(r#"{"s": "😀"}"#).unwrap();
let v = doc.root().get_one("s").unwrap();
assert_eq!(*v.value().unwrap(), Scalar::Str("\u{1F600}".to_string()));
}
#[test]
fn reads_surrogate_pair_written_as_two_u_escapes() {
const BSL: char = '\u{5c}';
let input = format!("{{\"s\": \"{BSL}ud83d{BSL}ude00\"}}");
let doc = read_json(&input).unwrap();
let v = doc.root().get_one("s").unwrap();
assert_eq!(*v.value().unwrap(), Scalar::Str("\u{1F600}".to_string()));
}
#[test]
fn reads_bare_nan_and_infinity_tokens() {
let doc = read_json(r#"{"a": NaN, "b": Infinity, "c": -Infinity}"#).unwrap();
let root = doc.root();
assert!(
matches!(root.get_one("a").unwrap().value().unwrap(), Scalar::Float(x) if x.is_nan())
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Float(f64::INFINITY)
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Float(f64::NEG_INFINITY)
);
}
#[test]
fn integer_literal_under_digit_cap_but_over_i64_range_parses() {
let text = format!(r#"{{"a": {}}}"#, "9".repeat(20));
let doc = read_json(&text).unwrap();
let value = doc.root().child("a").unwrap().value().unwrap();
assert_eq!(
value,
&Scalar::Int(num_bigint::BigInt::parse_bytes(b"99999999999999999999", 10).unwrap())
);
}
#[test]
fn integer_literal_over_digit_cap_is_rejected_before_range_check() {
let text = format!(r#"{{"a": {}}}"#, "9".repeat(MAX_INT_DIGITS + 1));
let err = read_json(&text).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("4300-digit")),
"got {err:?}"
)
}
#[test]
fn integer_literal_exactly_at_digit_cap_parses_not_digit_cap_error() {
let text = format!(r#"{{"a": {}}}"#, "9".repeat(MAX_INT_DIGITS));
let doc = read_json(&text).unwrap();
let value = doc.root().child("a").unwrap().value().unwrap();
assert!(
matches!(value, Scalar::Int(i) if i.to_string().len() == MAX_INT_DIGITS),
"got {value:?}"
);
}
#[test]
fn whitespace_and_negative_zero_and_exponent_numbers_read() {
let doc = read_json(" {\n\"a\" : 1e3,\n\"b\": -0.5, \"c\": 2E-2\t} ").unwrap();
let root = doc.root();
assert_eq!(
*root.get_one("a").unwrap().value().unwrap(),
Scalar::Float(1000.0)
);
assert_eq!(
*root.get_one("b").unwrap().value().unwrap(),
Scalar::Float(-0.5)
);
assert_eq!(
*root.get_one("c").unwrap().value().unwrap(),
Scalar::Float(0.02)
);
}
#[test]
fn error_position_after_multibyte_content_reports_correct_line() {
let err = read_json("{\"s\": \"café \u{1F600}\"}\n@").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.line == 2),
"got {err:?}"
);
}
#[test]
fn empty_object_and_array_read() {
let doc = read_json(r#"{"a": {}}"#).unwrap();
assert!(doc.root().get_one("a").unwrap().edges().unwrap().is_empty());
}
#[test]
fn empty_input_is_unexpected_end_of_input_error() {
let err = read_json("").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("unexpected end of input")),
"got {err:?}"
)
}
#[test]
fn unrecognized_character_is_a_parse_error() {
let err = read_json("@").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("unexpected character")),
"got {err:?}"
)
}
#[test]
fn a_bareword_that_only_partially_matches_a_keyword_is_unexpected_character() {
let err = read_json("tx").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("unexpected character")),
"got {err:?}"
)
}
#[test]
fn error_position_reports_the_line_after_a_newline() {
let err = read_json("{\n \"a\": @\n}").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.line == 2),
"got {err:?}"
);
}
#[test]
fn object_missing_colon_is_a_parse_error() {
let err = read_json(r#"{"a" 1}"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("expected ':'")),
"got {err:?}"
)
}
#[test]
fn object_missing_key_is_a_parse_error() {
let err = read_json(r#"{1: 2}"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("expected string key")),
"got {err:?}"
)
}
#[test]
fn object_missing_comma_or_brace_is_a_parse_error() {
let err = read_json(r#"{"a": 1 "b": 2}"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("expected ',' or '}'")),
"got {err:?}"
)
}
#[test]
fn array_missing_comma_or_bracket_is_a_parse_error() {
let err = read_json(r#"{"a": [1 2]}"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("expected ',' or ']'")),
"got {err:?}"
)
}
#[test]
fn unterminated_string_is_a_parse_error() {
let err = read_json(r#"{"a": "hi}"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("unterminated string")),
"got {err:?}"
)
}
#[test]
fn control_character_in_string_is_a_parse_error() {
let err = read_json("{\"a\": \"x\u{0007}y\"}").unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("control character")),
"got {err:?}"
)
}
#[test]
fn every_short_escape_reads_its_control_character() {
let doc = read_json(r#"{"a": "\/\b\f"}"#).unwrap();
let v = doc.root().get_one("a").unwrap();
assert_eq!(
*v.value().unwrap(),
Scalar::Str("/\u{08}\u{0c}".to_string())
);
}
#[test]
fn invalid_escape_character_is_a_parse_error() {
let err = read_json(r#"{"a": "\q"}"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid escape")),
"got {err:?}"
)
}
#[test]
fn unpaired_high_surrogate_is_a_parse_error() {
let err = read_json(r#"{"a": "\ud800x"}"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("unpaired high surrogate")),
"got {err:?}"
)
}
#[test]
fn high_surrogate_followed_by_non_low_surrogate_escape_is_a_parse_error() {
const BSL: char = '\u{5c}';
let input = format!("{{\"a\": \"{BSL}ud800{BSL}u0041\"}}");
let err = read_json(&input).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid low surrogate")),
"got {err:?}"
)
}
#[test]
fn unpaired_low_surrogate_is_a_parse_error() {
let err = read_json(r#"{"a": "\udc00"}"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("unpaired low surrogate")),
"got {err:?}"
)
}
#[test]
fn unterminated_unicode_escape_is_a_parse_error() {
let err = read_json(r#"{"a": "\u12"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("unterminated unicode escape")),
"got {err:?}"
)
}
#[test]
fn invalid_hex_digit_in_unicode_escape_is_a_parse_error() {
let err = read_json(r#"{"a": "\u12zz"}"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid hex digit")),
"got {err:?}"
)
}
#[test]
fn invalid_number_literal_is_a_parse_error() {
let err = read_json(r#"{"a": -x}"#).unwrap_err();
assert!(
matches!(&err, OmnistError::Parse(e) if e.message.contains("invalid number literal")),
"got {err:?}"
)
}
fn doc_of(v: Value) -> Doc {
Doc::of(&v).unwrap()
}
#[test]
fn round_trips_every_scalar_kind() {
let v = obj(vec![
("null", Value::Null),
("bool", Value::Bool(true)),
("int", Value::Int((42).into())),
("float", Value::Float(1.5)),
("str", Value::Str("hi".to_string())),
]);
let doc = doc_of(v);
let text = write_json(&doc, None, false, None).unwrap();
let back = read_json(&text).unwrap();
assert!(doc.eq_doc(&back));
}
#[test]
fn round_trips_integral_float_at_and_above_1e17_boundary_issue_46() {
for x in [1.0e17, 1.0e18, -1.23e17, 9.9e16_f64] {
let doc = doc_of(obj(vec![("a", Value::Float(x))]));
let text = write_json(&doc, None, false, None).unwrap();
let back = read_json(&text).unwrap();
assert_eq!(
*back.root().get_one("a").unwrap().value().unwrap(),
Scalar::Float(x),
"x={x} text={text}"
);
}
}
#[test]
fn round_trips_temporal_like_strings_since_scalar_has_no_temporal_type() {
let v = obj(vec![("d", Value::Str("2024-01-15".to_string()))]);
let doc = doc_of(v);
let mut rep = WriteReport::new();
let text = write_json(&doc, None, false, Some(&mut rep)).unwrap();
assert!(rep.is_empty());
let back = read_json(&text).unwrap();
assert!(doc.eq_doc(&back));
}
#[test]
fn writes_repeated_labels_as_a_json_array() {
let doc = doc_of(obj(vec![(
"m",
Value::Array(vec![Value::Int((1).into()), Value::Int((2).into())]),
)]));
let text = write_json(&doc, None, false, None).unwrap();
assert_eq!(text, r#"{"m": [1, 2]}"#);
}
#[test]
fn writes_compact_with_comma_space_separators() {
let doc = doc_of(obj(vec![
("a", Value::Int((1).into())),
("b", Value::Int((2).into())),
]));
let text = write_json(&doc, None, false, None).unwrap();
assert_eq!(text, r#"{"a": 1, "b": 2}"#);
}
#[test]
fn writes_indented_multiline() {
let doc = doc_of(obj(vec![("a", Value::Int((1).into()))]));
let text = write_json(&doc, Some(2), false, None).unwrap();
assert_eq!(text, "{\n \"a\": 1\n}");
}
#[test]
fn writes_empty_object_compactly() {
let doc = doc_of(obj(vec![("o", Value::Object(IndexMap::new()))]));
let text = write_json(&doc, Some(2), false, None).unwrap();
assert!(text.contains("\"o\": {}"));
}
#[test]
fn an_empty_array_value_produces_no_edge_at_all() {
let doc = doc_of(obj(vec![
("a", Value::Array(vec![])),
("b", Value::Int((1).into())),
]));
let text = write_json(&doc, None, false, None).unwrap();
assert_eq!(text, r#"{"b": 1}"#);
}
#[test]
fn lenient_write_substitutes_nan_and_infinity_with_null_and_reports_error_severity() {
let doc = doc_of(obj(vec![
("a", Value::Float(f64::NAN)),
("b", Value::Float(f64::INFINITY)),
]));
let mut rep = WriteReport::new();
let text = write_json(&doc, None, false, Some(&mut rep)).unwrap();
assert_eq!(text, r#"{"a": null, "b": null}"#);
assert_eq!(rep.len(), 2);
assert!(rep.errors().iter().all(|a| a.severity == Severity::Error));
assert!(!rep.is_ok());
}
#[test]
fn strict_write_raises_on_nan_and_carries_the_report() {
let doc = doc_of(obj(vec![("a", Value::Float(f64::NAN))]));
let err = write_json(&doc, None, true, None).unwrap_err();
let rep = err.report().expect("strict WriteError carries a report");
assert_eq!(rep.len(), 1);
assert_eq!(rep.adjustments()[0].code, "float.special");
}
#[test]
fn strict_write_with_no_adjustments_succeeds() {
let doc = doc_of(obj(vec![("a", Value::Int((1).into()))]));
let text = write_json(&doc, None, true, None).unwrap();
assert_eq!(text, r#"{"a": 1}"#);
}
#[test]
fn check_json_reports_without_producing_output() {
let doc = doc_of(obj(vec![("a", Value::Float(f64::INFINITY))]));
let rep = check_json(&doc);
assert_eq!(rep.len(), 1);
assert_eq!(rep.adjustments()[0].path, "$.a");
}
#[test]
fn writes_string_escapes() {
let doc = doc_of(obj(vec![("s", Value::Str("a\n\"\\\tb".to_string()))]));
let text = write_json(&doc, None, false, None).unwrap();
assert_eq!(text, r#"{"s": "a\n\"\\\tb"}"#);
}
#[test]
fn writes_unicode_without_escaping_non_ascii() {
let doc = doc_of(obj(vec![("s", Value::Str("caf\u{e9}".to_string()))]));
let text = write_json(&doc, None, false, None).unwrap();
assert_eq!(text, "{\"s\": \"caf\u{e9}\"}");
}
#[test]
fn writes_float_with_trailing_dot_zero_for_integral_values() {
let doc = doc_of(obj(vec![("f", Value::Float(2.0))]));
let text = write_json(&doc, None, false, None).unwrap();
assert_eq!(text, r#"{"f": 2.0}"#);
}
#[test]
fn strict_write_of_negative_infinity_renders_the_bare_token() {
let doc = doc_of(obj(vec![("f", Value::Float(f64::NEG_INFINITY))]));
let err = write_json(&doc, None, true, None).unwrap_err();
assert_eq!(err.report().unwrap().len(), 1);
}
#[test]
fn write_float_directly_covers_every_branch() {
let mut out = String::new();
write_float(f64::NAN, &mut out);
assert_eq!(out, "NaN");
out.clear();
write_float(f64::INFINITY, &mut out);
assert_eq!(out, "Infinity");
out.clear();
write_float(f64::NEG_INFINITY, &mut out);
assert_eq!(out, "-Infinity");
out.clear();
write_float(1.5, &mut out);
assert_eq!(out, "1.5");
}
#[test]
fn writes_carriage_return_and_control_character_escapes() {
let doc = doc_of(obj(vec![(
"s",
Value::Str("a\rb\u{08}c\u{0c}d\u{01}".to_string()),
)]));
let text = write_json(&doc, None, false, None).unwrap();
const BS: char = '\u{5c}';
let expected = format!("{{\"s\": \"a{BS}rb{BS}bc{BS}fd{BS}u0001\"}}");
assert_eq!(text, expected);
}
#[test]
fn deeply_nested_document_write_reuses_doc_construction_depth_guard() {
let mut v = Value::Int((0).into());
for _ in 0..=crate::document::MAX_DEPTH {
v = obj(vec![("a", v)]);
}
assert!(Doc::of(&v).is_err());
}
#[test]
fn round_trip_via_live_python_equivalent_scalars() {
let doc = doc_of(obj(vec![
("a", Value::Int((1).into())),
("b", Value::Str("x".to_string())),
(
"c",
Value::Array(vec![
Value::Int((1).into()),
Value::Int((2).into()),
Value::Int((3).into()),
]),
),
]));
let text = write_json(&doc, None, false, None).unwrap();
assert_eq!(text, r#"{"a": 1, "b": "x", "c": [1, 2, 3]}"#);
}
#[test]
fn test_deeply_nested_json_depth_limit() {
let nested = "[".repeat(50_000) + &"]".repeat(50_000);
let err = read_json(&nested).unwrap_err();
assert!(err.to_string().contains("maximum depth"));
}
}