use std::collections::HashSet;
use confique::meta::{FieldKind, Meta};
use toml::{Table, Value};
pub fn overrides_to_table(entries: &[(String, Value)]) -> Table {
let mut table = Table::new();
for (dotted_key, value) in entries {
set_nested(&mut table, dotted_key, value.clone());
}
table
}
fn set_nested(table: &mut Table, dotted_key: &str, value: Value) {
let segments: Vec<&str> = dotted_key.split('.').collect();
let mut current = table;
for segment in &segments[..segments.len() - 1] {
current = current
.entry(*segment)
.or_insert_with(|| Value::Table(Table::new()))
.as_table_mut()
.expect("clapfig: override path conflict — intermediate key is not a table");
}
let leaf = segments.last().unwrap();
current.insert(leaf.to_string(), value);
}
pub fn valid_keys(meta: &Meta) -> HashSet<String> {
let mut keys = HashSet::new();
collect_keys(meta, "", &mut keys);
keys
}
fn collect_keys(meta: &Meta, prefix: &str, keys: &mut HashSet<String>) {
for field in meta.fields {
let dotted = if prefix.is_empty() {
field.name.to_string()
} else {
format!("{prefix}.{}", field.name)
};
match &field.kind {
FieldKind::Leaf { .. } => {
keys.insert(dotted);
}
FieldKind::Nested { meta, .. } => {
collect_keys(meta, &dotted, keys);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entries(pairs: &[(&str, Value)]) -> Vec<(String, Value)> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
#[test]
fn flat_key() {
let table = overrides_to_table(&entries(&[("host", Value::String("0.0.0.0".into()))]));
assert_eq!(table["host"].as_str().unwrap(), "0.0.0.0");
}
#[test]
fn nested_key() {
let table =
overrides_to_table(&entries(&[("database.url", Value::String("pg://".into()))]));
let db = table["database"].as_table().unwrap();
assert_eq!(db["url"].as_str().unwrap(), "pg://");
}
#[test]
fn deep_nesting() {
let table = overrides_to_table(&entries(&[("a.b.c.d", Value::Integer(42))]));
assert_eq!(table["a"]["b"]["c"]["d"].as_integer().unwrap(), 42);
}
#[test]
fn multiple_entries_different_branches() {
let table = overrides_to_table(&entries(&[
("host", Value::String("x".into())),
("database.url", Value::String("pg://".into())),
("database.pool_size", Value::Integer(20)),
]));
assert_eq!(table["host"].as_str().unwrap(), "x");
let db = table["database"].as_table().unwrap();
assert_eq!(db["url"].as_str().unwrap(), "pg://");
assert_eq!(db["pool_size"].as_integer().unwrap(), 20);
}
#[test]
fn empty_list_empty_table() {
let table = overrides_to_table(&[]);
assert!(table.is_empty());
}
#[test]
fn last_entry_wins_for_same_key() {
let table = overrides_to_table(&entries(&[
("port", Value::Integer(3000)),
("port", Value::Integer(5000)),
]));
assert_eq!(table["port"].as_integer().unwrap(), 5000);
}
use crate::fixtures::test::TestConfig;
use confique::Config;
#[test]
fn valid_keys_collects_all_leaf_paths() {
let keys = valid_keys(&TestConfig::META);
assert!(keys.contains("host"));
assert!(keys.contains("port"));
assert!(keys.contains("debug"));
assert!(keys.contains("database.url"));
assert!(keys.contains("database.pool_size"));
assert_eq!(keys.len(), 5);
}
#[test]
fn valid_keys_excludes_section_names() {
let keys = valid_keys(&TestConfig::META);
assert!(!keys.contains("database"));
}
}