Archived
f4abfe6fdc
Belated rename commit for foundation/el-ui — was missed in the workspace-wide crates→vessels pass earlier today. Same structural intent as the rename in the other repos: 'crates' is the Rust word, 'vessel' is El's, and the directory rename is the marker that this slot holds an El buildable unit even if its current contents are still Rust pending port. Plus the El ports themselves — manifest.el + src/main.el per sub- vessel (el-aop, el-auth, el-config, el-i18n, el-identity, el-layout, el-platform, el-publish, el-secrets, el-services, el-style, el-ui- compiler). The ui-compiler is a stub: elc only emits C right now; generating browser-target JS/Wasm is the biggest open language gap and gets its own project. Until then, el-ui-compiler emits a JS module that throws elc.backend_missing so callers fail loudly. Cross-repo path dependencies in Cargo.toml updated to vessels/.
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");
|
|
}
|
|
}
|