use std::collections::BTreeMap;
use crate::text_value;
use crate::value::Value;
pub(crate) fn tree(prefix: &str, nest: &str, allow_empty: bool) -> Value {
let mut root = BTreeMap::new();
for (path, text) in variables(prefix, nest) {
if !allow_empty && text.trim().is_empty() {
continue;
}
insert(&mut root, &path, text_value::from_text(&text));
}
Value::Table(root)
}
pub(crate) fn variables(prefix: &str, nest: &str) -> Vec<(Vec<String>, String)> {
let mut found = Vec::new();
for (name, value) in std::env::vars_os() {
if name.is_empty() {
continue;
}
let name = name.to_string_lossy();
let name = name.trim();
let Some(head) = name.get(..prefix.len()) else {
continue;
};
if !head.eq_ignore_ascii_case(prefix) {
continue;
}
let rest = name[prefix.len()..].trim();
let segments: Vec<String> = rest
.split(nest)
.map(|segment| segment.trim().to_ascii_lowercase())
.collect();
if segments.iter().any(String::is_empty) {
continue;
}
found.push((segments, value.to_string_lossy().into_owned()));
}
found
}
fn insert(root: &mut BTreeMap<String, Value>, path: &[String], value: Value) {
let Some((last, walk)) = path.split_last() else {
return;
};
let mut here = root;
for segment in walk {
let slot = here
.entry(segment.clone())
.or_insert_with(|| Value::Table(BTreeMap::new()));
if !matches!(slot, Value::Table(_)) {
*slot = Value::Table(BTreeMap::new());
}
let Value::Table(table) = slot else {
unreachable!("the slot was just made a table");
};
here = table;
}
match here.get_mut(last) {
Some(Value::Table(_)) => {}
_ => {
here.insert(last.clone(), value);
}
}
}
#[cfg(test)]
mod tests {
use super::tree;
use crate::value::Value;
fn original(prefix: &str, nest: &str) -> Value {
use figment::Provider as _;
let provider = figment::providers::Env::prefixed(prefix).split(nest);
let data = provider.data().expect("the environment always reads");
let dict = data.into_values().next().unwrap_or_default();
Value::Table(
dict.iter()
.map(|(key, value)| (key.clone(), crate::backend::figment::from_figment(value)))
.collect(),
)
}
#[test]
fn the_environment_reads_the_same_as_it_always_did() {
let prefix = "DCENVPORT_";
let cases = [
("HOST", "localhost"),
("PORT", "8080"),
("DEBUG", "true"),
("RATIO", "1.5"),
("TAGS", "[a, b, c]"),
("POOL__MAX", "32"),
("POOL__MIN", "1"),
("DEEP__A__B__C", "leaf"),
("QUOTED", "\"8080\""),
("STRUCTURED", "{key=10}"),
("EMPTY_SEGMENT____HERE", "dropped"),
("MiXeD", "case"),
];
for (name, value) in cases {
std::env::set_var(format!("{prefix}{name}"), value);
}
let ours = tree(prefix, "__", true);
let theirs = original(prefix, "__");
for (name, _) in cases {
std::env::remove_var(format!("{prefix}{name}"));
}
assert_eq!(ours, theirs);
}
#[test]
fn a_variable_whose_name_is_not_ascii_is_passed_by_rather_than_panicked_on() {
let prefix = "DCENVUTF8_";
for name in ["€€", "ü", "日本語", "€€€€€€€€€€"] {
std::env::set_var(name, "whatever");
}
std::env::set_var(format!("{prefix}HOST"), "db.internal");
let tree = tree(prefix, "__", false);
for name in ["€€", "ü", "日本語", "€€€€€€€€€€"] {
std::env::remove_var(name);
}
std::env::remove_var(format!("{prefix}HOST"));
assert!(matches!(tree, Value::Table(table) if table.contains_key("host")));
}
#[test]
fn a_blank_variable_is_dropped_unless_it_was_asked_for() {
let prefix = "DCENVBLANK_";
std::env::set_var(format!("{prefix}HOST"), "");
let dropped = tree(prefix, "__", false);
let kept = tree(prefix, "__", true);
std::env::remove_var(format!("{prefix}HOST"));
assert_eq!(dropped, Value::Table(std::collections::BTreeMap::new()));
assert!(matches!(kept, Value::Table(table) if table.contains_key("host")));
}
}