use std::collections::BTreeMap;
use std::sync::Mutex;
use figment::value::{Dict, Value};
use figment::{Metadata, Profile, Provider};
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();
}
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 providers(&self, key: &str) -> Vec<BindingProvider> {
self.lock()
.iter()
.map(|(path, variable)| BindingProvider {
path: path.clone(),
variable: variable.clone(),
key: key.to_owned(),
})
.collect()
}
fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, String>> {
self.entries
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
pub(crate) const BINDING_PREFIX: &str = "the environment variable ";
pub(crate) struct BindingProvider {
path: String,
variable: String,
key: String,
}
impl Provider for BindingProvider {
fn metadata(&self) -> Metadata {
Metadata::named(format!("{BINDING_PREFIX}{}", self.variable))
}
fn data(&self) -> figment::Result<figment::value::Map<Profile, Dict>> {
let mut values = Dict::new();
if let Some(value) = resolve(&self.variable) {
crate::layer::insert_path(&mut values, &self.path, value);
}
let mut map = figment::value::Map::new();
map.insert(Profile::from(self.key.clone()), values);
Ok(map)
}
}
fn resolve(variable: &str) -> Option<Value> {
let text = std::env::var_os(variable)?;
let text = text.to_str()?;
if text.is_empty() {
return None;
}
Some(
text.parse::<Value>()
.unwrap_or_else(|_| Value::from(text.to_owned())),
)
}
#[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());
}
}