use crate::{error::Error, Result, Value};
use std::collections::HashMap;
use std::env;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)]
pub struct EnvOverrideConfig {
pub prefix: String,
pub separator: String,
pub lowercase_keys: bool,
pub enable_cache: bool,
pub custom_mappings: HashMap<String, String>,
}
impl Default for EnvOverrideConfig {
fn default() -> Self {
Self {
prefix: "APP_".to_string(),
separator: "_".to_string(),
lowercase_keys: true,
enable_cache: true,
custom_mappings: HashMap::new(),
}
}
}
pub struct EnvOverrideSystem {
config: EnvOverrideConfig,
cache: Arc<RwLock<HashMap<String, Option<String>>>>,
}
impl EnvOverrideSystem {
pub fn new(config: EnvOverrideConfig) -> Self {
Self {
config,
cache: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn with_defaults() -> Self {
Self::new(EnvOverrideConfig::default())
}
pub fn with_prefix(prefix: &str) -> Self {
let config = EnvOverrideConfig {
prefix: prefix.to_string(),
..Default::default()
};
Self::new(config)
}
pub fn apply_overrides(&self, mut value: Value) -> Result<Value> {
self.apply_overrides_recursive(&mut value, String::new())?;
Ok(value)
}
fn apply_overrides_recursive(&self, value: &mut Value, path: String) -> Result<()> {
match value {
Value::Table(ref mut table) => {
for (key, val) in table.iter_mut() {
let nested_path = if path.is_empty() {
key.clone()
} else {
format!("{path}.{key}")
};
if let Some(env_value) = self.get_env_override(&nested_path)? {
*val = env_value;
} else {
self.apply_overrides_recursive(val, nested_path)?;
}
}
}
Value::Array(ref mut array) => {
for (index, val) in array.iter_mut().enumerate() {
let nested_path = format!("{path}[{index}]");
self.apply_overrides_recursive(val, nested_path)?;
}
}
_ => {}
}
Ok(())
}
fn get_env_override(&self, key: &str) -> Result<Option<Value>> {
let env_keys = vec![
self.generate_env_key(key),
self.generate_docker_style_key(key),
self.generate_k8s_style_key(key),
];
for env_key in env_keys {
if let Some(env_value) = self.get_cached_env(&env_key)? {
return Ok(Some(self.parse_env_value(&env_value)));
}
}
if let Some(custom_key) = self.config.custom_mappings.get(key) {
if let Some(env_value) = self.get_cached_env(custom_key)? {
return Ok(Some(self.parse_env_value(&env_value)));
}
}
Ok(None)
}
fn generate_env_key(&self, key: &str) -> String {
let key = if self.config.lowercase_keys {
key.to_uppercase()
} else {
key.to_string()
};
let env_key = key.replace('.', &self.config.separator);
format!("{}{}", self.config.prefix, env_key)
}
fn generate_docker_style_key(&self, key: &str) -> String {
let key = key.to_uppercase().replace('.', "__");
format!("{}{}", self.config.prefix, key)
}
fn generate_k8s_style_key(&self, key: &str) -> String {
let key = key.to_uppercase().replace('.', "_");
format!("{}{}", self.config.prefix, key)
}
fn get_cached_env(&self, key: &str) -> Result<Option<String>> {
if !self.config.enable_cache {
return Ok(env::var(key).ok());
}
{
let cache = self
.cache
.read()
.map_err(|e| Error::internal(format!("Cache read error: {e}")))?;
if let Some(cached_value) = cache.get(key) {
return Ok(cached_value.clone());
}
}
let env_value = env::var(key).ok();
{
let mut cache = self
.cache
.write()
.map_err(|e| Error::internal(format!("Cache write error: {e}")))?;
cache.insert(key.to_string(), env_value.clone());
}
Ok(env_value)
}
fn parse_env_value(&self, value: &str) -> Value {
if value.contains(',') {
let items: Vec<Value> = value
.split(',')
.map(|s| self.parse_scalar_value(s.trim()))
.collect();
return Value::array(items);
}
if (value.trim_start().starts_with('{') || value.trim_start().starts_with('['))
&& cfg!(feature = "json")
{
#[cfg(feature = "json")]
{
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(value) {
return self.convert_json_value(json_value);
}
}
}
self.parse_scalar_value(value)
}
fn parse_scalar_value(&self, value: &str) -> Value {
match value.to_lowercase().as_str() {
"true" | "yes" | "on" | "1" => return Value::bool(true),
"false" | "no" | "off" | "0" => return Value::bool(false),
_ => {}
}
if let Ok(int_val) = value.parse::<i64>() {
return Value::integer(int_val);
}
if let Ok(float_val) = value.parse::<f64>() {
return Value::float(float_val);
}
Value::string(value)
}
#[cfg(feature = "json")]
#[allow(clippy::only_used_in_recursion)]
fn convert_json_value(&self, json_val: serde_json::Value) -> Value {
match json_val {
serde_json::Value::Null => Value::null(),
serde_json::Value::Bool(b) => Value::bool(b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::integer(i)
} else if let Some(f) = n.as_f64() {
Value::float(f)
} else {
Value::string(n.to_string())
}
}
serde_json::Value::String(s) => Value::string(s),
serde_json::Value::Array(arr) => {
let values: Vec<Value> = arr
.into_iter()
.map(|v| self.convert_json_value(v))
.collect();
Value::array(values)
}
serde_json::Value::Object(obj) => {
let mut map = std::collections::BTreeMap::new();
for (k, v) in obj {
map.insert(k, self.convert_json_value(v));
}
Value::table(map)
}
}
}
pub fn clear_cache(&self) -> Result<()> {
let mut cache = self
.cache
.write()
.map_err(|e| Error::internal(format!("Cache clear error: {e}")))?;
cache.clear();
Ok(())
}
pub fn cache_stats(&self) -> Result<(usize, usize)> {
let cache = self
.cache
.read()
.map_err(|e| Error::internal(format!("Cache stats error: {e}")))?;
let total_entries = cache.len();
let hit_entries = cache.values().filter(|v| v.is_some()).count();
Ok((hit_entries, total_entries))
}
}
pub fn apply_env_overrides(value: Value, config: EnvOverrideConfig) -> Result<Value> {
let system = EnvOverrideSystem::new(config);
system.apply_overrides(value)
}
pub fn apply_env_overrides_default(value: Value) -> Result<Value> {
let system = EnvOverrideSystem::with_defaults();
system.apply_overrides(value)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_env_key_generation() {
let config = EnvOverrideConfig {
prefix: "APP_".to_string(),
separator: "_".to_string(),
lowercase_keys: true,
enable_cache: false,
custom_mappings: HashMap::new(),
};
let system = EnvOverrideSystem::new(config);
assert_eq!(
system.generate_env_key("database.host"),
"APP_DATABASE_HOST"
);
assert_eq!(
system.generate_docker_style_key("database.host"),
"APP_DATABASE__HOST"
);
assert_eq!(
system.generate_k8s_style_key("database.host"),
"APP_DATABASE_HOST"
);
}
#[test]
fn test_value_parsing() {
let system = EnvOverrideSystem::with_defaults();
assert_eq!(system.parse_scalar_value("true"), Value::bool(true));
assert_eq!(system.parse_scalar_value("false"), Value::bool(false));
assert_eq!(system.parse_scalar_value("123"), Value::integer(123));
assert_eq!(system.parse_scalar_value("1.234"), Value::float(1.234));
assert_eq!(system.parse_scalar_value("hello"), Value::string("hello"));
}
#[test]
fn test_array_parsing() {
let system = EnvOverrideSystem::with_defaults();
let result = system.parse_env_value("a,b,c");
if let Value::Array(arr) = result {
assert_eq!(arr.len(), 3);
assert_eq!(arr[0], Value::string("a"));
assert_eq!(arr[1], Value::string("b"));
assert_eq!(arr[2], Value::string("c"));
} else {
panic!("Expected array");
}
}
#[test]
fn test_cache_operations() {
let system = EnvOverrideSystem::with_defaults();
let (hits, total) = system.cache_stats().unwrap();
assert_eq!(hits, 0);
assert_eq!(total, 0);
system.clear_cache().unwrap();
let (hits, total) = system.cache_stats().unwrap();
assert_eq!(hits, 0);
assert_eq!(total, 0);
}
}