use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use crate::value::Value;
use crate::error::Error;
#[derive(Debug, Default)]
pub struct EnvBindings {
entries: Mutex<BTreeMap<String, String>>,
}
impl EnvBindings {
#[must_use]
pub const fn new() -> Self {
Self {
entries: Mutex::new(BTreeMap::new()),
}
}
pub fn bind(&self, path: &str, variable: &str) -> Result<(), Error> {
crate::layer::check_path(path)?;
self.lock().insert(path.to_owned(), variable.to_owned());
Ok(())
}
pub fn clear(&self) {
self.lock().clear();
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.lock().is_empty()
}
#[must_use]
pub fn variable(&self, path: &str) -> Option<String> {
self.lock().get(path).cloned()
}
pub(crate) fn resolved(
&self,
allow_empty: bool,
fallback: Arc<BTreeMap<String, String>>,
) -> Vec<(String, String, crate::Value)> {
self.lock()
.iter()
.filter_map(|(path, variable)| {
resolve(variable, allow_empty, &fallback)
.map(|value| (path.clone(), variable.clone(), value))
})
.collect()
}
fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, String>> {
self.entries
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
fn resolve(
variable: &str,
allow_empty: bool,
fallback: &BTreeMap<String, String>,
) -> Option<Value> {
let from_environment = std::env::var_os(variable)
.and_then(|text| text.to_str().map(ToOwned::to_owned))
.filter(|text| allow_empty || !text.trim().is_empty());
let text = match from_environment {
Some(text) => text,
None => fallback.get(variable)?.clone(),
};
if text.trim().is_empty() && !allow_empty {
return None;
}
let text = text.as_str();
Some(crate::text_value::from_text(text))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_path_with_an_empty_segment_is_refused() {
let bindings = EnvBindings::new();
assert!(bindings.bind("pool..max", "X").is_err());
assert!(bindings.bind("", "X").is_err());
assert!(bindings.bind("pool.max", "X").is_ok());
}
#[test]
fn binding_the_same_path_twice_replaces_rather_than_layers() {
let bindings = EnvBindings::new();
bindings.bind("port", "OLD_PORT").unwrap();
bindings.bind("port", "PORT").unwrap();
assert_eq!(bindings.variable("port").as_deref(), Some("PORT"));
}
#[test]
fn clearing_removes_everything() {
let bindings = EnvBindings::new();
bindings.bind("port", "PORT").unwrap();
assert!(!bindings.is_empty());
bindings.clear();
assert!(bindings.is_empty());
}
}