use ktav::error::ErrorKind;
use ktav::thin::{parse_events, ParseEvent};
use ktav::{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 root_key(src: &str) -> String {
let Value::Object(map) = parse(src).unwrap() else {
panic!("{src:?}: expected Object root");
};
assert_eq!(map.len(), 1, "{src:?}: expected exactly one root key");
map.keys().next().unwrap().to_string()
}
#[test]
fn owned_decode_forces_string_not_bool() {
let v = parse(
r"{a: \u0074rue}
",
)
.unwrap();
assert_eq!(v, obj(&[("a", Value::String("true".into()))]));
let Value::Object(map) = v else {
panic!("expected Object")
};
let a = map.get("a").unwrap();
assert!(a.as_bool().is_none(), "decoded `true` must NOT be Bool");
}
#[test]
fn owned_decode_forces_string_not_null() {
let v = parse(
r"{a: \u006eull}
",
)
.unwrap();
assert_eq!(v, obj(&[("a", Value::String("null".into()))]));
let Value::Object(map) = v else {
panic!("expected Object")
};
let a = map.get("a").unwrap();
assert!(!a.is_null(), "decoded `null` must NOT be Null");
}
#[test]
fn plain_keywords_still_classify() {
assert_eq!(
parse("{a: true}\n").unwrap(),
obj(&[("a", Value::Bool(true))])
);
assert_eq!(parse("{a: null}\n").unwrap(), obj(&[("a", Value::Null)]));
}
#[test]
fn thin_owned_decode_yields_str_not_bool() {
assert_eq!(
collect("{a: \\u0074rue}\n"),
vec![
Ev::BeginObject,
Ev::Key("a".into()),
Ev::Str("true".into()),
Ev::EndObject
]
);
}
#[test]
fn quoted_interior_whitespace_preserved_both_parsers() {
assert_eq!(root_key("\" a b \": 1\n"), " a b ");
assert_eq!(
collect("\" a b \": 1\n"),
vec![
Ev::BeginObject,
Ev::Key(" a b ".into()),
Ev::Integer("1".into()),
Ev::EndObject
]
);
}
#[test]
fn quoted_escaped_dot_is_literal_not_separator() {
assert_eq!(
parse(
r"{q\.x: 1}
"
)
.unwrap(),
obj(&[("q.x", Value::Integer("1".into()))])
);
assert_eq!(
root_key(
r"q\.x: 1
"
),
"q.x"
);
}
#[test]
fn thin_quoted_escaped_dot_is_literal_not_separator() {
assert_eq!(
collect(
r"{q\.x: 1}
"
),
vec![
Ev::BeginObject,
Ev::Key("q.x".into()),
Ev::Integer("1".into()),
Ev::EndObject
]
);
}
#[test]
fn quoted_escaped_colon_decodes_in_interior() {
assert_eq!(
root_key(
r#""a\:b": 1
"#
),
"a:b"
);
assert_eq!(
collect(
r#""a\:b": 1
"#
),
vec![
Ev::BeginObject,
Ev::Key("a:b".into()),
Ev::Integer("1".into()),
Ev::EndObject
]
);
}
#[test]
fn bad_escape_sequence_error_unchanged() {
let err = parse(
r"{a: \x}
",
)
.unwrap_err();
match &err {
Error::Structured(ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got {other:?}"),
}
let err = parse_events(
r"{a: \x}
",
|_| {},
)
.unwrap_err();
match err {
Error::Structured(ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got {other:?}"),
}
}
#[test]
fn decoded_numeric_looks_like_integer_but_is_string() {
let v = parse(
r"{a: \u0031}
",
)
.unwrap();
assert_eq!(v, obj(&[("a", Value::String("1".into()))]));
let Value::Object(map) = v else {
panic!("expected Object")
};
assert!(matches!(map.get("a"), Some(Value::String(_))));
assert_eq!(
collect(
r"{a: \u0031}
"
),
vec![
Ev::BeginObject,
Ev::Key("a".into()),
Ev::Str("1".into()),
Ev::EndObject
]
);
}