cfgmatic 1.1.0

High-level configuration management framework for Rust with derive macros and validation
Documentation
//! Configuration manager for loading and accessing configuration.

#![allow(clippy::missing_docs_in_private_items)]

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

#[cfg(feature = "files")]
use cfgmatic_files::{FileFinder, load_first};

use cfgmatic_paths::{
    ConfigCandidate, ConfigDiscovery, DiscoveryOptions, FilePattern, PathsBuilder,
};

/// Result of loading configuration with discovery information.
///
/// Contains both the loaded configuration and diagnostic information
/// about where it was found and what other locations were searched.
#[derive(Debug)]
pub struct ConfigLoadResult<T> {
    /// The loaded configuration value.
    pub config: T,
    /// Discovery information about configuration locations.
    pub discovery: ConfigDiscovery,
}

/// Main configuration manager.
///
/// This is the primary entry point for loading configuration
/// from files and environment variables.
///
/// # Example
///
/// ```
/// use cfgmatic::ConfigManager;
///
/// let manager = ConfigManager::new("myapp");
/// // let config = manager.load::<serde_json::Value>();
/// ```
#[derive(Debug, Clone)]
pub struct ConfigManager {
    app_name: String,
    #[cfg(feature = "files")]
    finder: FileFinder,
    env_prefix: Option<String>,
    use_env: bool,
}

impl ConfigManager {
    /// Create a new configuration manager for the given application.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::ConfigManager;
    ///
    /// let manager = ConfigManager::new("myapp");
    /// ```
    pub fn new(app_name: impl Into<String>) -> Self {
        let app_name = app_name.into();
        Self {
            #[cfg(feature = "files")]
            finder: FileFinder::new(&app_name),
            app_name,
            env_prefix: None,
            use_env: true,
        }
    }

    /// Set the environment variable prefix.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::ConfigManager;
    ///
    /// let manager = ConfigManager::new("myapp")
    ///     .with_env_prefix("MYAPP");
    /// ```
    #[must_use]
    pub fn with_env_prefix(mut self, prefix: impl Into<String>) -> Self {
        self.env_prefix = Some(prefix.into());
        self
    }

    /// Enable or disable environment variable loading.
    ///
    /// Default is `true`.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::ConfigManager;
    ///
    /// let manager = ConfigManager::new("myapp")
    ///     .with_env(false);
    /// ```
    #[must_use]
    pub const fn with_env(mut self, use_env: bool) -> Self {
        self.use_env = use_env;
        self
    }

    /// Load configuration from the first available source.
    ///
    /// First tries to load from files, then applies environment variable overrides.
    ///
    /// # Errors
    ///
    /// Returns an error if no configuration is found.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::ConfigManager;
    ///
    /// let manager = ConfigManager::new("myapp");
    /// // let config = manager.load::<serde_json::Value>();
    /// ```
    #[cfg(feature = "files")]
    pub fn load<T>(&self) -> Result<T>
    where
        T: serde::de::DeserializeOwned + ConfigFields + Default,
    {
        let mut config: T = load_first::<T>(&self.app_name)
            .map_err(ConfigError::File)?
            .unwrap_or_default();

        if self.use_env {
            config.load_from_env(self.env_prefix.as_deref())?;
        }

        Ok(config)
    }

    /// Load configuration with discovery information.
    ///
    /// Returns both the loaded configuration and detailed diagnostic
    /// information about where configuration was found and what locations
    /// were searched.
    ///
    /// # Errors
    ///
    /// Returns an error if configuration cannot be loaded.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use cfgmatic::ConfigManager;
    ///
    /// let manager = ConfigManager::new("myapp");
    /// match manager.load_with_discovery::<MyConfig>() {
    ///     Ok(result) => {
    ///         println!("Loaded config");
    ///         println!("Preferred location: {}", result.discovery.preferred_path.display());
    ///         if let Some(found) = &result.discovery.found_path {
    ///             println!("Found at: {}", found.display());
    ///         }
    ///         for candidate in result.discovery.candidates {
    ///             println!("  - {:?}: {} ({:?})",
    ///                 candidate.tier,
    ///                 candidate.path.display(),
    ///                 candidate.status
    ///             );
    ///         }
    ///     }
    ///     Err(e) => eprintln!("Error: {}", e),
    /// }
    /// ```
    #[cfg(feature = "files")]
    pub fn load_with_discovery<T>(&self) -> Result<ConfigLoadResult<T>>
    where
        T: serde::de::DeserializeOwned + ConfigFields + Default,
    {
        let discovery = self.discover();
        let mut config: T = load_first::<T>(&self.app_name)
            .map_err(ConfigError::File)?
            .unwrap_or_default();

        if self.use_env {
            config.load_from_env(self.env_prefix.as_deref())?;
        }

        Ok(ConfigLoadResult { config, discovery })
    }

