Archived
add el-style, el-layout, el-i18n, el-config, el-secrets: responsive by default, theme-driven, zero breakpoints
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
/// Config — layered configuration with typed access.
|
||||
///
|
||||
/// Sources are stacked in priority order. The first source to provide a value
|
||||
/// for a key wins. Resolution order:
|
||||
/// 1. Environment variables (highest)
|
||||
/// 2. .env file (dev only)
|
||||
/// 3. el.toml [env.<current>] section
|
||||
/// 4. el.toml [config] section (base)
|
||||
/// 5. Defaults defined in code (lowest)
|
||||
|
||||
use std::collections::HashMap;
|
||||
use crate::error::ConfigError;
|
||||
use crate::source::{ConfigSource, FromConfigStr, MapSource};
|
||||
use crate::env::Environment;
|
||||
|
||||
/// The main configuration object.
|
||||
///
|
||||
/// Holds a stack of sources and resolves keys through them in order.
|
||||
pub struct Config {
|
||||
/// Sources in descending priority order (index 0 = highest priority).
|
||||
sources: Vec<Box<dyn ConfigSource>>,
|
||||
/// Environment in effect.
|
||||
pub environment: Environment,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create an empty config with no sources.
|
||||
pub fn new(env: Environment) -> Self {
|
||||
Self {
|
||||
sources: Vec::new(),
|
||||
environment: env,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build with the default source stack for an application:
|
||||
/// env vars > defaults map.
|
||||
pub fn default_stack() -> Self {
|
||||
use crate::source::EnvVarSource;
|
||||
let env = Environment::current();
|
||||
let mut config = Self::new(env);
|
||||
config.push_source(Box::new(EnvVarSource::new()));
|
||||
config
|
||||
}
|
||||
|
||||
/// Add a source at the lowest priority (end of the stack).
|
||||
pub fn push_source(&mut self, source: Box<dyn ConfigSource>) {
|
||||
self.sources.push(source);
|
||||
}
|
||||
|
||||
/// Add a source at the highest priority (beginning of the stack).
|
||||
pub fn prepend_source(&mut self, source: Box<dyn ConfigSource>) {
|
||||
self.sources.insert(0, source);
|
||||
}
|
||||
|
||||
/// Add defaults as the lowest-priority source.
|
||||
pub fn set_defaults(&mut self, defaults: HashMap<String, String>) {
|
||||
let src = MapSource::from_map("defaults", defaults);
|
||||
self.sources.push(Box::new(src));
|
||||
}
|
||||
|
||||
/// Get a raw string value for a key.
|
||||
pub fn get_raw(&self, key: &str) -> Option<String> {
|
||||
for source in &self.sources {
|
||||
if let Some(val) = source.get_raw(key) {
|
||||
return Some(val);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get a typed value for a key.
|
||||
///
|
||||
/// Returns an error if the key is not found or can't be parsed.
|
||||
pub fn get<T: FromConfigStr>(&self, key: &str) -> Result<T, ConfigError> {
|
||||
let raw = self.get_raw(key).ok_or_else(|| ConfigError::NotFound {
|
||||
key: key.to_string(),
|
||||
})?;
|
||||
|
||||
T::from_config_str(&raw).map_err(|e| {
|
||||
// Inject the key into TypeMismatch errors
|
||||
match e {
|
||||
ConfigError::TypeMismatch { expected, got, .. } => {
|
||||
ConfigError::TypeMismatch {
|
||||
key: key.to_string(),
|
||||
expected,
|
||||
got,
|
||||
}
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a typed value with a fallback default.
|
||||
pub fn get_or<T: FromConfigStr>(&self, key: &str, default: T) -> T {
|
||||
self.get(key).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Get an optional typed value. Returns None if not set (not an error).
|
||||
pub fn get_opt<T: FromConfigStr>(&self, key: &str) -> Result<Option<T>, ConfigError> {
|
||||
match self.get_raw(key) {
|
||||
None => Ok(None),
|
||||
Some(raw) => T::from_config_str(&raw)
|
||||
.map(Some)
|
||||
.map_err(|e| match e {
|
||||
ConfigError::TypeMismatch { expected, got, .. } => {
|
||||
ConfigError::TypeMismatch {
|
||||
key: key.to_string(),
|
||||
expected,
|
||||
got,
|
||||
}
|
||||
}
|
||||
other => other,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// All key→value pairs from all sources (merged, highest-priority wins).
|
||||
pub fn all(&self) -> HashMap<String, String> {
|
||||
let mut result = HashMap::new();
|
||||
// Iterate in reverse order (lowest priority first) so higher-priority
|
||||
// sources overwrite lower-priority ones.
|
||||
for source in self.sources.iter().rev() {
|
||||
for (k, v) in source.all() {
|
||||
result.insert(k, v);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Load config from an `el.toml` string.
|
||||
///
|
||||
/// Reads `[config]` as the base, then overlays `[env.<environment>]`.
|
||||
pub fn load_from_toml(toml_str: &str, env: &Environment) -> Result<MapSource, ConfigError> {
|
||||
let value: toml::Value = toml::from_str(toml_str)
|
||||
.map_err(|e| ConfigError::ParseError(e.to_string()))?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
|
||||
// Load base [config] section
|
||||
if let Some(config_section) = value.get("config") {
|
||||
if let Some(table) = config_section.as_table() {
|
||||
flatten_toml_table(table, "", &mut map);
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay [env.<name>] section
|
||||
let env_key = env.name();
|
||||
if let Some(env_sections) = value.get("env") {
|
||||
if let Some(env_table) = env_sections.get(env_key) {
|
||||
if let Some(table) = env_table.as_table() {
|
||||
flatten_toml_table(table, "", &mut map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MapSource::from_map("el.toml", map))
|
||||
}
|
||||
|
||||
fn flatten_toml_table(
|
||||
table: &toml::value::Table,
|
||||
prefix: &str,
|
||||
out: &mut HashMap<String, String>,
|
||||
) {
|
||||
for (key, value) in table {
|
||||
let full_key = if prefix.is_empty() {
|
||||
key.clone()
|
||||
} else {
|
||||
format!("{}.{}", prefix, key)
|
||||
};
|
||||
|
||||
match value {
|
||||
toml::Value::String(s) => {
|
||||
out.insert(full_key, s.clone());
|
||||
}
|
||||
toml::Value::Integer(i) => {
|
||||
out.insert(full_key, i.to_string());
|
||||
}
|
||||
toml::Value::Float(f) => {
|
||||
out.insert(full_key, f.to_string());
|
||||
}
|
||||
toml::Value::Boolean(b) => {
|
||||
out.insert(full_key, b.to_string());
|
||||
}
|
||||
toml::Value::Table(t) => {
|
||||
flatten_toml_table(t, &full_key, out);
|
||||
}
|
||||
_ => {} // Arrays, datetimes: skip for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro for typed config access on a global/injected Config.
|
||||
///
|
||||
/// ```ignore
|
||||
/// let name = config!(cfg, "app.name", String);
|
||||
/// let port = config!(cfg, "server.port", u32, 8080);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! config {
|
||||
($cfg:expr, $key:expr, $type:ty) => {
|
||||
$cfg.get::<$type>($key)
|
||||
};
|
||||
($cfg:expr, $key:expr, $type:ty, $default:expr) => {
|
||||
$cfg.get_or::<$type>($key, $default)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::source::MapSource;
|
||||
|
||||
fn make_config(pairs: &[(&str, &str)]) -> Config {
|
||||
let mut src = MapSource::new("test");
|
||||
for (k, v) in pairs {
|
||||
src.insert(*k, *v);
|
||||
}
|
||||
let mut cfg = Config::new(Environment::Development);
|
||||
cfg.push_source(Box::new(src));
|
||||
cfg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_string() {
|
||||
let cfg = make_config(&[("app.name", "TestApp")]);
|
||||
assert_eq!(cfg.get::<String>("app.name").unwrap(), "TestApp");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_u32() {
|
||||
let cfg = make_config(&[("server.port", "8080")]);
|
||||
assert_eq!(cfg.get::<u32>("server.port").unwrap(), 8080u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_bool() {
|
||||
let cfg = make_config(&[("feature.enabled", "true")]);
|
||||
assert_eq!(cfg.get::<bool>("feature.enabled").unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_missing_returns_error() {
|
||||
let cfg = Config::new(Environment::Development);
|
||||
assert!(cfg.get::<String>("missing.key").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_or_default() {
|
||||
let cfg = Config::new(Environment::Development);
|
||||
assert_eq!(cfg.get_or("timeout", 30u32), 30u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_or_prefers_source() {
|
||||
let cfg = make_config(&[("timeout", "60")]);
|
||||
assert_eq!(cfg.get_or("timeout", 30u32), 60u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_priority_source_wins() {
|
||||
let mut cfg = Config::new(Environment::Development);
|
||||
let mut low = MapSource::new("low");
|
||||
low.insert("key", "low-value");
|
||||
let mut high = MapSource::new("high");
|
||||
high.insert("key", "high-value");
|
||||
cfg.push_source(Box::new(high));
|
||||
cfg.push_source(Box::new(low));
|
||||
// First source (index 0) is highest priority
|
||||
assert_eq!(cfg.get::<String>("key").unwrap(), "high-value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_opt_missing_is_none() {
|
||||
let cfg = Config::new(Environment::Development);
|
||||
assert_eq!(cfg.get_opt::<String>("missing").unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_opt_present_is_some() {
|
||||
let cfg = make_config(&[("key", "value")]);
|
||||
assert_eq!(
|
||||
cfg.get_opt::<String>("key").unwrap(),
|
||||
Some("value".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_from_toml_base() {
|
||||
let toml = r#"
|
||||
[config]
|
||||
app.name = "MyApp"
|
||||
app.version = "1.0.0"
|
||||
"#;
|
||||
let src = load_from_toml(toml, &Environment::Development).unwrap();
|
||||
assert_eq!(src.get_raw("app.name"), Some("MyApp".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_from_toml_env_overlay() {
|
||||
let toml = r#"
|
||||
[config]
|
||||
api.base_url = "https://api.example.com"
|
||||
|
||||
[env.development]
|
||||
api.base_url = "http://localhost:8080"
|
||||
"#;
|
||||
let src = load_from_toml(toml, &Environment::Development).unwrap();
|
||||
assert_eq!(
|
||||
src.get_raw("api.base_url"),
|
||||
Some("http://localhost:8080".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_from_toml_env_does_not_override_in_prod() {
|
||||
let toml = r#"
|
||||
[config]
|
||||
api.base_url = "https://api.example.com"
|
||||
|
||||
[env.development]
|
||||
api.base_url = "http://localhost:8080"
|
||||
"#;
|
||||
let src = load_from_toml(toml, &Environment::Production).unwrap();
|
||||
assert_eq!(
|
||||
src.get_raw("api.base_url"),
|
||||
Some("https://api.example.com".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_from_toml_invalid() {
|
||||
let result = load_from_toml("not valid toml %%%", &Environment::Development);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/// Environment detection — which deployment context are we in?
|
||||
|
||||
/// The current deployment environment.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Environment {
|
||||
/// Local developer machine. Verbose errors, hot reload, relaxed auth.
|
||||
Development,
|
||||
/// Pre-production environment. Production build, test data.
|
||||
Staging,
|
||||
/// Live production. Minimal logging, strict auth, performance mode.
|
||||
Production,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
/// Detect from the `EL_ENV` environment variable (or `APP_ENV`, `RUST_ENV`).
|
||||
///
|
||||
/// Falls back to Development if unset or unrecognized.
|
||||
pub fn current() -> Self {
|
||||
let val = std::env::var("EL_ENV")
|
||||
.or_else(|_| std::env::var("APP_ENV"))
|
||||
.or_else(|_| std::env::var("RUST_ENV"))
|
||||
.unwrap_or_default();
|
||||
|
||||
Self::from_str(&val)
|
||||
}
|
||||
|
||||
/// Parse from a string.
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"production" | "prod" => Environment::Production,
|
||||
"staging" | "stage" => Environment::Staging,
|
||||
_ => Environment::Development,
|
||||
}
|
||||
}
|
||||
|
||||
/// The canonical name for this environment.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Environment::Development => "development",
|
||||
Environment::Staging => "staging",
|
||||
Environment::Production => "production",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this is a production environment.
|
||||
pub fn is_production(&self) -> bool {
|
||||
matches!(self, Environment::Production)
|
||||
}
|
||||
|
||||
/// Whether this is a development environment.
|
||||
pub fn is_development(&self) -> bool {
|
||||
matches!(self, Environment::Development)
|
||||
}
|
||||
|
||||
/// Whether debug features should be enabled.
|
||||
pub fn debug_enabled(&self) -> bool {
|
||||
!self.is_production()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Environment {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.name())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_production() {
|
||||
assert_eq!(Environment::from_str("production"), Environment::Production);
|
||||
assert_eq!(Environment::from_str("prod"), Environment::Production);
|
||||
assert_eq!(Environment::from_str("PRODUCTION"), Environment::Production);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_staging() {
|
||||
assert_eq!(Environment::from_str("staging"), Environment::Staging);
|
||||
assert_eq!(Environment::from_str("stage"), Environment::Staging);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_development_fallback() {
|
||||
assert_eq!(Environment::from_str("dev"), Environment::Development);
|
||||
assert_eq!(Environment::from_str(""), Environment::Development);
|
||||
assert_eq!(Environment::from_str("unknown"), Environment::Development);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_is_production() {
|
||||
assert!(Environment::Production.is_production());
|
||||
assert!(!Environment::Development.is_production());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn development_debug_enabled() {
|
||||
assert!(Environment::Development.debug_enabled());
|
||||
assert!(!Environment::Production.debug_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_name() {
|
||||
assert_eq!(Environment::Production.name(), "production");
|
||||
assert_eq!(Environment::Staging.name(), "staging");
|
||||
assert_eq!(Environment::Development.name(), "development");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
assert_eq!(format!("{}", Environment::Production), "production");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("config key '{key}' not found")]
|
||||
NotFound { key: String },
|
||||
|
||||
#[error("config key '{key}': expected {expected}, got '{got}'")]
|
||||
TypeMismatch {
|
||||
key: String,
|
||||
expected: String,
|
||||
got: String,
|
||||
},
|
||||
|
||||
#[error("config parse error: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("config source error '{source_name}': {message}")]
|
||||
SourceError { source_name: String, message: String },
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//! el-config — Layered, typed configuration for el-ui applications.
|
||||
//!
|
||||
//! ## Resolution order (highest priority wins)
|
||||
//!
|
||||
//! 1. Environment variables (`EL_APP_NAME=...`)
|
||||
//! 2. `.env` file (development only)
|
||||
//! 3. `el.toml` `[env.<current>]` section
|
||||
//! 4. `el.toml` `[config]` base section
|
||||
//! 5. Defaults defined in code
|
||||
//!
|
||||
//! ## Quick start
|
||||
//!
|
||||
//! ```
|
||||
//! use el_config::prelude::*;
|
||||
//!
|
||||
//! let mut cfg = Config::new(Environment::Development);
|
||||
//! let mut defaults = std::collections::HashMap::new();
|
||||
//! defaults.insert("app.name".to_string(), "MyApp".to_string());
|
||||
//! defaults.insert("server.port".to_string(), "8080".to_string());
|
||||
//! cfg.set_defaults(defaults);
|
||||
//!
|
||||
//! let name = cfg.get::<String>("app.name").unwrap();
|
||||
//! let port = cfg.get::<u32>("server.port").unwrap();
|
||||
//! assert_eq!(name, "MyApp");
|
||||
//! assert_eq!(port, 8080);
|
||||
//! ```
|
||||
|
||||
#![deny(warnings)]
|
||||
|
||||
pub mod config;
|
||||
pub mod env;
|
||||
pub mod error;
|
||||
pub mod source;
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::config::{load_from_toml, Config};
|
||||
pub use crate::env::Environment;
|
||||
pub use crate::error::ConfigError;
|
||||
pub use crate::source::{ConfigSource, EnvVarSource, FromConfigStr, MapSource};
|
||||
}
|
||||
|
||||
pub use prelude::*;
|
||||
@@ -0,0 +1,248 @@
|
||||
/// 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user