use toml::Value;
pub fn deep_merge(base: Value, patch: Value) -> Value {
match (base, patch) {
(Value::Table(mut base_tbl), Value::Table(patch_tbl)) => {
for (key, patch_val) in patch_tbl {
match base_tbl.remove(&key) {
Some(Value::Table(base_sub)) if patch_val.is_table() => {
let merged = deep_merge(Value::Table(base_sub), patch_val);
base_tbl.insert(key, merged);
}
_ => {
base_tbl.insert(key, patch_val);
}
}
}
Value::Table(base_tbl)
}
(_, patch) => patch,
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn toml_from_str(s: &str) -> Value {
toml::from_str(s).unwrap()
}
#[test]
fn test_merge_disjoint_tables() {
let base = toml_from_str("a = 1");
let patch = toml_from_str("b = 2");
let result = deep_merge(base, patch);
assert_eq!(result.get("a").unwrap().as_integer(), Some(1));
assert_eq!(result.get("b").unwrap().as_integer(), Some(2));
}
#[test]
fn test_merge_overlapping_tables() {
let base = toml_from_str("a = 1\nb = 2");
let patch = toml_from_str("b = 3\nc = 4");
let result = deep_merge(base, patch);
assert_eq!(result.get("a").unwrap().as_integer(), Some(1));
assert_eq!(result.get("b").unwrap().as_integer(), Some(3));
assert_eq!(result.get("c").unwrap().as_integer(), Some(4));
}
#[test]
fn test_merge_nested_tables() {
let base = toml_from_str(
r"
[section]
x = 1
y = 2
",
);
let patch = toml_from_str(
r"
[section]
y = 3
z = 4
",
);
let result = deep_merge(base, patch);
let section = result.get("section").unwrap().as_table().unwrap();
assert_eq!(section.get("x").unwrap().as_integer(), Some(1));
assert_eq!(section.get("y").unwrap().as_integer(), Some(3));
assert_eq!(section.get("z").unwrap().as_integer(), Some(4));
}
#[test]
fn test_arrays_replace() {
let base = toml_from_str("a = [1, 2, 3]");
let patch = toml_from_str("a = [4, 5]");
let result = deep_merge(base, patch);
let arr = result.get("a").unwrap().as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_integer(), Some(4));
assert_eq!(arr[1].as_integer(), Some(5));
}
#[test]
fn test_scalar_replaces_table() {
let base = toml_from_str(
r"
[section]
nested = true
",
);
let patch = toml_from_str("section = 42");
let result = deep_merge(base, patch);
assert_eq!(result.get("section").unwrap().as_integer(), Some(42));
}
#[test]
fn test_table_replaces_scalar() {
let base = toml_from_str("section = 42");
let patch = toml_from_str(
r"
[section]
nested = true
",
);
let result = deep_merge(base, patch);
let section = result.get("section").unwrap().as_table().unwrap();
assert_eq!(section.get("nested").unwrap().as_bool(), Some(true));
}
#[test]
fn test_empty_patch_is_identity() {
let base = toml_from_str(
r"
a = 1
[section]
b = 2
",
);
let patch = toml_from_str("");
let result = deep_merge(base.clone(), patch);
assert_eq!(result, base);
}
#[test]
fn test_empty_base_receives_patch() {
let base = toml_from_str("");
let patch = toml_from_str("a = 1");
let result = deep_merge(base, patch);
assert_eq!(result.get("a").unwrap().as_integer(), Some(1));
}
#[test]
fn test_deeply_nested_merge() {
let base = toml_from_str(
r"
[level1.level2.level3]
a = 1
b = 2
",
);
let patch = toml_from_str(
r"
[level1.level2.level3]
b = 99
c = 3
",
);
let result = deep_merge(base, patch);
let level3 = result
.get("level1")
.unwrap()
.get("level2")
.unwrap()
.get("level3")
.unwrap()
.as_table()
.unwrap();
assert_eq!(level3.get("a").unwrap().as_integer(), Some(1));
assert_eq!(level3.get("b").unwrap().as_integer(), Some(99));
assert_eq!(level3.get("c").unwrap().as_integer(), Some(3));
}
proptest! {
#[test]
fn prop_empty_patch_is_identity(base in arb_toml_table()) {
let empty = Value::Table(Default::default());
let result = deep_merge(base.clone(), empty);
prop_assert_eq!(result, base);
}
#[test]
fn prop_idempotent_merge(base in arb_toml_table(), patch in arb_toml_table()) {
let once = deep_merge(base, patch.clone());
let twice = deep_merge(once.clone(), patch);
prop_assert_eq!(once, twice);
}
}
fn arb_toml_table() -> impl Strategy<Value = Value> {
prop::collection::hash_map("[a-z]{1,3}", arb_toml_value(), 0..5)
.prop_map(|m| Value::Table(m.into_iter().collect()))
}
fn arb_toml_value() -> impl Strategy<Value = Value> {
prop_oneof![
any::<bool>().prop_map(Value::Boolean),
any::<i64>().prop_map(Value::Integer),
"[a-z]{0,10}".prop_map(Value::String),
prop::collection::hash_map("[a-z]{1,2}", arb_leaf_value(), 0..3)
.prop_map(|m| Value::Table(m.into_iter().collect())),
]
}
fn arb_leaf_value() -> impl Strategy<Value = Value> {
prop_oneof![
any::<bool>().prop_map(Value::Boolean),
any::<i64>().prop_map(Value::Integer),
"[a-z]{0,10}".prop_map(Value::String),
]
}
}