Files
el/ui/vessels/el-config/src/source.rs
T

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");
}
}