cfgmatic 1.1.0

High-level configuration management framework for Rust with derive macros and validation
Documentation
//! Multi-file configuration loading with rule-based discovery.

use crate::{ConfigError, Result};
use cfgmatic_files::{ConfigFile, ConfigFiles, FileFinder, MergeOptions};
use cfgmatic_paths::{ConfigFileRule, ConfigRuleSet, PathsBuilder};
use serde::de::DeserializeOwned;

/// Loader for multi-file configurations using rule-based discovery.
///
/// Allows loading configuration from multiple files following
/// specific rules for different tiers and file patterns.
///
/// # Example
///
/// ```ignore
/// use cfgmatic::{MultiFileConfigLoader, MergeBehavior};
/// use cfgmatic_paths::{ConfigRuleSet, ConfigFileRule, TierSearchMode};
///
/// let rules = ConfigRuleSet::builder()
///     .main_file(ConfigFileRule::toml("resolve")
///         .tiers(TierSearchMode::All)
///         .required(true))
///     .main_file(ConfigFileRule::exact("main.conf")
///         .tiers(TierSearchMode::UserOnly))
///     .build();
///
/// let loader = MultiFileConfigLoader::new("myapp")
///     .rules(rules)
///     .merge_options(MergeOptions::deep());
///
/// let config: MyConfig = loader.load()?;
/// ```
#[derive(Debug, Clone)]
pub struct MultiFileConfigLoader {
    /// Application name.
    pub app_name: String,

    /// Rule set for file discovery.
    pub rules: Option<ConfigRuleSet>,

    /// Merge options.
    pub merge_options: MergeOptions,
}

impl MultiFileConfigLoader {
    /// Create a new loader for an application.
    #[must_use]
    pub fn new(app_name: impl Into<String>) -> Self {
        Self {
            app_name: app_name.into(),
            rules: None,
            merge_options: MergeOptions::new(),
        }
    }

    /// Set the rule set for file discovery.
    #[must_use]
    pub fn rules(mut self, rules: ConfigRuleSet) -> Self {
        self.rules = Some(rules);
        self
    }

    /// Set merge options.
    #[must_use]
    pub const fn merge_options(mut self, options: MergeOptions) -> Self {
        self.merge_options = options;
        self
    }

    /// Load configuration using rules.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Required files are not found
    /// - Files cannot be read or parsed
    /// - Merge fails in strict mode
    pub fn load<T>(&self) -> Result<T>
    where
        T: DeserializeOwned + Default,
    {
        let path_finder = PathsBuilder::new(&self.app_name).build();

        if let Some(rules) = &self.rules {
            // Use rule-based discovery
            let mut files = ConfigFiles::new();
            let mut has_required = false;

            for rule in &rules.main_files {
                let found = self.find_files_by_rule(&path_finder, rule)?;
                if !found.is_empty() {
                    has_required = true;
                }
                for file in found {
                    files.push(file);
                }
            }

            // Check required files
            let has_any_required = rules.main_files.iter().any(|r| r.required);
            if has_any_required && !has_required && files.is_empty() {
                return Err(ConfigError::NotFound(format!(
                    "No configuration files found for '{}'",
                    self.app_name
                )));
            }

            self.merge_files(files)
        } else {
            // Default discovery using FileFinder
            let finder = FileFinder::new(&self.app_name).build();
            let files = finder.find()?;
            self.merge_files(files)
        }
    }

    /// Find files matching a rule.
    #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
    fn find_files_by_rule(
        &self,
        finder: &cfgmatic_paths::PathFinder,
        rule: &ConfigFileRule,
    ) -> Result<Vec<ConfigFile>> {
        let mut found = Vec::new();

        for tier in rule.tiers.tiers() {
            let dirs = match tier {
                cfgmatic_paths::ConfigTier::User => finder.user_dirs(),
                cfgmatic_paths::ConfigTier::Local => finder.local_dirs(),
                cfgmatic_paths::ConfigTier::System => finder.system_dirs(),
            };

            for dir in dirs {
                if let Ok(entries) = std::fs::read_dir(&dir) {
                    for entry in entries.flatten() {
                        let path = entry.path();
                        if path.is_file()
                            && rule.pattern.matches(&path)
                            && let Some(format) = cfgmatic_files::Format::from_path(&path)
                        {
                            found.push(ConfigFile {
                                path,
                                tier,
                                format,
                                content: None,
                            });
                        }
                    }
                }
            }
        }

        Ok(found)
    }

    /// Merge configuration files.
    fn merge_files<T>(&self, files: ConfigFiles) -> Result<T>
    where
        T: DeserializeOwned + Default,
    {
        if files.is_empty() {
            return Ok(T::default());
        }

        let mut result = T::default();
        for mut file in files {
            let value: T = file.parse().map_err(|e| {
                ConfigError::Parse(format!("Failed to parse '{}': {}", file.path.display(), e))
            })?;
            result = self.merge_values(result, value)?;
        }

        Ok(result)
    }

    /// Merge two configuration values.
    #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
    fn merge_values<T>(&self, _base: T, other: T) -> Result<T>
    where
        T: DeserializeOwned + Default,
    {
        // For now, other wins (simplified)
        // In future, would use merge_options for deep merge
        Ok(other)
    }
}

/// Convenience function to load multi-file configuration.
///
/// # Errors
///
/// Returns an error if configuration cannot be loaded.
///
/// # Example
///
/// ```ignore
/// use cfgmatic::load_multi;
///
/// let config: MyConfig = load_multi("myapp").unwrap();
/// ```
pub fn load_multi<T>(app_name: impl Into<String>) -> Result<T>
where
    T: DeserializeOwned + Default,
{
    MultiFileConfigLoader::new(app_name).load()
}