use knf::{MergeError, MergeOptions, Value, merge};
use Expect::{Doc, Error};
struct Case {
name: &'static str,
layers: &'static [&'static str],
strict: bool,
shallow: bool,
expect: Expect,
}
enum Expect {
Doc(&'static str),
Error(&'static str),
}
const fn ok(name: &'static str, layers: &'static [&'static str], doc: &'static str) -> Case {
Case {
name,
layers,
strict: false,
shallow: false,
expect: Doc(doc),
}
}
const fn strict(name: &'static str, layers: &'static [&'static str], doc: &'static str) -> Case {
Case {
name,
layers,
strict: true,
shallow: false,
expect: Doc(doc),
}
}
const fn conflict(name: &'static str, layers: &'static [&'static str], path: &'static str) -> Case {
Case {
name,
layers,
strict: true,
shallow: false,
expect: Error(path),
}
}
const fn shallow(name: &'static str, layers: &'static [&'static str], expect: Expect) -> Case {
Case {
name,
layers,
strict: false,
shallow: true,
expect,
}
}
const fn strict_shallow(
name: &'static str,
layers: &'static [&'static str],
expect: Expect,
) -> Case {
Case {
name,
layers,
strict: true,
shallow: true,
expect,
}
}
fn options(case: &Case) -> MergeOptions {
MergeOptions {
strict: case.strict,
shallow: case.shallow,
}
}
#[rustfmt::skip]
const CASES: &[Case] = &[
ok("no layers is an empty object", &[], "{}"),
ok("one layer is a no-op", &[r#"{"a":1,"b":{"c":[1,2]}}"#], r#"{"a":1,"b":{"c":[1,2]}}"#),
ok("object + object recurses per key", &[r#"{"a":{"x":1}}"#, r#"{"a":{"y":2}}"#], r#"{"a":{"x":1,"y":2}}"#),
ok("disjoint keys union", &[r#"{"a":1}"#, r#"{"b":2}"#], r#"{"a":1,"b":2}"#),
ok("scalar + scalar is last-wins", &[r#"{"a":1}"#, r#"{"a":2}"#], r#"{"a":2}"#),
ok("scalar shadows an object", &[r#"{"a":{"b":1}}"#, r#"{"a":5}"#], r#"{"a":5}"#),
ok("object shadows a scalar", &[r#"{"a":5}"#, r#"{"a":{"b":1}}"#], r#"{"a":{"b":1}}"#),
ok("array replaces, never index-merges", &[r#"{"a":["x","y","z"]}"#, r#"{"a":["a"]}"#], r#"{"a":["a"]}"#),
ok("array replaces with the empty array", &[r#"{"a":[1,2]}"#, r#"{"a":[]}"#], r#"{"a":[]}"#),
ok("array is not merged element-wise", &[r#"{"a":[{"x":1}]}"#, r#"{"a":[{"y":2}]}"#], r#"{"a":[{"y":2}]}"#),
ok("array replaces a scalar", &[r#"{"a":1}"#, r#"{"a":[1]}"#], r#"{"a":[1]}"#),
ok("null overwrites a scalar", &[r#"{"a":1}"#, r#"{"a":null}"#], r#"{"a":null}"#),
ok("null overwrites an object", &[r#"{"a":{"b":1}}"#, r#"{"a":null}"#], r#"{"a":null}"#),
ok("a value overwrites null", &[r#"{"a":null}"#, r#"{"a":1}"#], r#"{"a":1}"#),
ok("null survives a single layer", &[r#"{"a":null}"#], r#"{"a":null}"#),
ok("left fold, not right", &[r#"{"a":{"b":1}}"#, r#"{"a":5}"#, r#"{"a":{"c":2}}"#], r#"{"a":{"c":2}}"#),
ok("three-layer deep merge", &[r#"{"a":{"b":1}}"#, r#"{"a":{"c":2}}"#, r#"{"a":{"b":9}}"#], r#"{"a":{"b":9,"c":2}}"#),
ok("deep recursion", &[r#"{"a":{"b":{"c":{"d":1}}}}"#, r#"{"a":{"b":{"c":{"e":2}}}}"#], r#"{"a":{"b":{"c":{"d":1,"e":2}}}}"#),
ok("deep insert into a missing branch", &[r#"{"a":{"b":1}}"#, r#"{"x":{"y":{"z":2}}}"#], r#"{"a":{"b":1},"x":{"y":{"z":2}}}"#),
strict("strict allows new keys", &[r#"{"a":1}"#, r#"{"b":2}"#], r#"{"a":1,"b":2}"#),
strict("strict allows same-kind replacement", &[r#"{"a":1}"#, r#"{"a":2}"#], r#"{"a":2}"#),
strict("strict treats int and float as one kind", &[r#"{"a":1}"#, r#"{"a":1.5}"#], r#"{"a":1.5}"#),
strict("strict allows array replacement", &[r#"{"a":[1]}"#, r#"{"a":["x","y"]}"#], r#"{"a":["x","y"]}"#),
strict("strict allows null over null", &[r#"{"a":null}"#, r#"{"a":null}"#], r#"{"a":null}"#),
strict("strict recurses without conflict", &[r#"{"a":{"b":1}}"#, r#"{"a":{"b":2,"c":3}}"#], r#"{"a":{"b":2,"c":3}}"#),
conflict("scalar shadowing an object", &[r#"{"a":{"b":1}}"#, r#"{"a":5}"#], "a"),
conflict("object shadowing a scalar", &[r#"{"a":5}"#, r#"{"a":{"b":1}}"#], "a"),
conflict("array shadowing a scalar", &[r#"{"a":1}"#, r#"{"a":[1]}"#], "a"),
conflict("null shadowing a value", &[r#"{"a":1}"#, r#"{"a":null}"#], "a"),
conflict("value shadowing null", &[r#"{"a":null}"#, r#"{"a":1}"#], "a"),
conflict("string shadowing a number", &[r#"{"a":1}"#, r#"{"a":"1"}"#], "a"),
conflict("bool shadowing a number", &[r#"{"a":1}"#, r#"{"a":true}"#], "a"),
conflict("conflict reports a nested path", &[r#"{"a":{"b":{"c":1}}}"#, r#"{"a":{"b":{"c":[]}}}"#], "a.b.c"),
conflict("conflict from the third layer", &[r#"{"a":1}"#, r#"{"a":2}"#, r#"{"a":"three"}"#], "a"),
shallow("shallow takes a nested object whole", &[r#"{"a":{"x":1,"y":2}}"#, r#"{"a":{"y":9}}"#], Doc(r#"{"a":{"y":9}}"#)),
shallow("shallow keeps untouched top-level keys", &[r#"{"a":{"x":1},"b":1}"#, r#"{"a":{"y":2}}"#], Doc(r#"{"a":{"y":2},"b":1}"#)),
shallow("shallow still inserts new keys", &[r#"{"a":1}"#, r#"{"b":{"c":2}}"#], Doc(r#"{"a":1,"b":{"c":2}}"#)),
shallow("shallow replaces arrays too", &[r#"{"a":[1,2]}"#, r#"{"a":[3]}"#], Doc(r#"{"a":[3]}"#)),
shallow("shallow one layer is a no-op", &[r#"{"a":{"b":[1]}}"#], Doc(r#"{"a":{"b":[1]}}"#)),
shallow("shallow across three layers", &[r#"{"a":{"x":1}}"#, r#"{"a":5}"#, r#"{"a":{"y":2}}"#], Doc(r#"{"a":{"y":2}}"#)),
strict_shallow("strict kind-checks a shallow replace", &[r#"{"a":{"x":1}}"#, r#"{"a":5}"#], Error("a")),
strict_shallow("strict allows a same-kind shallow replace", &[r#"{"a":{"x":1}}"#, r#"{"a":{"y":"s"}}"#], Doc(r#"{"a":{"y":"s"}}"#)),
strict_shallow("strict shallow never looks below the top level", &[r#"{"a":{"x":1}}"#, r#"{"a":{"x":"s"}}"#], Doc(r#"{"a":{"x":"s"}}"#)),
];
#[test]
fn table() {
for case in CASES {
let layers = case.layers.iter().map(|s| ir(s));
let got = merge(layers, &options(case));
match (&case.expect, got) {
(Doc(want), Ok(got)) => {
assert_eq!(got, ir(want), "case `{}`", case.name);
}
(Doc(want), Err(e)) => {
panic!("case `{}`: expected {want}, got error: {e}", case.name);
}
(Error(want), Err(e)) => {
assert_eq!(
e.path().join("."),
*want,
"case `{}`: wrong path",
case.name
);
}
(Error(want), Ok(got)) => {
panic!(
"case `{}`: expected a type conflict at `{want}`, merged to {got:?}",
case.name
);
}
}
}
}
#[test]
fn merge_into_matches_merge() {
for case in CASES {
let Doc(want) = case.expect else {
continue;
};
let opts = options(case);
let mut acc = Value::Object(Default::default());
for layer in case.layers {
knf::merge_into(&mut acc, ir(layer), &opts).expect(case.name);
}
assert_eq!(acc, ir(want), "case `{}`", case.name);
}
}
#[test]
fn root_level_conflict_has_empty_path() {
let mut base = Value::Object(Default::default());
let err = knf::merge_into(&mut base, Value::Bool(true), &MergeOptions::STRICT).unwrap_err();
assert_eq!(err.path(), &[] as &[String]);
assert!(err.to_string().contains("<root>"), "{err}");
}
#[test]
fn conflict_message_names_both_kinds() {
let err = merge(
[ir(r#"{"a":{"b":1}}"#), ir(r#"{"a":5}"#)],
&MergeOptions::STRICT,
)
.unwrap_err();
assert_eq!(
err.to_string(),
"type conflict at `a`: object would be replaced by number"
);
}
#[test]
fn datetime_conflicts_with_string_under_strict() {
let mut base = Value::Object(
[(
"a".to_string(),
Value::Datetime("1979-05-27T07:32:00Z".to_string()),
)]
.into_iter()
.collect(),
);
let err = knf::merge_into(
&mut base,
ir(r#"{"a":"1979-05-27T07:32:00Z"}"#),
&MergeOptions::STRICT,
)
.unwrap_err();
let MergeError::TypeConflict {
path,
expected,
found,
} = err;
assert_eq!(path, ["a"]);
assert_eq!(expected, "datetime");
assert_eq!(found, "string");
}
fn ir(s: &str) -> Value {
let json = serde_json::from_str(s).unwrap_or_else(|e| panic!("bad JSON literal `{s}`: {e}"));
knf::value::from_json(json)
}