use std::collections::BTreeMap;
use toml::Value;
pub type WinnerMap = BTreeMap<String, String>;
pub fn deep_merge(base: &mut Value, overlay: Value) {
match (base, overlay) {
(Value::Table(base_tbl), Value::Table(overlay_tbl)) => {
for (key, val) in overlay_tbl {
match base_tbl.get_mut(&key) {
Some(existing) => deep_merge(existing, val),
None => {
base_tbl.insert(key, val);
}
}
}
}
(base, overlay) => *base = overlay,
}
}
pub fn deep_merge_traced(
base: &mut Value,
overlay: Value,
layer_id: &str,
prefix: &str,
winner_map: &mut WinnerMap,
) {
match (base, overlay) {
(Value::Table(base_tbl), Value::Table(overlay_tbl)) => {
for (key, val) in overlay_tbl {
let child_path =
if prefix.is_empty() { key.clone() } else { format!("{prefix}.{key}") };
match base_tbl.get_mut(&key) {
Some(existing) => {
deep_merge_traced(existing, val, layer_id, &child_path, winner_map);
}
None => {
record_all_leaves(&val, layer_id, &child_path, winner_map);
base_tbl.insert(key, val);
}
}
}
}
(base, overlay) => {
if !prefix.is_empty() {
winner_map.insert(prefix.to_owned(), layer_id.to_owned());
}
*base = overlay;
}
}
}
fn record_all_leaves(value: &Value, layer_id: &str, prefix: &str, winner_map: &mut WinnerMap) {
match value {
Value::Table(tbl) => {
for (key, val) in tbl {
let child = if prefix.is_empty() { key.clone() } else { format!("{prefix}.{key}") };
record_all_leaves(val, layer_id, &child, winner_map);
}
}
_ => {
if !prefix.is_empty() {
winner_map.insert(prefix.to_owned(), layer_id.to_owned());
}
}
}
}
pub(crate) fn set_dotted(root: &mut Value, path: &str, value: Value) {
let segments: Vec<&str> = path.split('.').filter(|s| !s.is_empty()).collect();
if segments.is_empty() {
return;
}
set_dotted_recursive(root, &segments, value);
}
fn set_dotted_recursive(root: &mut Value, segments: &[&str], value: Value) {
if segments.is_empty() {
return;
}
let head = segments[0];
let tail = &segments[1..];
let tbl = match root {
Value::Table(t) => t,
other => {
*other = Value::Table(toml::map::Map::new());
other.as_table_mut().unwrap()
}
};
if tail.is_empty() {
tbl.insert(head.to_owned(), value);
} else {
let entry =
tbl.entry(head.to_owned()).or_insert_with(|| Value::Table(toml::map::Map::new()));
set_dotted_recursive(entry, tail, value);
}
}
pub(crate) fn env_str_to_value(s: &str) -> Value {
if s.eq_ignore_ascii_case("true") {
return Value::Boolean(true);
}
if s.eq_ignore_ascii_case("false") {
return Value::Boolean(false);
}
if let Ok(n) = s.parse::<i64>() {
return Value::Integer(n);
}
if let Ok(f) = s.parse::<f64>() {
return Value::Float(f);
}
Value::String(s.to_owned())
}
#[cfg(test)]
mod tests {
use toml::Value;
use super::*;
fn parse(s: &str) -> Value {
toml::from_str(s).unwrap()
}
#[test]
fn merges_tables_recursively() {
let mut base = parse("[a]\nx = 1\ny = 2\n");
let overlay = parse("[a]\ny = 99\nz = 3\n");
deep_merge(&mut base, overlay);
let a = base["a"].as_table().unwrap();
assert_eq!(a["x"].as_integer(), Some(1)); assert_eq!(a["y"].as_integer(), Some(99)); assert_eq!(a["z"].as_integer(), Some(3)); }
#[test]
fn overlay_replaces_scalar() {
let mut base = parse("x = 1\n");
let overlay = parse("x = 42\n");
deep_merge(&mut base, overlay);
assert_eq!(base["x"].as_integer(), Some(42));
}
#[test]
fn overlay_replaces_array_entirely() {
let mut base = parse("arr = [1, 2, 3]\n");
let overlay = parse("arr = [4, 5]\n");
deep_merge(&mut base, overlay);
let arr = base["arr"].as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_integer(), Some(4));
}
#[test]
fn base_key_absent_in_overlay_preserved() {
let mut base = parse("keep = \"me\"\nchange = 1\n");
let overlay = parse("change = 2\n");
deep_merge(&mut base, overlay);
assert_eq!(base["keep"].as_str(), Some("me"));
assert_eq!(base["change"].as_integer(), Some(2));
}
#[test]
fn set_dotted_creates_nested() {
let mut root = Value::Table(toml::map::Map::new());
set_dotted(&mut root, "a.b.c", Value::Integer(7));
assert_eq!(root["a"]["b"]["c"].as_integer(), Some(7));
}
#[test]
fn set_dotted_handles_anomalous_dots() {
let mut root = Value::Table(toml::map::Map::new());
set_dotted(&mut root, ".a..b.c.", Value::Integer(42));
assert_eq!(root["a"]["b"]["c"].as_integer(), Some(42));
}
#[test]
fn env_str_parses_bool() {
assert_eq!(env_str_to_value("true"), Value::Boolean(true));
assert_eq!(env_str_to_value("false"), Value::Boolean(false));
assert_eq!(env_str_to_value("True"), Value::Boolean(true));
}
#[test]
fn env_str_parses_integer() {
assert_eq!(env_str_to_value("42"), Value::Integer(42));
assert_eq!(env_str_to_value("-1"), Value::Integer(-1));
}
#[test]
fn env_str_parses_float() {
assert!(matches!(env_str_to_value("3.14"), Value::Float(_)));
}
#[test]
fn env_str_falls_back_to_string() {
assert_eq!(env_str_to_value("hello"), Value::String("hello".to_owned()));
}
#[test]
fn traced_merge_records_winning_layers() {
let mut base = parse("x = 1\n");
let mut wm = WinnerMap::new();
deep_merge_traced(&mut base, parse("x = 99\n"), "layer-1", "", &mut wm);
assert_eq!(wm.get("x").map(String::as_str), Some("layer-1"));
assert_eq!(base["x"].as_integer(), Some(99));
}
#[test]
fn traced_merge_table_recursive() {
let mut base = parse("[a]\nx = 1\ny = 2\n");
let overlay = parse("[a]\ny = 99\nz = 3\n");
let mut wm = WinnerMap::new();
deep_merge_traced(&mut base, overlay, "layer-1", "", &mut wm);
let a = base["a"].as_table().unwrap();
assert_eq!(a["x"].as_integer(), Some(1));
assert_eq!(a["y"].as_integer(), Some(99));
assert_eq!(wm.get("a.y").map(String::as_str), Some("layer-1"));
assert_eq!(a["z"].as_integer(), Some(3));
assert_eq!(wm.get("a.z").map(String::as_str), Some("layer-1"));
}
#[test]
fn traced_merge_array_replaces_not_merges() {
let mut base = parse("arr = [1, 2, 3]\n");
let overlay = parse("arr = [4, 5]\n");
let mut wm = WinnerMap::new();
deep_merge_traced(&mut base, overlay, "layer-1", "", &mut wm);
let arr = base["arr"].as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_integer(), Some(4));
assert_eq!(wm.get("arr").map(String::as_str), Some("layer-1"));
}
#[test]
fn traced_merge_scalar_replacement() {
let mut base = parse("n = 1\n");
let mut wm = WinnerMap::new();
deep_merge_traced(&mut base, parse("n = 42\n"), "env", "", &mut wm);
assert_eq!(base["n"].as_integer(), Some(42));
assert_eq!(wm.get("n").map(String::as_str), Some("env"));
}
#[test]
fn traced_merge_layer_order_defaults_files_env() {
let mut merged = parse("port = 1\nname = \"default\"\n");
let mut wm = WinnerMap::new();
deep_merge_traced(&mut merged, parse("port = 2\n"), "layer-file", "", &mut wm);
deep_merge_traced(&mut merged, parse("port = 3\n"), "env", "", &mut wm);
assert_eq!(merged["port"].as_integer(), Some(3));
assert_eq!(wm.get("port").map(String::as_str), Some("env"));
assert_eq!(merged["name"].as_str(), Some("default"));
}
#[test]
fn traced_merge_every_field_has_winning_layer() {
let mut merged = Value::Table(toml::map::Map::new());
let mut wm = WinnerMap::new();
deep_merge_traced(&mut merged, parse("a = 1\n[s]\nb = 2\n"), "layer-0", "", &mut wm);
deep_merge_traced(&mut merged, parse("[s]\nc = 3\n"), "layer-1", "", &mut wm);
fn leaves(val: &Value, prefix: &str) -> Vec<String> {
match val {
Value::Table(t) => t
.iter()
.flat_map(|(k, v)| {
let p = if prefix.is_empty() { k.clone() } else { format!("{prefix}.{k}") };
leaves(v, &p)
})
.collect(),
_ => vec![prefix.to_owned()],
}
}
let leaf_paths = leaves(&merged, "");
for path in &leaf_paths {
assert!(wm.contains_key(path), "field '{path}' has no winning layer");
}
}
}