use std::collections::BTreeMap;
use ktav::render::render;
use ktav::{emit_canonical, from_str, parse, to_string, to_string_force_strings, ObjectMap, Value};
use serde::{Deserialize, Serialize};
fn s(v: &str) -> Value {
Value::String(v.parse().unwrap_or_else(|_| panic!("scalar: {v:?}")))
}
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 assert_canonical_roundtrip(v: &Value) {
let text = emit_canonical(v).unwrap_or_else(|e| panic!("emit_canonical({v:?}): {e}"));
let back = parse(&text).unwrap_or_else(|e| panic!("parse({text:?}): {e}"));
assert_eq!(&back, v, "canonical text was {text:?}");
}
fn assert_force_strings_roundtrip(v: &Value) {
let text =
to_string_force_strings(v).unwrap_or_else(|e| panic!("to_string_force_strings: {e}"));
let back = parse(&text).unwrap_or_else(|e| panic!("parse({text:?}): {e}"));
assert_eq!(&back, v, "rendered text was {text:?}");
}
#[test]
fn canonical_keeps_leading_whitespace_in_string() {
assert_canonical_roundtrip(&obj(&[("k", s(" padded"))]));
}
#[test]
fn canonical_keeps_trailing_whitespace_in_string() {
assert_canonical_roundtrip(&obj(&[("password", s("hunter2 "))]));
}
#[test]
fn canonical_keeps_edge_whitespace_on_both_sides() {
assert_canonical_roundtrip(&obj(&[("k", s(" padded "))]));
}
#[test]
fn canonical_keeps_whitespace_only_string() {
assert_canonical_roundtrip(&obj(&[("k", s(" "))]));
}
#[test]
fn canonical_keeps_tab_padded_string() {
assert_canonical_roundtrip(&obj(&[("k", s("\tpadded\t"))]));
}
#[test]
fn canonical_keeps_padded_array_items() {
assert_canonical_roundtrip(&obj(&[(
"arr",
Value::Array(vec![s(" pad"), s("pad "), s(", ")]),
)]));
}
#[test]
fn canonical_keeps_control_byte_string() {
assert_canonical_roundtrip(&obj(&[("k", s("a\x01b"))]));
}
#[test]
fn canonical_paren_prefixed_string_reparses() {
assert_canonical_roundtrip(&obj(&[("mode", s("(disabled)"))]));
assert_canonical_roundtrip(&obj(&[("k", s("(x"))]));
}
#[test]
fn canonical_paren_prefixed_array_item_reparses() {
assert_canonical_roundtrip(&obj(&[("arr", Value::Array(vec![s("(x"), s("(none)")]))]));
}
#[test]
fn canonical_rejects_padded_sole_double_paren_line() {
assert!(emit_canonical(&obj(&[("k", s(" )) "))])).is_err());
}
#[test]
fn canonical_keeps_representable_tricky_keys() {
for key in [
"a.b",
"a:b",
"a\\b",
"#note",
"x##y",
"first name",
"a\tb",
"путь",
] {
assert_canonical_roundtrip(&obj(&[(key, s("x"))]));
}
}
#[test]
fn canonical_keeps_keys_with_the_seven_newly_escapable_bytes() {
for key in ["a,b", "a{b", "a}b", "a[b", "a]b", "a\nb", "a\rb"] {
assert_canonical_roundtrip(&obj(&[(key, s("x"))]));
}
}
#[test]
fn serde_keeps_trailing_whitespace_string() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Cfg {
password: String,
}
let cfg = Cfg {
password: "hunter2 ".into(),
};
let back: Cfg = from_str(&to_string(&cfg).unwrap()).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn serde_keeps_leading_whitespace_string() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Cfg {
sep: String,
}
let cfg = Cfg { sep: " x".into() };
let back: Cfg = from_str(&to_string(&cfg).unwrap()).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn serde_keeps_whitespace_only_string() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Cfg {
pad: String,
}
let cfg = Cfg { pad: " ".into() };
let back: Cfg = from_str(&to_string(&cfg).unwrap()).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn serde_keeps_padded_strings_in_seq() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Cfg {
parts: Vec<String>,
}
let cfg = Cfg {
parts: vec![" pad".into(), "pad ".into(), ", ".into()],
};
let back: Cfg = from_str(&to_string(&cfg).unwrap()).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn serde_keeps_padded_string_in_nested_struct() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Inner {
v: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Cfg {
items: Vec<Inner>,
}
let cfg = Cfg {
items: vec![Inner { v: "pad ".into() }, Inner { v: " pad".into() }],
};
let back: Cfg = from_str(&to_string(&cfg).unwrap()).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn serde_rejects_cr_string() {
#[derive(Debug, Serialize)]
struct Cfg {
v: String,
}
let cfg = Cfg { v: "a\rb".into() };
assert!(to_string(&cfg).is_err());
}
#[test]
fn serde_escapes_dotted_map_keys() {
let mut m = BTreeMap::new();
m.insert("a.b".to_string(), 1_i32);
let back: BTreeMap<String, i32> = from_str(&to_string(&m).unwrap()).unwrap();
assert_eq!(m, back);
}
#[test]
fn serde_escapes_map_keys_with_the_seven_newly_escapable_bytes() {
let mut m = BTreeMap::new();
for (i, key) in ["a,b", "a{b", "a}b", "a[b", "a]b", "a\nb", "a\rb"]
.into_iter()
.enumerate()
{
m.insert(key.to_string(), i as i32);
}
let back: BTreeMap<String, i32> = from_str(&to_string(&m).unwrap()).unwrap();
assert_eq!(m, back);
}
#[test]
fn force_strings_keeps_padded_strings() {
assert_force_strings_roundtrip(&obj(&[("k", s(" padded "))]));
assert_force_strings_roundtrip(&obj(&[("k", s(" "))]));
assert_force_strings_roundtrip(&obj(&[("arr", Value::Array(vec![s(" pad"), s("pad ")]))]));
}
#[test]
fn force_strings_rejects_cr_string() {
assert!(to_string_force_strings(&obj(&[("k", s("a\rb"))])).is_err());
}
#[test]
fn force_strings_keeps_keys_with_the_seven_newly_escapable_bytes() {
for key in ["a,b", "a{b", "a}b", "a[b", "a]b", "a\nb", "a\rb"] {
assert_force_strings_roundtrip(&obj(&[(key, s("x"))]));
}
}
#[test]
fn canonical_keeps_unicode_whitespace_edges() {
for v in ["\u{A0}val\u{A0}", "\u{85}val", "val\u{3000}", "\u{2028}a"] {
assert_canonical_roundtrip(&obj(&[("k", s(v))]));
assert_canonical_roundtrip(&obj(&[("arr", Value::Array(vec![s(v)]))]));
}
}
#[test]
fn serde_keeps_unicode_whitespace_edges() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Cfg {
v: String,
parts: Vec<String>,
}
let cfg = Cfg {
v: "\u{A0}val\u{A0}".into(),
parts: vec!["\u{85}val".into(), "val\u{3000}".into()],
};
let back: Cfg = from_str(&to_string(&cfg).unwrap()).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn force_strings_keeps_unicode_whitespace_edges() {
assert_force_strings_roundtrip(&obj(&[("k", s("\u{A0}val\u{A0}"))]));
}
#[test]
fn canonical_keeps_comment_marker_array_item() {
assert_canonical_roundtrip(&obj(&[("arr", Value::Array(vec![s("## x"), s("##")]))]));
}
#[test]
fn serde_keeps_comment_marker_array_item() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Cfg {
notes: Vec<String>,
}
let cfg = Cfg {
notes: vec!["## x".into()],
};
let back: Cfg = from_str(&to_string(&cfg).unwrap()).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn force_strings_keeps_comment_marker_array_item() {
assert_force_strings_roundtrip(&obj(&[("arr", Value::Array(vec![s("## x")]))]));
}
#[test]
fn canonical_keeps_marker_shaped_array_items() {
assert_canonical_roundtrip(&obj(&[(
"arr",
Value::Array(vec![s(":: x"), s("::x"), s("]"), s("}")]),
)]));
}
#[test]
fn serde_keeps_marker_shaped_array_items() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Cfg {
items: Vec<String>,
}
let cfg = Cfg {
items: vec![":: x".into(), "]".into(), "}".into()],
};
let back: Cfg = from_str(&to_string(&cfg).unwrap()).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn canonical_rejects_both_terminator_lines() {
assert!(emit_canonical(&obj(&[("k", s("a\n)\n))"))])).is_err());
assert!(emit_canonical(&obj(&[("arr", Value::Array(vec![s("a\n)\n))")]))])).is_err());
}
#[test]
fn canonical_keeps_partially_indented_body_with_double_paren_line() {
assert_canonical_roundtrip(&obj(&[("k", s("a\n b\n))"))]));
}
#[test]
fn serde_keeps_double_paren_body_with_indented_line() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Cfg {
v: String,
}
let cfg = Cfg {
v: "))\n x\ny".into(),
};
let back: Cfg = from_str(&to_string(&cfg).unwrap()).unwrap();
assert_eq!(cfg, back);
}
#[test]
fn force_strings_keeps_double_paren_body_with_indented_line() {
assert_force_strings_roundtrip(&obj(&[("k", s("))\n x\ny"))]));
}
fn assert_render_roundtrip(v: &Value) {
let text = ktav::render::render(v).unwrap_or_else(|e| panic!("render({v:?}): {e}"));
let back = parse(&text).unwrap_or_else(|e| panic!("parse({text:?}): {e}"));
assert_eq!(&back, v, "rendered text was {text:?}");
}
#[test]
fn render_wraps_single_object_item() {
assert_render_roundtrip(&Value::Array(vec![obj(&[("k", s("v"))])]));
}
#[test]
fn render_wraps_multiple_object_items() {
assert_render_roundtrip(&Value::Array(vec![
obj(&[("k", s("v"))]),
obj(&[("k2", s("v2"))]),
]));
}
#[test]
fn render_wraps_when_object_item_is_followed_by_a_scalar() {
assert_render_roundtrip(&Value::Array(vec![obj(&[("k", s("v"))]), s("tail")]));
}
#[test]
fn render_does_not_wrap_when_first_item_is_a_scalar() {
let v = Value::Array(vec![s("head"), obj(&[("k", s("v"))])]);
let text = render(&v).unwrap();
assert!(
!text.starts_with('['),
"did not need wrapping, but got {text:?}"
);
assert_render_roundtrip(&v);
}
#[test]
fn render_wraps_nested_array_item() {
assert_render_roundtrip(&Value::Array(vec![Value::Array(vec![s("x")])]));
}
#[test]
fn render_wraps_single_empty_object_item() {
assert_render_roundtrip(&Value::Array(vec![Value::Object(ObjectMap::default())]));
}
#[test]
fn render_wraps_single_empty_array_item() {
assert_render_roundtrip(&Value::Array(vec![Value::Array(vec![])]));
}
#[test]
fn render_wraps_empty_compound_item_followed_by_more_items() {
assert_render_roundtrip(&Value::Array(vec![
Value::Object(ObjectMap::default()),
s("tail"),
]));
}
#[test]
fn render_emits_bracket_form_for_a_truly_empty_array_root() {
let v = Value::Array(vec![]);
let text = render(&v).unwrap();
assert_eq!(text, "[]\n");
assert_render_roundtrip(&v);
}
#[test]
fn canonical_wraps_single_empty_object_item() {
let v = Value::Array(vec![Value::Object(ObjectMap::default())]);
assert_eq!(emit_canonical(&v).unwrap(), "[\n {}\n]\n");
assert_canonical_roundtrip(&v);
}
#[test]
fn canonical_wraps_single_empty_array_item() {
let v = Value::Array(vec![Value::Array(vec![])]);
assert_eq!(emit_canonical(&v).unwrap(), "[\n []\n]\n");
assert_canonical_roundtrip(&v);
}
#[test]
fn canonical_wraps_empty_compound_item_followed_by_more_items() {
assert_canonical_roundtrip(&Value::Array(vec![
Value::Object(ObjectMap::default()),
s("tail"),
]));
}
#[test]
fn force_strings_wraps_single_object_item() {
assert_force_strings_roundtrip(&Value::Array(vec![obj(&[("k", s("v"))])]));
}