use std::str::FromStr;
#[must_use]
pub fn flat_env_string(prefix: &str, suffix: &str) -> Option<String> {
let key = format!("{prefix}_{suffix}");
match std::env::var(&key) {
Ok(v) if !v.is_empty() => {
tracing::debug!(env_var = %key, "flat env override applied");
Some(v)
}
_ => None,
}
}
#[must_use]
pub fn flat_env_list(prefix: &str, suffix: &str) -> Option<Vec<String>> {
flat_env_string(prefix, suffix).map(|v| {
v.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
}
#[must_use]
pub fn flat_env_bool(prefix: &str, suffix: &str) -> Option<bool> {
flat_env_string(prefix, suffix).and_then(|v| match v.to_lowercase().as_str() {
"true" | "1" | "yes" => Some(true),
"false" | "0" | "no" => Some(false),
_ => {
let key = format!("{prefix}_{suffix}");
tracing::warn!(env_var = %key, value = %v, "invalid bool value, ignoring");
None
}
})
}
#[must_use]
pub fn flat_env_parsed<T: FromStr>(prefix: &str, suffix: &str) -> Option<T> {
flat_env_string(prefix, suffix).and_then(|v| {
v.parse::<T>().ok().or_else(|| {
let key = format!("{prefix}_{suffix}");
tracing::warn!(env_var = %key, value = %v, "failed to parse, ignoring");
None
})
})
}
#[must_use]
pub fn flat_env_string_sensitive(prefix: &str, suffix: &str) -> Option<String> {
let key = format!("{prefix}_{suffix}");
match std::env::var(&key) {
Ok(v) if !v.is_empty() => {
tracing::debug!(env_var = %key, "flat env override applied (sensitive, value masked)");
Some(v)
}
_ => None,
}
}
pub trait ApplyFlatEnv {
fn apply_flat_env(&mut self, prefix: &str);
#[must_use]
fn env_var_docs(prefix: &str) -> Vec<EnvVarDoc>
where
Self: Sized,
{
let _ = prefix; Vec::new()
}
}
pub trait Normalize {
fn normalize(&mut self) {}
}
#[derive(Debug, Clone)]
pub struct EnvVarDoc {
pub name: String,
pub field_path: String,
pub type_hint: &'static str,
pub sensitive: bool,
}
pub fn load_config<T>(config_path: Option<&str>, env_prefix: &str) -> Result<T, super::ConfigError>
where
T: Default + serde::de::DeserializeOwned + ApplyFlatEnv + Normalize,
{
let mut opts = super::ConfigOptions {
env_prefix: env_prefix.to_string(),
..Default::default()
};
if let Some(path) = config_path {
let path_buf = std::path::PathBuf::from(path);
if let Some(parent) = path_buf.parent() {
if parent.as_os_str().is_empty() {
opts.config_paths.push(std::path::PathBuf::from("."));
} else {
opts.config_paths.push(parent.to_path_buf());
}
}
}
let cfg = super::Config::new(opts)?;
let mut config: T = cfg.unmarshal().unwrap_or_default();
config.apply_flat_env(env_prefix);
config.normalize();
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_flat_env_string() {
temp_env::with_var("TEST_PREFIX_MY_FIELD", Some("hello"), || {
assert_eq!(
flat_env_string("TEST_PREFIX", "MY_FIELD"),
Some("hello".to_string())
);
});
}
#[test]
fn test_flat_env_string_empty() {
temp_env::with_var("TEST_PREFIX_EMPTY", Some(""), || {
assert_eq!(flat_env_string("TEST_PREFIX", "EMPTY"), None);
});
}
#[test]
fn test_flat_env_string_missing() {
assert_eq!(flat_env_string("NONEXISTENT_PREFIX", "FIELD"), None);
}
#[test]
fn test_flat_env_list() {
temp_env::with_var("TEST_PREFIX_ITEMS", Some("a, b, c"), || {
assert_eq!(
flat_env_list("TEST_PREFIX", "ITEMS"),
Some(vec!["a".to_string(), "b".to_string(), "c".to_string()])
);
});
}
#[test]
fn test_flat_env_list_single() {
temp_env::with_var("TEST_PREFIX_SINGLE", Some("only"), || {
assert_eq!(
flat_env_list("TEST_PREFIX", "SINGLE"),
Some(vec!["only".to_string()])
);
});
}
#[test]
fn test_flat_env_list_with_empty_elements() {
temp_env::with_var("TEST_PREFIX_SPARSE", Some("a,,b, ,c"), || {
assert_eq!(
flat_env_list("TEST_PREFIX", "SPARSE"),
Some(vec!["a".to_string(), "b".to_string(), "c".to_string()])
);
});
}
#[test]
fn test_flat_env_bool_true_variants() {
temp_env::with_var("TEST_PREFIX_FLAG", Some("true"), || {
assert_eq!(flat_env_bool("TEST_PREFIX", "FLAG"), Some(true));
});
temp_env::with_var("TEST_PREFIX_FLAG", Some("1"), || {
assert_eq!(flat_env_bool("TEST_PREFIX", "FLAG"), Some(true));
});
temp_env::with_var("TEST_PREFIX_FLAG", Some("yes"), || {
assert_eq!(flat_env_bool("TEST_PREFIX", "FLAG"), Some(true));
});
temp_env::with_var("TEST_PREFIX_FLAG", Some("YES"), || {
assert_eq!(flat_env_bool("TEST_PREFIX", "FLAG"), Some(true));
});
temp_env::with_var("TEST_PREFIX_FLAG", Some("True"), || {
assert_eq!(flat_env_bool("TEST_PREFIX", "FLAG"), Some(true));
});
}
#[test]
fn test_flat_env_bool_false_variants() {
temp_env::with_var("TEST_PREFIX_FLAG", Some("false"), || {
assert_eq!(flat_env_bool("TEST_PREFIX", "FLAG"), Some(false));
});
temp_env::with_var("TEST_PREFIX_FLAG", Some("0"), || {
assert_eq!(flat_env_bool("TEST_PREFIX", "FLAG"), Some(false));
});
temp_env::with_var("TEST_PREFIX_FLAG", Some("no"), || {
assert_eq!(flat_env_bool("TEST_PREFIX", "FLAG"), Some(false));
});
}
#[test]
fn test_flat_env_bool_invalid() {
temp_env::with_var("TEST_PREFIX_FLAG", Some("maybe"), || {
assert_eq!(flat_env_bool("TEST_PREFIX", "FLAG"), None);
});
}
#[test]
fn test_flat_env_parsed_u64() {
temp_env::with_var("TEST_PREFIX_PORT", Some("8080"), || {
assert_eq!(flat_env_parsed::<u64>("TEST_PREFIX", "PORT"), Some(8080));
});
}
#[test]
fn test_flat_env_parsed_u16() {
temp_env::with_var("TEST_PREFIX_SMALL_PORT", Some("443"), || {
assert_eq!(
flat_env_parsed::<u16>("TEST_PREFIX", "SMALL_PORT"),
Some(443)
);
});
}
#[test]
fn test_flat_env_parsed_f64() {
temp_env::with_var("TEST_PREFIX_RATIO", Some("0.75"), || {
assert_eq!(flat_env_parsed::<f64>("TEST_PREFIX", "RATIO"), Some(0.75));
});
}
#[test]
fn test_flat_env_parsed_invalid() {
temp_env::with_var("TEST_PREFIX_PORT", Some("not_a_number"), || {
assert_eq!(flat_env_parsed::<u64>("TEST_PREFIX", "PORT"), None);
});
}
#[test]
fn test_flat_env_parsed_missing() {
assert_eq!(flat_env_parsed::<u64>("NONEXISTENT_PREFIX", "PORT"), None);
}
#[test]
fn test_flat_env_sensitive() {
temp_env::with_var("TEST_PREFIX_SECRET", Some("s3cr3t"), || {
assert_eq!(
flat_env_string_sensitive("TEST_PREFIX", "SECRET"),
Some("s3cr3t".to_string())
);
});
}
#[test]
fn test_flat_env_sensitive_empty() {
temp_env::with_var("TEST_PREFIX_SECRET", Some(""), || {
assert_eq!(flat_env_string_sensitive("TEST_PREFIX", "SECRET"), None);
});
}
#[test]
fn test_flat_env_sensitive_missing() {
assert_eq!(
flat_env_string_sensitive("NONEXISTENT_PREFIX", "SECRET"),
None
);
}
#[test]
fn test_apply_flat_env_trait() {
struct TestConfig {
value: String,
}
impl ApplyFlatEnv for TestConfig {
fn apply_flat_env(&mut self, prefix: &str) {
if let Some(v) = flat_env_string(prefix, "VALUE") {
self.value = v;
}
}
}
let mut config = TestConfig {
value: "default".into(),
};
temp_env::with_var("MY_PREFIX_VALUE", Some("overridden"), || {
config.apply_flat_env("MY_PREFIX");
});
assert_eq!(config.value, "overridden");
}
#[test]
fn test_apply_flat_env_no_override() {
struct TestConfig {
value: String,
}
impl ApplyFlatEnv for TestConfig {
fn apply_flat_env(&mut self, prefix: &str) {
if let Some(v) = flat_env_string(prefix, "VALUE") {
self.value = v;
}
}
}
let mut config = TestConfig {
value: "default".into(),
};
config.apply_flat_env("ABSENT_PREFIX");
assert_eq!(config.value, "default");
}
#[test]
fn test_normalize_trait_default() {
struct TestConfig;
impl Normalize for TestConfig {}
let mut config = TestConfig;
config.normalize(); }
#[test]
fn test_normalize_trait_custom() {
struct TestConfig {
username: String,
auth_enabled: bool,
}
impl Normalize for TestConfig {
fn normalize(&mut self) {
if !self.username.is_empty() {
self.auth_enabled = true;
}
}
}
let mut config = TestConfig {
username: "admin".into(),
auth_enabled: false,
};
config.normalize();
assert!(config.auth_enabled);
}
#[test]
fn test_env_var_doc() {
let doc = EnvVarDoc {
name: "DFE_LOADER_KAFKA_BROKERS".to_string(),
field_path: "kafka.brokers".to_string(),
type_hint: "list",
sensitive: false,
};
assert_eq!(doc.name, "DFE_LOADER_KAFKA_BROKERS");
assert_eq!(doc.field_path, "kafka.brokers");
assert_eq!(doc.type_hint, "list");
assert!(!doc.sensitive);
}
#[test]
fn test_env_var_docs_default() {
struct TestConfig;
impl ApplyFlatEnv for TestConfig {
fn apply_flat_env(&mut self, _prefix: &str) {}
}
let docs = TestConfig::env_var_docs("TEST");
assert!(docs.is_empty());
}
#[test]
fn test_flat_env_list_missing() {
assert_eq!(flat_env_list("NONEXISTENT_PREFIX", "ITEMS"), None);
}
#[test]
fn test_flat_env_bool_missing() {
assert_eq!(flat_env_bool("NONEXISTENT_PREFIX", "FLAG"), None);
}
}