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 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()));
}
}