use toml::Value;
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(crate) fn set_dotted(root: &mut Value, path: &str, value: Value) {
let mut parts = path.splitn(2, '.');
let head = match parts.next() {
Some(h) if !h.is_empty() => h,
_ => return,
};
let tail = parts.next();
let tbl = match root {
Value::Table(t) => t,
other => {
*other = Value::Table(toml::map::Map::new());
match other {
Value::Table(t) => t,
_ => unreachable!(),
}
}
};
match tail {
None => {
tbl.insert(head.to_owned(), value);
}
Some(rest) => {
let entry = tbl
.entry(head.to_owned())
.or_insert_with(|| Value::Table(toml::map::Map::new()));
set_dotted(entry, rest, 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)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use toml::Value;
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 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()));
}
}