use std::collections::HashMap;
use std::sync::Arc;
pub trait EnvSource: Send + Sync {
fn var(&self, name: &str) -> Option<String>;
fn vars(&self) -> Vec<(String, String)>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ProcessEnvSource;
impl EnvSource for ProcessEnvSource {
fn var(&self, name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn vars(&self) -> Vec<(String, String)> {
std::env::vars().collect()
}
}
#[derive(Debug, Clone, Default)]
pub struct MapEnvSource {
inner: HashMap<String, String>,
}
impl MapEnvSource {
pub fn new() -> Self {
Self::default()
}
pub fn with<K: Into<String>, V: Into<String>>(mut self, k: K, v: V) -> Self {
self.inner.insert(k.into(), v.into());
self
}
pub fn set<K: Into<String>, V: Into<String>>(&mut self, k: K, v: V) -> &mut Self {
self.inner.insert(k.into(), v.into());
self
}
}
impl<K, V> From<HashMap<K, V>> for MapEnvSource
where
K: Into<String>,
V: Into<String>,
{
fn from(map: HashMap<K, V>) -> Self {
Self {
inner: map.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
}
}
}
impl EnvSource for MapEnvSource {
fn var(&self, name: &str) -> Option<String> {
self.inner.get(name).cloned()
}
fn vars(&self) -> Vec<(String, String)> {
self.inner
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
}
#[derive(Clone)]
pub struct LayeredEnvSource {
base: Arc<dyn EnvSource>,
overrides: HashMap<String, String>,
}
impl LayeredEnvSource {
pub fn new<I, K, V>(base: Arc<dyn EnvSource>, overrides: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
Self {
base,
overrides: overrides
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
}
}
}
impl EnvSource for LayeredEnvSource {
fn var(&self, name: &str) -> Option<String> {
match self.overrides.get(name) {
Some(v) if !v.is_empty() => Some(v.clone()),
_ => self.base.var(name),
}
}
fn vars(&self) -> Vec<(String, String)> {
let mut merged: HashMap<String, String> = self.base.vars().into_iter().collect();
for (k, v) in &self.overrides {
if !v.is_empty() {
merged.insert(k.clone(), v.clone());
}
}
merged.into_iter().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::env::env_mutex;
const UNSET_VAR: &str = "ANODIZER_T3_FIXTURE_UNSET_VAR";
#[test]
fn process_env_source_reads_actual_env() {
let _g = env_mutex().lock().unwrap_or_else(|e| e.into_inner());
let key = "ANODIZER_T3_PROCESS_ENV_FIXTURE";
unsafe { std::env::set_var(key, "from-process-env") };
let got = ProcessEnvSource.var(key);
unsafe { std::env::remove_var(key) };
assert_eq!(got, Some("from-process-env".to_string()));
}
#[test]
fn process_env_source_returns_none_for_unset_var() {
assert_eq!(ProcessEnvSource.var(UNSET_VAR), None);
}
#[test]
fn map_env_source_returns_inserted_value() {
let src = MapEnvSource::new().with("K", "V");
assert_eq!(src.var("K"), Some("V".to_string()));
}
#[test]
fn map_env_source_returns_none_for_missing_key() {
let src = MapEnvSource::new().with("K", "V");
assert_eq!(src.var("OTHER"), None);
}
#[test]
fn layered_env_source_override_wins_over_base() {
let base: Arc<dyn EnvSource> = Arc::new(MapEnvSource::new().with("K", "base"));
let layered = LayeredEnvSource::new(base, [("K", "overridden")]);
assert_eq!(layered.var("K"), Some("overridden".to_string()));
}
#[test]
fn layered_env_source_empty_override_falls_through_to_base() {
let base: Arc<dyn EnvSource> = Arc::new(MapEnvSource::new().with("K", "base"));
let layered = LayeredEnvSource::new(base, [("K", "")]);
assert_eq!(layered.var("K"), Some("base".to_string()));
}
#[test]
fn layered_env_source_unset_falls_through_to_base() {
let base: Arc<dyn EnvSource> = Arc::new(MapEnvSource::new().with("BASE_ONLY", "b"));
let layered = LayeredEnvSource::new(base, [("OTHER", "o")]);
assert_eq!(layered.var("BASE_ONLY"), Some("b".to_string()));
assert_eq!(layered.var("NEITHER"), None);
assert_eq!(layered.var("OTHER"), Some("o".to_string()));
}
#[test]
fn layered_env_source_vars_merges_base_and_overrides() {
let base: Arc<dyn EnvSource> =
Arc::new(MapEnvSource::new().with("A", "base-a").with("B", "b"));
let layered = LayeredEnvSource::new(base, [("A", "over-a"), ("C", "c")]);
let map: HashMap<String, String> = layered.vars().into_iter().collect();
assert_eq!(map.get("A").map(String::as_str), Some("over-a"));
assert_eq!(map.get("B").map(String::as_str), Some("b"));
assert_eq!(map.get("C").map(String::as_str), Some("c"));
}
#[test]
fn map_env_source_from_hashmap_preserves_entries() {
let mut m: HashMap<&str, &str> = HashMap::new();
m.insert("A", "1");
m.insert("B", "2");
let src: MapEnvSource = m.into();
assert_eq!(src.var("A"), Some("1".to_string()));
assert_eq!(src.var("B"), Some("2".to_string()));
assert_eq!(src.var("C"), None);
}
}