Skip to main content

cfgmatic_paths/builder/
rules.rs

1//! Rule-based configuration discovery for [`PathFinder`](super::PathFinder).
2
3use std::path::PathBuf;
4
5use crate::core::{
6    ConfigCandidate, ConfigFileRule, ConfigRuleSet, ConfigTier, FilePattern, FragmentRule,
7    RuleBasedDiscovery, RuleMatchResult, SourceType,
8};
9use crate::env::StdEnv;
10
11use super::{PathFinder, scan};
12
13impl PathFinder {
14    /// Discover configuration files using a rule set.
15    ///
16    /// Uses the provided rule set to discover all configuration files,
17    /// including main files and fragments. Returns a detailed result
18    /// with all matched files grouped by rule.
19    ///
20    /// # Examples
21    ///
22    /// ```
23    /// use cfgmatic_paths::{ConfigFileRule, ConfigRuleSet, FragmentRule, PathsBuilder};
24    ///
25    /// let finder = PathsBuilder::new("myapp").build();
26    ///
27    /// let rules = ConfigRuleSet::builder()
28    ///     .main_file(ConfigFileRule::toml("config").required(true))
29    ///     .main_file(ConfigFileRule::extensions("config", &["yaml"]))
30    ///     .fragments(FragmentRule::new("conf.d", "*.conf"))
31    ///     .build();
32    ///
33    /// let discovery = finder.discover_with_rules(&rules);
34    ///
35    /// for result in &discovery.main_files {
36    ///     println!("Rule: {:?}", result.rule.pattern);
37    ///     for found in &result.matches {
38    ///         println!("  Found: {:?}", found.path);
39    ///     }
40    /// }
41    /// ```
42    #[must_use]
43    pub fn discover_with_rules(&self, rules: &ConfigRuleSet) -> RuleBasedDiscovery {
44        let mut main_files = Vec::new();
45        let mut fragments = Vec::new();
46
47        for rule in &rules.main_files {
48            let matches = self.find_by_rule(rule);
49            main_files.push(RuleMatchResult {
50                rule: rule.clone(),
51                matches,
52            });
53        }
54
55        if let Some(fragment_rule) = &rules.fragments {
56            fragments = self.find_fragments_by_rule(fragment_rule);
57        }
58
59        RuleBasedDiscovery {
60            rules: rules.clone(),
61            main_files,
62            fragments,
63        }
64    }
65
66    /// Find all files matching a single rule.
67    fn find_by_rule(&self, rule: &ConfigFileRule) -> Vec<ConfigCandidate> {
68        let mut candidates = Vec::new();
69
70        for tier in rule.tiers.tiers() {
71            let dirs = self.directories_for_tier(tier);
72            let source_type = if tier == ConfigTier::User {
73                SourceType::MainFile
74            } else {
75                SourceType::Legacy
76            };
77
78            for dir in dirs {
79                self.find_files_in_dirs_with_rule(
80                    &[dir],
81                    tier,
82                    &rule.pattern,
83                    &mut candidates,
84                    source_type,
85                );
86            }
87        }
88
89        candidates.sort_by(|a, b| b.tier.cmp(&a.tier));
90        candidates
91    }
92
93    /// Find files matching a pattern in directories with a specific source type.
94    fn find_files_in_dirs_with_rule(
95        &self,
96        dirs: &[PathBuf],
97        tier: ConfigTier,
98        pattern: &FilePattern,
99        candidates: &mut Vec<ConfigCandidate>,
100        source_type: SourceType,
101    ) {
102        candidates.extend(scan::collect_matching_candidates(
103            self.fs.as_ref(),
104            dirs,
105            tier,
106            pattern,
107            source_type,
108            |path| self.path_status(path),
109        ));
110    }
111
112    /// Find fragment files by a fragment rule.
113    fn find_fragments_by_rule(&self, rule: &FragmentRule) -> Vec<RuleMatchResult> {
114        let mut results = Vec::new();
115        let file_rule = ConfigFileRule {
116            pattern: rule.pattern.clone(),
117            tiers: rule.tiers,
118            required: false,
119        };
120
121        for tier in rule.tiers.tiers() {
122            let dirs = self.directories_for_tier(tier);
123
124            for base_dir in dirs {
125                let conf_d = base_dir.join(&rule.dir_name);
126                if self.fs.is_dir(&conf_d) {
127                    let mut matches = Vec::new();
128
129                    for entry in self.fs.read_dir(&conf_d) {
130                        if rule.pattern.matches(&entry) && self.fs.is_file(&entry) {
131                            let status = self.path_status(&entry);
132                            matches.push(ConfigCandidate::new(
133                                entry.clone(),
134                                status,
135                                tier,
136                                SourceType::FragmentsDir,
137                            ));
138                        }
139                    }
140
141                    if !matches.is_empty() {
142                        results.push(RuleMatchResult {
143                            rule: file_rule.clone(),
144                            matches,
145                        });
146                    }
147                }
148            }
149        }
150
151        results
152    }
153
154    /// Resolve directories for a configuration tier.
155    fn directories_for_tier(&self, tier: ConfigTier) -> Vec<PathBuf> {
156        match tier {
157            ConfigTier::User => self.dir_finder.user_dirs(&StdEnv),
158            ConfigTier::Local => self.dir_finder.local_dirs(&StdEnv),
159            ConfigTier::System => self.dir_finder.system_dirs(&StdEnv),
160        }
161    }
162}