use ktav::error::ErrorKind;
use ktav::thin::{parse_events, ParseEvent};
use ktav::{from_str, parse, Error, ObjectMap, Value};
#[derive(Debug, PartialEq)]
enum Ev {
Null,
Bool(bool),
Integer(String),
Float(String),
Str(String),
Key(String),
BeginObject,
EndObject,
BeginArray,
EndArray,
}
fn collect(src: &str) -> Vec<Ev> {
let mut out = Vec::new();
parse_events(src, |e| {
out.push(match e {
ParseEvent::Null => Ev::Null,
ParseEvent::Bool(b) => Ev::Bool(b),
ParseEvent::Integer(s) => Ev::Integer(s.to_string()),
ParseEvent::Float(s) => Ev::Float(s.to_string()),
ParseEvent::Str(s) => Ev::Str(s.to_string()),
ParseEvent::Key(s) => Ev::Key(s.to_string()),
ParseEvent::BeginObject => Ev::BeginObject,
ParseEvent::EndObject => Ev::EndObject,
ParseEvent::BeginArray => Ev::BeginArray,
ParseEvent::EndArray => Ev::EndArray,
_ => unreachable!(),
});
})
.unwrap();
out
}
fn obj(pairs: &[(&str, Value)]) -> Value {
let mut m = ObjectMap::default();
for (k, v) in pairs {
m.insert((*k).into(), v.clone());
}
Value::Object(m)
}
fn arr(items: &[Value]) -> Value {
Value::Array(items.to_vec())
}
#[test]
fn escaped_brace_consumes_value_start() {
let src = r"{a: \{[text}
";
assert_eq!(
parse(src).unwrap(),
obj(&[("a", Value::String("{[text".into()))]),
"escaped-brace repro failed for {src:?}"
);
assert_eq!(
collect(src),
vec![
Ev::BeginObject,
Ev::Key("a".into()),
Ev::Str("{[text".into()),
Ev::EndObject
]
);
}
#[test]
fn escaped_bracket_consumes_value_start() {
let src = r"{a: \[{text}
";
assert_eq!(
parse(src).unwrap(),
obj(&[("a", Value::String("[{text".into()))]),
"escaped-bracket repro failed for {src:?}"
);
}
#[test]
fn unicode_escape_consumes_value_start() {
let src = r"{a: \u0041[text}
";
assert_eq!(
parse(src).unwrap(),
obj(&[("a", Value::String("A[text".into()))]),
"unicode-escape repro failed for {src:?}"
);
}
#[test]
fn escaped_brace_in_nested_compound() {
let src = r"{x: {a: \{[text}}
";
assert_eq!(
parse(src).unwrap(),
obj(&[("x", obj(&[("a", Value::String("{[text".into()))]))]),
"nested escaped-brace repro failed for {src:?}"
);
assert_eq!(
collect(src),
vec![
Ev::BeginObject,
Ev::Key("x".into()),
Ev::BeginObject,
Ev::Key("a".into()),
Ev::Str("{[text".into()),
Ev::EndObject,
Ev::EndObject
]
);
}
#[test]
fn unicode_escape_in_nested_compound() {
let src = r"{x: {a: \u0041[text}}
";
assert_eq!(
parse(src).unwrap(),
obj(&[("x", obj(&[("a", Value::String("A[text".into()))]))]),
"nested unicode-escape repro failed for {src:?}"
);
}
#[test]
fn escaped_brace_with_quote_in_body() {
let src = r#"{a: \{[text, "k": 2}
"#;
assert_eq!(
parse(src).unwrap(),
obj(&[
("a", Value::String("{[text".into())),
("k", Value::Integer("2".into())),
]),
"slow-path splitter repro failed for {src:?}"
);
}
#[test]
fn escaped_brace_in_nested_compound_with_quote() {
let src = r#"{x: {a: \{[text, "k": 2}}
"#;
assert_eq!(
parse(src).unwrap(),
obj(&[(
"x",
obj(&[
("a", Value::String("{[text".into())),
("k", Value::Integer("2".into())),
])
)]),
"slow-path closer-scan repro failed for {src:?}"
);
}
#[test]
fn genuine_value_start_compounds_unaffected() {
assert_eq!(
parse("{a: {b: 1}}\n").unwrap(),
obj(&[("a", obj(&[("b", Value::Integer("1".into()))]))])
);
assert_eq!(
parse("{a: [1]}\n").unwrap(),
obj(&[("a", arr(&[Value::Integer("1".into())]))])
);
assert_eq!(
parse("[1, {a: 2}]\n").unwrap(),
arr(&[
Value::Integer("1".into()),
obj(&[("a", Value::Integer("2".into()))])
])
);
}
#[test]
fn key_escapes_and_dotted_keys_unaffected() {
let v = parse("a\\.b: 1\n").unwrap();
let Value::Object(map) = v else {
panic!("expected Object root for a\\.b");
};
assert_eq!(map.len(), 1);
assert_eq!(map.keys().next().unwrap().as_str(), "a.b");
let v = parse("a\\:b: 1\n").unwrap();
let Value::Object(map) = v else {
panic!("expected Object root for a\\:b");
};
assert_eq!(map.keys().next().unwrap().as_str(), "a:b");
assert_eq!(
parse("{a.b: 1}\n").unwrap(),
obj(&[("a", obj(&[("b", Value::Integer("1".into()))]))])
);
}
#[test]
fn typed_api() {
#[derive(Debug, PartialEq, serde::Deserialize)]
struct Doc {
a: String,
}
let d: Doc = from_str(
r"{a: \{[text}
",
)
.unwrap();
assert_eq!(d.a, "{[text");
}
#[test]
fn pins_unchanged() {
let err = parse("{a: {[text}\n").unwrap_err();
match &err {
Error::Structured(ErrorKind::UnterminatedInlineCompound { .. }) => {}
other => panic!("expected UnterminatedInlineCompound, got {other:?}"),
}
}