cfgmatic-paths 5.0.1

Cross-platform configuration path discovery following XDG and platform conventions
Documentation
//! Rule-based configuration discovery for [`PathFinder`](super::PathFinder).

use std::path::PathBuf;

use crate::core::{
    ConfigCandidate, ConfigFileRule, ConfigRuleSet, ConfigTier, FilePattern, FragmentRule,
    RuleBasedDiscovery, RuleMatchResult, SourceType,
};
use crate::env::StdEnv;

use super::{PathFinder, scan};

impl PathFinder {
    /// Discover configuration files using a rule set.
    ///
    /// Uses the provided rule set to discover all configuration files,
    /// including main files and fragments. Returns a detailed result
    /// with all matched files grouped by rule.
    ///
    /// # Examples
    ///
    /// ```
    /// use cfgmatic_paths::{ConfigFileRule, ConfigRuleSet, FragmentRule, PathsBuilder};
    ///
    /// let finder = PathsBuilder::new("myapp").build();
    ///
    /// let rules = ConfigRuleSet::builder()
    ///     .main_file(ConfigFileRule::toml("config").required(true))
    ///     .main_file(ConfigFileRule::extensions("config", &["yaml"]))
    ///     .fragments(FragmentRule::new("conf.d", "*.conf"))
    ///     .build();
    ///
    /// let discovery = finder.discover_with_rules(&rules);
    ///
    /// for result in &discovery.main_files {
    ///     println!("Rule: {:?}", result.rule.pattern);
    ///     for found in &result.matches {
    ///         println!("  Found: {:?}", found.path);
    ///     }
    /// }
    /// ```
    #[must_use]
    pub fn discover_with_rules(&self, rules: &ConfigRuleSet) -> RuleBasedDiscovery {
        let mut main_files = Vec::new();
        let mut fragments = Vec::new();

        for rule in &rules.main_files {
            let matches = self.find_by_rule(rule);
            main_files.push(RuleMatchResult {
                rule: rule.clone(),
                matches,
            });
        }

        if let Some(fragment_rule) = &rules.fragments {
            fragments = self.find_fragments_by_rule(fragment_rule);
        }

        RuleBasedDiscovery {
            rules: rules.clone(),
            main_files,
            fragments,
        }
    }

    /// Find all files matching a single rule.
    fn find_by_rule(&self, rule: &ConfigFileRule) -> Vec<ConfigCandidate> {
        let mut candidates = Vec::new();

        for tier in rule.tiers.tiers() {
            let dirs = self.directories_for_tier(tier);
            let source_type = if tier == ConfigTier::User {
                SourceType::MainFile
            } else {
                SourceType::Legacy
            };

            for dir in dirs {
                self.find_files_in_dirs_with_rule(
                    &[dir],
                    tier,
                    &rule.pattern,
                    &mut candidates,
                    source_type,
                );
            }
        }

        candidates.sort_by(|a, b| b.tier.cmp(&a.tier));
        candidates
    }

    /// Find files matching a pattern in directories with a specific source type.
    fn find_files_in_dirs_with_rule(
        &self,
        dirs: &[PathBuf],
        tier: ConfigTier,
        pattern: &FilePattern,
        candidates: &mut Vec<ConfigCandidate>,
        source_type: SourceType,
    ) {
        candidates.extend(scan::collect_matching_candidates(
            self.fs.as_ref(),
            dirs,
            tier,
            pattern,
            source_type,
            |path| self.path_status(path),
        ));
    }

    /// Find fragment files by a fragment rule.
    fn find_fragments_by_rule(&self, rule: &FragmentRule) -> Vec<RuleMatchResult> {
        let mut results = Vec::new();
        let file_rule = ConfigFileRule {
            pattern: rule.pattern.clone(),
            tiers: rule.tiers,
            required: false,
        };

        for tier in rule.tiers.tiers() {
            let dirs = self.directories_for_tier(tier);

            for base_dir in dirs {
                let conf_d = base_dir.join(&rule.dir_name);
                if self.fs.is_dir(&conf_d) {
                    let mut matches = Vec::new();

                    for entry in self.fs.read_dir(&conf_d) {
                        if rule.pattern.matches(&entry) && self.fs.is_file(&entry) {
                            let status = self.path_status(&entry);
                            matches.push(ConfigCandidate::new(
                                entry.clone(),
                                status,
                                tier,
                                SourceType::FragmentsDir,
                            ));
                        }
                    }

                    if !matches.is_empty() {
                        results.push(RuleMatchResult {
                            rule: file_rule.clone(),
                            matches,
                        });
                    }
                }
            }
        }

        results
    }

    /// Resolve directories for a configuration tier.
    fn directories_for_tier(&self, tier: ConfigTier) -> Vec<PathBuf> {
        match tier {
            ConfigTier::User => self.dir_finder.user_dirs(&StdEnv),
            ConfigTier::Local => self.dir_finder.local_dirs(&StdEnv),
            ConfigTier::System => self.dir_finder.system_dirs(&StdEnv),
        }
    }
}