    /// Discover configuration locations without loading.
    ///
    /// Returns comprehensive diagnostic information about configuration
    /// locations, including what exists and where new configuration
    /// would be created.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::ConfigManager;
    ///
    /// let manager = ConfigManager::new("myapp");
    /// let discovery = manager.discover();
    ///
    /// println!("Config would be at: {}", discovery.preferred_path.display());
    /// for candidate in discovery.candidates {
    ///     println!("  - {} ({:?})", candidate.path.display(), candidate.status);
    /// }
    /// ```
    #[must_use]
    pub fn discover(&self) -> ConfigDiscovery {
        PathsBuilder::new(&self.app_name).build().discover_config()
    }

    /// Discover configuration with custom options.
    ///
    /// Allows customization of the discovery process including
    /// file patterns and fragment discovery.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::{ConfigManager, DiscoveryOptions, FilePattern};
    ///
    /// let manager = ConfigManager::new("myapp");
    ///
    /// let options = DiscoveryOptions::new()
    ///     .with_pattern(FilePattern::extensions("config", &["toml", "yaml"]))
    ///     .with_fragments(true);
    ///
    /// let discovery = manager.discover_with_options(&options);
    /// ```
    #[must_use]
    pub fn discover_with_options(&self, options: &DiscoveryOptions) -> ConfigDiscovery {
        PathsBuilder::new(&self.app_name)
            .build()
            .discover_config_with_options(options)
    }

    /// Get the preferred configuration path.
    ///
    /// Returns the path where configuration would be created,
    /// without checking if it exists.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::ConfigManager;
    ///
    /// let manager = ConfigManager::new("myapp");
    /// let path = manager.config_path();
    /// println!("Config location: {}", path.display());
    /// ```
    #[must_use]
    pub fn config_path(&self) -> std::path::PathBuf {
        PathsBuilder::new(&self.app_name)
            .build()
            .preferred_config_path()
    }

    /// Get the preferred configuration file path.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::ConfigManager;
    ///
    /// let manager = ConfigManager::new("myapp");
    /// let path = manager.config_file("config.toml");
    /// println!("Config file: {}", path.display());
    /// ```
    #[must_use]
    pub fn config_file(&self, filename: impl AsRef<std::path::Path>) -> std::path::PathBuf {
        PathsBuilder::new(&self.app_name)
            .build()
            .preferred_config_file(filename)
    }

    /// Find all configuration files matching a pattern.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::{ConfigManager, FilePattern};
    ///
    /// let manager = ConfigManager::new("myapp");
    /// let pattern = FilePattern::extensions("config", &["toml", "yaml", "json"]);
    ///
    /// let configs = manager.find_config_files(&pattern);
    /// for config in configs {
    ///     if config.exists() {
    ///         println!("Found: {}", config.path.display());
    ///     }
    /// }
    /// ```
    #[must_use]
    pub fn find_config_files(&self, pattern: &FilePattern) -> Vec<ConfigCandidate> {
        PathsBuilder::new(&self.app_name)
            .build()
            .find_config_files(pattern)
    }

    /// Find configuration fragments (conf.d-style).
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::{ConfigManager, FilePattern};
    ///
    /// let manager = ConfigManager::new("myapp");
    /// let fragments = manager.find_fragments(&FilePattern::glob("*.conf"), "conf.d");
    ///
    /// for fragment in fragments {
    ///     println!("Fragment: {}", fragment.display());
    /// }
    /// ```
    #[must_use]
    pub fn find_fragments(
        &self,
        pattern: &FilePattern,
        fragment_dir_name: &str,
    ) -> Vec<std::path::PathBuf> {
        PathsBuilder::new(&self.app_name)
            .build()
            .find_fragments(pattern, fragment_dir_name)
    }

    /// Load configuration from files only.
    ///
    /// # Errors
    ///
    /// Returns an error if no configuration file is found.
    #[cfg(feature = "files")]
    pub fn load_from_files<T>(&self) -> Result<T>
    where
        T: serde::de::DeserializeOwned,
    {
        load_first::<T>(&self.app_name)
            .map_err(ConfigError::File)?
            .ok_or_else(|| ConfigError::NotFound(self.app_name.clone()))
    }

    /// Load configuration from environment variables only.
    ///
    /// # Errors
    ///
    /// Returns an error if environment variables cannot be parsed.
    pub fn load_from_env<T>(&self) -> Result<T>
    where
        T: ConfigFields + Default,
    {
        let mut config = T::default();
        config.load_from_env(self.env_prefix.as_deref())?;
        Ok(config)
    }

    /// Check if configuration exists for this application.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::ConfigManager;
    ///
    /// let manager = ConfigManager::new("myapp");
    /// if manager.exists() {
    ///     println!("Configuration found!");
    /// }
    /// ```
    #[cfg(feature = "files")]
    #[must_use]
    pub fn exists(&self) -> bool {
        self.finder.clone().find_first().ok().flatten().is_some()
    }

    /// Get the application name.
    ///
    /// # Example
    ///
    /// ```
    /// use cfgmatic::ConfigManager;
    ///
    /// let manager = ConfigManager::new("myapp");
    /// assert_eq!(manager.app_name(), "myapp");
    /// ```
    #[must_use]
    pub fn app_name(&self) -> &str {
        &self.app_name
    }
}