rskit-config 0.2.0-alpha.3

Adapter-oriented configuration loading with validation
Documentation
use rskit_errors::{AppError, AppResult};

use super::contract::ConfigSource;

/// Environment-variable configuration source.
#[derive(Debug, Clone, Default)]
pub struct EnvironmentSource {
    prefix: String,
}

impl EnvironmentSource {
    /// Create an environment source without a prefix.
    pub fn new() -> Self {
        Self::default()
    }

    /// Create an environment source with a prefix.
    pub fn with_prefix(prefix: impl Into<String>) -> Self {
        Self {
            prefix: prefix.into(),
        }
    }
}

impl ConfigSource for EnvironmentSource {
    fn collect(&self) -> AppResult<config::Config> {
        let env_source = if self.prefix.is_empty() {
            config::Environment::default()
                .separator("__")
                .try_parsing(true)
        } else {
            config::Environment::with_prefix(&self.prefix)
                .separator("__")
                .try_parsing(true)
        };

        config::Config::builder()
            .add_source(env_source)
            .build()
            .map_err(|e| AppError::invalid_input("config", e.to_string()))
    }
}

pub(crate) fn normalize_env_key(prefix: &str, key: &str) -> Option<String> {
    let key = key.to_ascii_lowercase();
    if prefix.is_empty() {
        return Some(key.replace("__", "."));
    }

    let prefix = prefix.to_ascii_lowercase();
    key.strip_prefix(&format!("{prefix}__"))
        .map(|stripped| stripped.replace("__", "."))
}

pub(crate) fn parse_env_value(value: String) -> config::Value {
    if let Ok(value) = value.parse::<bool>() {
        return value.into();
    }
    if let Ok(value) = value.parse::<i64>() {
        return value.into();
    }
    if let Ok(value) = value.parse::<f64>() {
        return value.into();
    }
    value.into()
}

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

    #[test]
    fn normalize_env_key_handles_prefixes_and_nested_keys() {
        assert_eq!(
            normalize_env_key("", "SERVICE__PORT").as_deref(),
            Some("service.port")
        );
        assert_eq!(
            normalize_env_key("APP", "APP__SERVICE__DEBUG").as_deref(),
            Some("service.debug")
        );
        assert_eq!(normalize_env_key("APP", "OTHER__VALUE"), None);
    }

    #[test]
    fn parse_env_value_preserves_basic_types() {
        assert!(parse_env_value("true".to_string()).into_bool().unwrap());
        assert_eq!(parse_env_value("42".to_string()).into_int().unwrap(), 42);
        assert_eq!(
            parse_env_value("1.5".to_string()).into_float().unwrap(),
            1.5
        );
        assert_eq!(
            parse_env_value("plain".to_string()).into_string().unwrap(),
            "plain"
        );
    }
}