249 lines
6.4 KiB
Rust
249 lines
6.4 KiB
Rust
/// 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<String>;
|
|
|
|
/// All key→value pairs from this source.
|
|
fn all(&self) -> HashMap<String, String>;
|
|
}
|
|
|
|
/// 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<String>) -> 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<String> {
|
|
std::env::var(self.env_key(key)).ok()
|
|
}
|
|
|
|
fn all(&self) -> HashMap<String, String> {
|
|
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<String, String>,
|
|
}
|
|
|
|
impl MapSource {
|
|
pub fn new(name: impl Into<String>) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
values: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
|
|
self.values.insert(key.into(), value.into());
|
|
}
|
|
|
|
pub fn from_map(name: impl Into<String>, map: HashMap<String, String>) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
values: map,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ConfigSource for MapSource {
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
|
|
fn get_raw(&self, key: &str) -> Option<String> {
|
|
self.values.get(key).cloned()
|
|
}
|
|
|
|
fn all(&self) -> HashMap<String, String> {
|
|
self.values.clone()
|
|
}
|
|
}
|
|
|
|
/// Typed config value extractor.
|
|
pub trait FromConfigStr: Sized {
|
|
fn from_config_str(s: &str) -> Result<Self, ConfigError>;
|
|
}
|
|
|
|
impl FromConfigStr for String {
|
|
fn from_config_str(s: &str) -> Result<Self, ConfigError> {
|
|
Ok(s.to_string())
|
|
}
|
|
}
|
|
|
|
impl FromConfigStr for u32 {
|
|
fn from_config_str(s: &str) -> Result<Self, ConfigError> {
|
|
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<Self, ConfigError> {
|
|
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<Self, ConfigError> {
|
|
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<Self, ConfigError> {
|
|
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<Self, ConfigError> {
|
|
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<Self, ConfigError> {
|
|
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<Self, ConfigError> {
|
|
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");
|
|
}
|
|
}
|