noema 0.4.0

Noema IOC and DI framework for Rust
Documentation
use std::collections::HashMap;
use std::env::VarError;
use std::sync::Arc;

use async_trait::async_trait;

use super::ConfigError;

/// Runtime origin of config values (env, map, Vault, …). All methods are async
/// so a provider can perform I/O per key or in `load`.
#[async_trait]
pub trait ConfigSource: Send + Sync {
    /// Optional prefetch (download a secret bundle, read a file, warm a cache).
    async fn load(&self) -> Result<(), ConfigError>;

    /// Look up one key. `Ok(None)` means the key is absent.
    async fn get(&self, key: &str) -> Result<Option<String>, ConfigError>;

    /// `get`, or `default` when the key is absent.
    async fn get_or_default(&self, key: &str, default: String) -> Result<String, ConfigError> {
        Ok(self.get(key).await?.unwrap_or(default))
    }
}

/// Process environment (`std::env::var`).
#[derive(Debug, Default, Clone, Copy)]
pub struct EnvSource;

#[async_trait]
impl ConfigSource for EnvSource {
    async fn load(&self) -> Result<(), ConfigError> {
        Ok(())
    }

    async fn get(&self, key: &str) -> Result<Option<String>, ConfigError> {
        match std::env::var(key) {
            Ok(value) => Ok(Some(value)),
            Err(VarError::NotPresent) => Ok(None),
            Err(VarError::NotUnicode(_)) => Err(ConfigError::Source(format!(
                "environment variable {key} is not valid unicode"
            ))),
        }
    }
}

/// In-memory source (tests and simple provider stand-ins).
#[derive(Debug, Clone)]
pub struct MapSource {
    values: HashMap<String, String>,
}

impl MapSource {
    pub fn new(values: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> Self {
        Self {
            values: values
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
        }
    }
}

#[async_trait]
impl ConfigSource for MapSource {
    async fn load(&self) -> Result<(), ConfigError> {
        Ok(())
    }

    async fn get(&self, key: &str) -> Result<Option<String>, ConfigError> {
        Ok(self.values.get(key).cloned())
    }
}

#[async_trait]
impl<T: ConfigSource + ?Sized> ConfigSource for Arc<T> {
    async fn load(&self) -> Result<(), ConfigError> {
        (**self).load().await
    }

    async fn get(&self, key: &str) -> Result<Option<String>, ConfigError> {
        (**self).get(key).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use futures::executor::block_on;

    #[test]
    fn map_source_get_and_default() {
        let src = MapSource::new([("K", "v")]);
        block_on(async {
            assert_eq!(src.get("K").await.unwrap().as_deref(), Some("v"));
            assert_eq!(src.get("missing").await.unwrap(), None);
            assert_eq!(
                src.get_or_default("missing", "d".into()).await.unwrap(),
                "d"
            );
        });
    }

    #[test]
    fn env_source_absent_is_none() {
        let src = EnvSource;
        block_on(async {
            assert_eq!(
                src.get("NOEMA_TEST_CONFIG_ABSENT_KEY_XYZ").await.unwrap(),
                None
            );
        });
    }
}