use std::collections::BTreeMap;
use std::fmt;
use super::environment::{Environment, EnvironmentError, EnvironmentSnapshot};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ConfigurationLoader;
impl ConfigurationLoader {
#[must_use]
pub const fn new() -> Self {
Self
}
pub fn load_environment(
&self,
environment: &Environment,
) -> Result<LoadedConfiguration, EnvironmentError> {
let snapshot = environment.snapshot()?;
Ok(LoadedConfiguration::from_environment(snapshot))
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct LoadedConfiguration {
source: ConfigurationSource,
values: BTreeMap<String, String>,
}
impl fmt::Debug for LoadedConfiguration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LoadedConfiguration")
.field("source", &self.source)
.field("value_count", &self.values.len())
.finish()
}
}
impl LoadedConfiguration {
fn from_environment(snapshot: EnvironmentSnapshot) -> Self {
let values = snapshot
.iter()
.map(|(key, value)| (key.to_owned(), value.to_owned()))
.collect();
Self {
source: ConfigurationSource::Environment,
values,
}
}
#[must_use]
pub const fn source(&self) -> ConfigurationSource {
self.source
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
self.values.get(key).map(String::as_str)
}
#[must_use]
pub fn contains(&self, key: &str) -> bool {
self.values.contains_key(key)
}
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.values
.iter()
.map(|(key, value)| (key.as_str(), value.as_str()))
}
pub fn from_values<I, K, V>(values: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
Self {
source: ConfigurationSource::Programmatic,
values: values
.into_iter()
.map(|(key, value)| (key.into(), value.into()))
.collect(),
}
}
#[cfg(test)]
pub(crate) fn from_test_values<I, K, V>(values: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
Self {
source: ConfigurationSource::Test,
values: values
.into_iter()
.map(|(key, value)| (key.into(), value.into()))
.collect(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ConfigurationSource {
Environment,
Programmatic,
#[cfg(test)]
Test,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loader_can_be_constructed() {
assert_eq!(ConfigurationLoader::new(), ConfigurationLoader);
}
#[test]
fn environment_loading_preserves_source_identity() {
let loader = ConfigurationLoader::new();
let environment = Environment::new();
let loaded = loader
.load_environment(&environment)
.expect("environment loading should succeed");
assert_eq!(loaded.source(), ConfigurationSource::Environment);
}
#[test]
fn environment_loading_produces_owned_raw_values() {
let loader = ConfigurationLoader::new();
let environment = Environment::new();
let loaded = loader
.load_environment(&environment)
.expect("environment loading should succeed");
let entries: Vec<_> = loaded.iter().collect();
assert_eq!(loaded.len(), entries.len());
assert_eq!(loaded.is_empty(), entries.is_empty());
for pair in entries.windows(2) {
assert!(pair[0].0 <= pair[1].0);
}
if let Some((key, value)) = entries.first() {
assert!(loaded.contains(key));
assert_eq!(loaded.get(key), Some(*value));
}
}
#[test]
fn loader_preserves_environment_values_without_parsing_them() {
let loader = ConfigurationLoader::new();
let environment = Environment::new();
let loaded = loader
.load_environment(&environment)
.expect("environment loading should succeed");
let key = if cfg!(windows) { "PATH" } else { "HOME" };
if let Some(value) = loaded.get(key) {
assert!(!value.contains('\0'));
}
}
#[test]
fn programmatic_values_preserve_source_and_data() {
let loaded = LoadedConfiguration::from_values([("PORT", "8080"), ("HOST", "localhost")]);
assert_eq!(loaded.source(), ConfigurationSource::Programmatic);
assert_eq!(loaded.get("PORT"), Some("8080"));
assert_eq!(loaded.get("HOST"), Some("localhost"));
assert_eq!(loaded.len(), 2);
}
#[test]
fn loaded_configuration_debug_does_not_render_values() {
let loaded = LoadedConfiguration::from_values([("SECRET", "super-secret-value")]);
let rendered = format!("{loaded:?}");
assert!(rendered.contains("LoadedConfiguration"));
assert!(rendered.contains("Programmatic"));
assert!(rendered.contains("value_count"));
assert!(!rendered.contains("super-secret-value"));
}
#[test]
fn test_fixture_loader_preserves_values_and_marks_test_source() {
let loaded = LoadedConfiguration::from_test_values([("A", "one"), ("B", "two")]);
assert_eq!(loaded.source(), ConfigurationSource::Test);
assert_eq!(loaded.get("A"), Some("one"));
assert_eq!(loaded.get("B"), Some("two"));
}
#[test]
fn missing_keys_remain_absent() {
let loader = ConfigurationLoader::new();
let environment = Environment::new();
let loaded = loader
.load_environment(&environment)
.expect("environment loading should succeed");
let missing_key = format!("NIZAAM_LOADER_TEST_MISSING_{}", std::process::id());
assert!(!loaded.contains(&missing_key));
assert_eq!(loaded.get(&missing_key), None);
}
}