cfgmatic 1.1.0

High-level configuration management framework for Rust with derive macros and validation
Documentation
//! Environment variable configuration support.
//!
//! This module provides traits and utilities for loading configuration
//! from environment variables using NPM-style naming conventions.

#![allow(
    clippy::missing_errors_doc,
    clippy::missing_docs_in_private_items,
    clippy::option_if_let_else
)]

use crate::error::{ConfigError, ConfigFields, Result};

/// Trait for types that can be loaded from environment variables.
///
/// This is the main trait implemented by the `Config` derive macro.
///
/// # Example
///
/// ```
/// use cfgmatic::{Config, ConfigLoader};
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize, Config)]
/// #[config(prefix = "MYAPP")]
/// struct AppConfig {
///     #[config(default = "8080")]
///     port: u16,
/// }
///
/// // Load from environment
/// // std::env::set_var("MYAPP__PORT", "3000");
/// // let config = AppConfig::from_env().unwrap();
/// ```
pub trait ConfigLoader: ConfigFields + Default {
    /// Load configuration from environment variables.
    ///
    /// Uses the struct's prefix and field defaults to populate values.
    ///
    /// # Errors
    ///
    /// Returns an error if environment variables cannot be parsed.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::{Config, ConfigLoader};
    /// use serde::Deserialize;
    ///
    /// #[derive(Debug, Deserialize, Config, Default)]
    /// #[config(prefix = "TEST")]
    /// struct Config {
    ///     #[config(default = "100")]
    ///     value: u32,
    /// }
    ///
    /// let config = Config::from_env().unwrap();
    /// ```
    fn from_env() -> Result<Self> {
        let mut config = Self::default();
        config.load_from_env(None)?;
        Ok(config)
    }

    /// Load configuration with a custom prefix.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::{Config, ConfigLoader};
    /// use serde::Deserialize;
    ///
    /// #[derive(Debug, Deserialize, Config, Default)]
    /// struct Config {
    ///     #[config(default = "100")]
    ///     value: u32,
    /// }
    ///
    /// // Load with custom prefix
    /// let config = Config::from_env_with_prefix("CUSTOM").unwrap();
    /// ```
    fn from_env_with_prefix(prefix: &str) -> Result<Self> {
        let mut config = Self::default();
        config.load_from_env(Some(prefix))?;
        Ok(config)
    }

    /// Load configuration from a specific environment variable.
    ///
    /// The value should be a JSON/TOML-like representation of the config.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::{Config, ConfigLoader};
    /// use serde::Deserialize;
    ///
    /// #[derive(Debug, Deserialize, Default)]
    /// struct Config {
    ///     value: u32,
    /// }
    ///
    /// // std::env::set_var("MY_CONFIG", r#"{"value": 42}"#);
    /// // let config = Config::from_env_var("MY_CONFIG").unwrap();
    /// ```
    fn from_env_var(var_name: &str) -> Result<Self>
    where
        Self: serde::de::DeserializeOwned,
    {
        let value = std::env::var(var_name)
            .map_err(|_| ConfigError::NotFound(format!("environment variable {var_name}")))?;

        // Try JSON first
        if let Ok(config) = serde_json::from_str::<Self>(&value) {
            return Ok(config);
        }

        // Then try TOML
        if let Ok(config) = toml::from_str::<Self>(&value) {
            return Ok(config);
        }

        Err(ConfigError::Parse(format!(
            "Failed to parse {var_name} as JSON or TOML"
        )))
    }
}

impl<T: ConfigFields + Default> ConfigLoader for T {}

