/// ConfigSource trait and implementations. /// /// Each source provides key→value pairs. Sources are stacked in priority order; /// the Config struct resolves by asking each source in turn. use std::collections::HashMap; use crate::error::ConfigError; /// A source of configuration values. pub trait ConfigSource: Send + Sync { /// The name of this source (for debugging/error messages). fn name(&self) -> &str; /// Get a raw string value for a key. /// Returns None if this source doesn't have the key. fn get_raw(&self, key: &str) -> Option; /// All key→value pairs from this source. fn all(&self) -> HashMap; } /// Reads from environment variables. /// /// Keys are mapped: `app.name` → `EL_APP_NAME` (uppercased, dots → underscores). pub struct EnvVarSource { /// Optional prefix. Default: "EL". prefix: String, } impl EnvVarSource { pub fn new() -> Self { Self { prefix: "EL".to_string() } } pub fn with_prefix(prefix: impl Into) -> Self { Self { prefix: prefix.into() } } fn env_key(&self, key: &str) -> String { let normalized = key.replace('.', "_").replace('-', "_").to_uppercase(); format!("{}_{}", self.prefix, normalized) } } impl Default for EnvVarSource { fn default() -> Self { Self::new() } } impl ConfigSource for EnvVarSource { fn name(&self) -> &str { "environment" } fn get_raw(&self, key: &str) -> Option { std::env::var(self.env_key(key)).ok() } fn all(&self) -> HashMap { let prefix = format!("{}_", self.prefix); std::env::vars() .filter(|(k, _)| k.starts_with(&prefix)) .map(|(k, v)| { let stripped = k.strip_prefix(&prefix).unwrap_or(&k); let config_key = stripped.to_lowercase().replace('_', "."); (config_key, v) }) .collect() } } /// Holds an in-memory map of config values. /// /// Used for defaults defined in code, or for config loaded from a parsed /// TOML/JSON file section. pub struct MapSource { name: String, values: HashMap, } impl MapSource { pub fn new(name: impl Into) -> Self { Self { name: name.into(), values: HashMap::new(), } } pub fn insert(&mut self, key: impl Into, value: impl Into) { self.values.insert(key.into(), value.into()); } pub fn from_map(name: impl Into, map: HashMap) -> Self { Self { name: name.into(), values: map, } } } impl ConfigSource for MapSource { fn name(&self) -> &str { &self.name } fn get_raw(&self, key: &str) -> Option { self.values.get(key).cloned() } fn all(&self) -> HashMap { self.values.clone() } } /// Typed config value extractor. pub trait FromConfigStr: Sized { fn from_config_str(s: &str) -> Result; } impl FromConfigStr for String { fn from_config_str(s: &str) -> Result { Ok(s.to_string()) } } impl FromConfigStr for u32 { fn from_config_str(s: &str) -> Result { s.parse().map_err(|_| ConfigError::TypeMismatch { key: String::new(), expected: "u32".to_string(), got: s.to_string(), }) } } impl FromConfigStr for u64 { fn from_config_str(s: &str) -> Result { s.parse().map_err(|_| ConfigError::TypeMismatch { key: String::new(), expected: "u64".to_string(), got: s.to_string(), }) } } impl FromConfigStr for i32 { fn from_config_str(s: &str) -> Result { s.parse().map_err(|_| ConfigError::TypeMismatch { key: String::new(), expected: "i32".to_string(), got: s.to_string(), }) } } impl FromConfigStr for i64 { fn from_config_str(s: &str) -> Result { s.parse().map_err(|_| ConfigError::TypeMismatch { key: String::new(), expected: "i64".to_string(), got: s.to_string(), }) } } impl FromConfigStr for f32 { fn from_config_str(s: &str) -> Result { s.parse().map_err(|_| ConfigError::TypeMismatch { key: String::new(), expected: "f32".to_string(), got: s.to_string(), }) } } impl FromConfigStr for f64 { fn from_config_str(s: &str) -> Result { s.parse().map_err(|_| ConfigError::TypeMismatch { key: String::new(), expected: "f64".to_string(), got: s.to_string(), }) } } impl FromConfigStr for bool { fn from_config_str(s: &str) -> Result { match s.to_lowercase().as_str() { "true" | "1" | "yes" | "on" => Ok(true), "false" | "0" | "no" | "off" => Ok(false), _ => Err(ConfigError::TypeMismatch { key: String::new(), expected: "bool".to_string(), got: s.to_string(), }), } } } #[cfg(test)] mod tests { use super::*; #[test] fn map_source_get() { let mut src = MapSource::new("test"); src.insert("app.name", "TestApp"); assert_eq!(src.get_raw("app.name"), Some("TestApp".to_string())); } #[test] fn map_source_missing() { let src = MapSource::new("test"); assert_eq!(src.get_raw("no.key"), None); } #[test] fn bool_from_config_str() { assert_eq!(bool::from_config_str("true").unwrap(), true); assert_eq!(bool::from_config_str("1").unwrap(), true); assert_eq!(bool::from_config_str("false").unwrap(), false); assert_eq!(bool::from_config_str("0").unwrap(), false); } #[test] fn u32_from_config_str() { assert_eq!(u32::from_config_str("42").unwrap(), 42u32); } #[test] fn u32_type_mismatch() { assert!(u32::from_config_str("not-a-number").is_err()); } #[test] fn env_key_mapping() { let src = EnvVarSource::new(); // app.name → EL_APP_NAME assert_eq!(src.env_key("app.name"), "EL_APP_NAME"); } #[test] fn env_source_name() { let src = EnvVarSource::new(); assert_eq!(src.name(), "environment"); } }