use serde_json::Value;
use super::{ConfigProvider, ProviderKind};
use crate::config::map::ConfigMap;
use crate::error::ConfigError;
const DEFAULT_PREFIX: &str = "APP";
const NESTING_SEPARATOR: &str = "__";
pub struct EnvProvider {
prefix: String,
}
impl EnvProvider {
pub fn new() -> Self {
Self { prefix: DEFAULT_PREFIX.to_owned() }
}
pub fn with_prefix(prefix: impl Into<String>) -> Self {
Self { prefix: prefix.into() }
}
fn collect<I>(&self, vars: I) -> ConfigMap
where
I: IntoIterator<Item = (String, String)>,
{
let match_prefix = format!("{}_", self.prefix.to_uppercase());
let flat: ConfigMap = vars
.into_iter()
.filter_map(|(key, value)| {
let rest = key.to_uppercase().strip_prefix(&match_prefix)?.to_owned();
if rest.is_empty() {
return None;
}
let config_key = rest.to_lowercase().replace(NESTING_SEPARATOR, ".");
Some((config_key, coerce_scalar(value)))
})
.collect();
flat.expand_dotted()
}
}
impl Default for EnvProvider {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl ConfigProvider for EnvProvider {
fn name(&self) -> String {
format!("env:{}_*", self.prefix.to_uppercase())
}
fn kind(&self) -> ProviderKind {
ProviderKind::Env
}
async fn load(&self) -> Result<ConfigMap, ConfigError> {
Ok(self.collect(std::env::vars()))
}
}
fn coerce_scalar(raw: String) -> Value {
match raw.as_str() {
"true" => return Value::Bool(true),
"false" => return Value::Bool(false),
"null" => return Value::Null,
_ => {}
}
if let Ok(i) = raw.parse::<i64>() {
return Value::from(i);
}
if let Ok(u) = raw.parse::<u64>() {
return Value::from(u);
}
if let Ok(f) = raw.parse::<f64>()
&& f.is_finite()
{
return Value::from(f);
}
Value::String(raw)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
#[test]
fn filters_by_prefix_and_ignores_unrelated_env() {
let provider = EnvProvider::new();
let out = provider.collect(vars(&[
("PATH", "/usr/bin"),
("HOME", "/home/x"),
("APP_NAME", "svc"),
]));
assert_eq!(out.len(), 1);
assert_eq!(out.get("name"), Some(&json!("svc")));
}
#[test]
fn maps_double_underscore_to_nesting() {
let provider = EnvProvider::new();
let out = provider.collect(vars(&[("APP_DATABASE__POOL__MAX", "20")]));
assert_eq!(out.get("database"), Some(&json!({ "pool": { "max": 20 } })));
}
#[test]
fn coerces_scalar_types() {
let provider = EnvProvider::new();
let out = provider.collect(vars(&[
("APP_DEBUG", "true"),
("APP_PORT", "8080"),
("APP_RATIO", "0.5"),
("APP_LABEL", "edge"),
]));
assert_eq!(out.get("debug"), Some(&json!(true)));
assert_eq!(out.get("port"), Some(&json!(8080)));
assert_eq!(out.get("ratio"), Some(&json!(0.5)));
assert_eq!(out.get("label"), Some(&json!("edge")));
}
#[test]
fn custom_prefix() {
let provider = EnvProvider::with_prefix("KLAUTHED");
let out = provider.collect(vars(&[("KLAUTHED_NAME", "svc"), ("APP_NAME", "no")]));
assert_eq!(out.len(), 1);
assert_eq!(out.get("name"), Some(&json!("svc")));
}
}