/// Trait for types that provide environment variable mappings.
///
/// This is implemented by the `ConfigEnv` derive macro.
///
/// # Example
///
/// ```
/// use cfgmatic::{ConfigEnv, EnvConfig};
///
/// #[derive(ConfigEnv)]
/// #[config_env(prefix = "MYAPP")]
/// struct AppConfig {
///     #[config_env(name = "SERVER_PORT")]
///     port: u16,
/// }
///
/// let mappings = AppConfig::env_mappings();
/// ```
pub trait EnvConfig {
    /// Get a list of field names to environment variable names.
    ///
    /// Returns a vector of (`field_name`, `env_var_name`) tuples.
    fn env_mappings() -> Vec<(&'static str, &'static str)>;

    /// Get the prefix for environment variables.
    fn env_prefix() -> &'static str;
}

/// Environment variable loader with builder pattern.
///
/// Provides a flexible way to load configuration from environment variables.
///
/// # Example
///
/// ```
/// use cfgmatic::EnvLoader;
///
/// let loader = EnvLoader::new()
///     .prefix("MYAPP")
///     .separator("__");
/// ```
#[derive(Debug, Clone)]
pub struct EnvLoader {
    prefix: String,
    separator: String,
}

impl Default for EnvLoader {
    fn default() -> Self {
        Self {
            prefix: String::new(),
            separator: "__".to_string(),
        }
    }
}

impl EnvLoader {
    /// Create a new environment loader.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::EnvLoader;
    ///
    /// let loader = EnvLoader::new();
    /// ```
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the prefix for environment variables.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::EnvLoader;
    ///
    /// let loader = EnvLoader::new().prefix("MYAPP");
    /// ```
    #[must_use]
    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
        self.prefix = prefix.into();
        self
    }

    /// Set the separator for nested fields.
    ///
    /// Default is double underscore (`__`).
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::EnvLoader;
    ///
    /// let loader = EnvLoader::new().separator("_");
    /// ```
    #[must_use]
    pub fn separator(mut self, separator: impl Into<String>) -> Self {
        self.separator = separator.into();
        self
    }

    /// Build an environment variable name from path components.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::EnvLoader;
    ///
    /// let loader = EnvLoader::new().prefix("MYAPP");
    /// let name = loader.build_name(&["database", "url"]);
    /// assert_eq!(name, "MYAPP__DATABASE__URL");
    /// ```
    #[must_use]
    pub fn build_name(&self, path: &[&str]) -> String {
        let mut parts = Vec::with_capacity(path.len() + 1);
        if !self.prefix.is_empty() {
            parts.push(self.prefix.to_uppercase());
        }
        parts.extend(path.iter().map(|s| s.to_uppercase()));
        parts.join(&self.separator)
    }

    /// Load a value from environment variables.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::EnvLoader;
    ///
    /// let loader = EnvLoader::new().prefix("MYAPP");
    /// // std::env::set_var("MYAPP__PORT", "8080");
    /// // let port: u16 = loader.load(&["port"]).unwrap();
    /// ```
    pub fn load<T: std::str::FromStr>(&self, path: &[&str]) -> Result<T>
    where
        T::Err: std::fmt::Display,
    {
        let name = self.build_name(path);
        let value = std::env::var(&name)
            .map_err(|_| ConfigError::NotFound(format!("environment variable {name}")))?;

        value
            .parse()
            .map_err(|e| ConfigError::Parse(format!("Failed to parse {name}: {e}")))
    }

    /// Try to load a value from environment variables.
    ///
    /// Returns `Ok(None)` if the variable is not set.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::EnvLoader;
    ///
    /// let loader = EnvLoader::new().prefix("MYAPP");
    /// // let port: Option<u16> = loader.try_load(&["port"]).unwrap();
    /// ```
    pub fn try_load<T: std::str::FromStr>(&self, path: &[&str]) -> Result<Option<T>>
    where
        T::Err: std::fmt::Display,
    {
        let name = self.build_name(path);
        match std::env::var(&name) {
            Ok(value) => value
                .parse()
                .map(Some)
                .map_err(|e| ConfigError::Parse(format!("Failed to parse {name}: {e}"))),
            Err(_) => Ok(None),
        }
    }
}