use std::collections::HashMap;
use crate::constants::{DEFAULT_ENV_PREFIX, ENV_KEY_SEPARATOR};
use crate::domain::{Format, RawContent, Result, Source, SourceError, SourceKind, SourceMetadata};
#[derive(Debug, Clone)]
pub struct EnvConfig {
pub prefix: String,
pub separator: char,
pub lowercase_keys: bool,
pub key_mappings: HashMap<String, String>,
}
impl Default for EnvConfig {
fn default() -> Self {
Self {
prefix: DEFAULT_ENV_PREFIX.to_string(),
separator: ENV_KEY_SEPARATOR,
lowercase_keys: true,
key_mappings: HashMap::new(),
}
}
}
impl EnvConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
#[must_use]
pub fn separator(mut self, sep: char) -> Self {
self.separator = sep;
self
}
#[must_use]
pub fn lowercase_keys(mut self, lowercase: bool) -> Self {
self.lowercase_keys = lowercase;
self
}
#[must_use]
pub fn with_mapping(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
self.key_mappings.insert(from.into(), to.into());
self
}
}
#[derive(Debug)]
pub struct EnvSourceBuilder {
config: EnvConfig,
required: bool,
overrides: HashMap<String, String>,
}
impl EnvSourceBuilder {
#[must_use]
pub fn new() -> Self {
Self {
config: EnvConfig::default(),
required: false,
overrides: HashMap::new(),
}
}
#[must_use]
pub fn config(mut self, config: EnvConfig) -> Self {
self.config = config;
self
}
#[must_use]
pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
self.config.prefix = prefix.into();
self
}
#[must_use]
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
#[must_use]
pub fn with_override(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.overrides.insert(key.into(), value.into());
self
}
#[must_use]
pub fn with_overrides(mut self, overrides: HashMap<String, String>) -> Self {
self.overrides.extend(overrides);
self
}
#[must_use]
pub fn build(self) -> EnvSource {
EnvSource {
config: self.config,
required: self.required,
overrides: self.overrides,
}
}
}
impl Default for EnvSourceBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct EnvSource {
config: EnvConfig,
required: bool,
overrides: HashMap<String, String>,
}
impl EnvSource {
#[must_use]
pub fn new() -> Self {
Self {
config: EnvConfig::default(),
required: false,
overrides: HashMap::new(),
}
}
#[must_use]
pub fn builder() -> EnvSourceBuilder {
EnvSourceBuilder::new()
}
#[must_use]
pub fn with_prefix(prefix: impl Into<String>) -> Self {
Self {
config: EnvConfig {
prefix: prefix.into(),
..EnvConfig::default()
},
required: false,
overrides: HashMap::new(),
}
}
fn collect_vars(&self) -> HashMap<String, String> {
let mut result = HashMap::new();
let prefix_with_sep = format!("{}{}", self.config.prefix, self.config.separator);
for (key, value) in &self.overrides {
if key.starts_with(&prefix_with_sep) {
result.insert(key.clone(), value.clone());
}
}
for (key, value) in std::env::vars() {
if key.starts_with(&prefix_with_sep) {
result.insert(key, value);
}
}
result
}
fn vars_to_nested_map(&self, vars: HashMap<String, String>) -> serde_json::Value {
let prefix_with_sep = format!("{}{}", self.config.prefix, self.config.separator);
let mut result = serde_json::Map::new();
for (key, value) in vars {
let key_without_prefix = key.strip_prefix(&prefix_with_sep).unwrap_or(&key);
let mapped_key = self
.config
.key_mappings
.get(key_without_prefix)
.map(|s| s.as_str())
.unwrap_or(key_without_prefix);
let parts: Vec<&str> = mapped_key.split(self.config.separator).collect();
Self::insert_nested(&mut result, &parts, value);
}
serde_json::Value::Object(result)
}
fn insert_nested(
map: &mut serde_json::Map<String, serde_json::Value>,
parts: &[&str],
value: String,
) {
if parts.is_empty() {
return;
}
if parts.len() == 1 {
let json_value =
serde_json::from_str(&value).unwrap_or_else(|_| serde_json::Value::String(value));
map.insert(parts[0].to_string(), json_value);
return;
}
let first = parts[0];
let rest = &parts[1..];
let child = map
.entry(first.to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let serde_json::Value::Object(child_map) = child {
Self::insert_nested(child_map, rest, value);
}
}
}
impl Default for EnvSource {
fn default() -> Self {
Self::new()
}
}
impl Source for EnvSource {
fn kind(&self) -> SourceKind {
SourceKind::Env
}
fn metadata(&self) -> SourceMetadata {
SourceMetadata::new(format!("env:{}", self.config.prefix))
.with_env_var(&self.config.prefix)
.with_priority(200)
.with_optional(!self.required)
}
fn load_raw(&self) -> Result<RawContent> {
let vars = self.collect_vars();
if vars.is_empty() && self.required {
return Err(SourceError::not_found(
"No matching environment variables found",
));
}
let json_value = self.vars_to_nested_map(vars);
let content = serde_json::to_string(&json_value)
.map_err(|e| SourceError::serialization(&e.to_string()))?;
Ok(RawContent::from_string(content))
}
fn detect_format(&self) -> Option<Format> {
Some(Format::Json)
}
fn is_required(&self) -> bool {
self.required
}
}
#[cfg(feature = "async")]
mod async_impl {
use super::*;
use async_trait::async_trait;
#[async_trait]
impl Source for EnvSource {
async fn load_raw_async(&self) -> Result<RawContent> {
self.load_raw()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_env_config_default() {
let config = EnvConfig::default();
assert_eq!(config.prefix, "APP");
assert_eq!(config.separator, '_');
assert!(config.lowercase_keys);
}
#[test]
fn test_env_config_builder() {
let config = EnvConfig::new()
.prefix("MYAPP")
.separator(':')
.lowercase_keys(false);
assert_eq!(config.prefix, "MYAPP");
assert_eq!(config.separator, ':');
assert!(!config.lowercase_keys);
}
#[test]
fn test_env_source_builder() {
let source = EnvSource::builder().prefix("TEST").required(true).build();
assert!(source.is_required());
assert_eq!(source.config.prefix, "TEST");
}
#[test]
fn test_env_source_with_overrides() {
let source = EnvSource::builder()
.prefix("TEST")
.with_override("TEST_NAME", "test-value")
.with_override("TEST_NESTED_KEY", "nested-value")
.build();
let vars = source.collect_vars();
assert_eq!(vars.get("TEST_NAME"), Some(&"test-value".to_string()));
assert_eq!(
vars.get("TEST_NESTED_KEY"),
Some(&"nested-value".to_string())
);
}
#[test]
fn test_vars_to_nested_map() {
let source = EnvSource::builder()
.prefix("TEST")
.with_override("TEST_NAME", "myapp")
.with_override("TEST_SERVER_HOST", "localhost")
.with_override("TEST_SERVER_PORT", "8080")
.build();
let vars = source.collect_vars();
let nested = source.vars_to_nested_map(vars);
assert_eq!(nested.get("name"), Some(&serde_json::json!("myapp")));
assert!(nested.get("server").is_some());
let server = nested.get("server").unwrap().as_object().unwrap();
assert_eq!(server.get("host"), Some(&serde_json::json!("localhost")));
assert_eq!(server.get("port"), Some(&serde_json::json!(8080)));
}
#[test]
fn test_load_raw_with_overrides() {
let source = EnvSource::builder()
.prefix("TEST")
.with_override("TEST_NAME", "test-app")
.with_override("TEST_VALUE", "42")
.build();
let raw = source.load_raw().unwrap();
let content = raw.as_str().unwrap();
#[derive(Debug, serde::Deserialize, PartialEq)]
struct TestConfig {
name: String,
value: u32,
}
let config: TestConfig = serde_json::from_str(content.as_ref()).unwrap();
assert_eq!(config.name, "test-app");
assert_eq!(config.value, 42);
}
#[test]
fn test_metadata() {
let source = EnvSource::with_prefix("MYAPP");
let meta = source.metadata();
assert_eq!(meta.name, "env:MYAPP");
assert_eq!(source.kind(), SourceKind::Env);
assert_eq!(meta.priority, 200);
}
#[test]
fn test_key_mapping() {
let config = EnvConfig::new()
.prefix("TEST")
.with_mapping("OLD_KEY", "new_key");
let source = EnvSource {
config,
required: false,
overrides: [("TEST_OLD_KEY".to_string(), "value".to_string())]
.into_iter()
.collect(),
};
let vars = source.collect_vars();
let nested = source.vars_to_nested_map(vars);
assert_eq!(nested.get("new_key"), Some(&serde_json::json!("value")));
}
#[test]
fn test_empty_source() {
let source = EnvSource::builder()
.prefix("NONEXISTENT_PREFIX_XYZ")
.required(false)
.build();
let raw = source.load_raw().unwrap();
let content = raw.as_str().unwrap();
assert_eq!(content.as_ref(), "{}");
}
#[test]
fn test_json_value_parsing() {
let source = EnvSource::builder()
.prefix("TEST")
.with_override("TEST_NUMBERS", "[1, 2, 3]")
.with_override("TEST_ENABLED", "true")
.with_override("TEST_RATIO", "3.14")
.build();
let raw = source.load_raw().unwrap();
let content = raw.as_str().unwrap();
#[derive(Debug, serde::Deserialize, PartialEq)]
struct TestConfig {
numbers: Vec<i32>,
enabled: bool,
ratio: f64,
}
let config: TestConfig = serde_json::from_str(content.as_ref()).unwrap();
assert_eq!(config.numbers, vec![1, 2, 3]);
assert!(config.enabled);
assert!((config.ratio - 3.14).abs() < 0.001);
}
}