Skip to main content

cfgmatic_paths/core/
rules.rs

1//! Configuration file rules for multi-file configuration discovery.
2
3use crate::core::{config_tier::ConfigTier, pattern::FilePattern};
4
5/// Rule for finding a single configuration file.
6///
7/// Defines how to search for a specific configuration file across
8/// different tiers (User, Local, System) and what to do when
9/// multiple instances are found.
10///
11/// # Example
12///
13/// ```
14/// use cfgmatic_paths::{ConfigFileRule, TierSearchMode};
15///
16/// let rule = ConfigFileRule::extensions("config", &["toml", "yaml"])
17///     .tiers(TierSearchMode::All)
18///     .required(true);
19/// ```
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ConfigFileRule {
22    /// File pattern to match.
23    pub pattern: FilePattern,
24
25    /// Which tiers to search for this file.
26    pub tiers: TierSearchMode,
27
28    /// Whether the file is required (error if not found).
29    pub required: bool,
30}
31
32impl ConfigFileRule {
33    /// Create a new rule for a file with a single extension.
34    ///
35    /// # Example
36    ///
37    /// ```
38    /// use cfgmatic_paths::ConfigFileRule;
39    ///
40    /// let rule = ConfigFileRule::toml("config");
41    /// ```
42    #[must_use]
43    pub fn toml(base: impl Into<String>) -> Self {
44        Self {
45            pattern: FilePattern::extensions(base.into(), &["toml"]),
46            tiers: TierSearchMode::default(),
47            required: false,
48        }
49    }
50
51    /// Create a new rule for a file with multiple extensions.
52    ///
53    /// # Example
54    ///
55    /// ```
56    /// use cfgmatic_paths::ConfigFileRule;
57    ///
58    /// let rule = ConfigFileRule::extensions("config", &["toml", "yaml", "json"]);
59    /// ```
60    #[must_use]
61    pub fn extensions(base: impl Into<String>, extensions: &[&str]) -> Self {
62        Self {
63            pattern: FilePattern::extensions(base.into(), extensions),
64            tiers: TierSearchMode::default(),
65            required: false,
66        }
67    }
68
69    /// Create a new rule for an exact filename match.
70    ///
71    /// # Example
72    ///
73    /// ```
74    /// use cfgmatic_paths::ConfigFileRule;
75    ///
76    /// let rule = ConfigFileRule::exact("main.conf");
77    /// ```
78    #[must_use]
79    pub fn exact(name: impl Into<String>) -> Self {
80        Self {
81            pattern: FilePattern::exact(name.into()),
82            tiers: TierSearchMode::default(),
83            required: false,
84        }
85    }
86
87    /// Create a new rule for glob pattern matching.
88    ///
89    /// # Example
90    ///
91    /// ```
92    /// use cfgmatic_paths::ConfigFileRule;
93    ///
94    /// let rule = ConfigFileRule::glob("*.conf");
95    /// ```
96    #[must_use]
97    pub fn glob(pattern: impl Into<String>) -> Self {
98        Self {
99            pattern: FilePattern::glob(pattern.into()),
100            tiers: TierSearchMode::default(),
101            required: false,
102        }
103    }
104
105    /// Set which tiers to search.
106    ///
107    /// # Example
108    ///
109    /// ```
110    /// use cfgmatic_paths::{ConfigFileRule, TierSearchMode, ConfigTier};
111    ///
112    /// let rule = ConfigFileRule::toml("config")
113    ///     .tiers(TierSearchMode::FromTier(ConfigTier::System));
114    /// ```
115    #[must_use]
116    pub const fn tiers(mut self, tiers: TierSearchMode) -> Self {
117        self.tiers = tiers;
118        self
119    }
120
121    /// Mark the file as required.
122    ///
123    /// # Example
124    ///
125    /// ```
126    /// use cfgmatic_paths::ConfigFileRule;
127    ///
128    /// let rule = ConfigFileRule::toml("config")
129    ///     .required(true);
130    /// ```
131    #[must_use]
132    pub const fn required(mut self, required: bool) -> Self {
133        self.required = required;
134        self
135    }
136}
137
138/// How to search tiers for configuration files.
139///
140/// Defines which configuration tiers (User, Local, System) should be searched
141/// and in what order.
142///
143/// # Example
144///
145/// ```
146/// use cfgmatic_paths::{TierSearchMode, ConfigTier};
147///
148/// // Search all tiers (default)
149/// let mode = TierSearchMode::All;
150///
151/// // Only user tier
152/// let mode = TierSearchMode::UserOnly;
153///
154/// // User and Local tiers
155/// let mode = TierSearchMode::UserAndLocal;
156///
157/// // From a specific tier upward
158/// let mode = TierSearchMode::FromTier(ConfigTier::System);
159/// ```
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
161pub enum TierSearchMode {
162    /// Only User tier.
163    UserOnly,
164
165    /// User and Local tiers.
166    UserAndLocal,
167
168    /// All tiers: User, Local, System (default).
169    #[default]
170    All,
171
172    /// From a specific tier upward (e.g., FromTier(System) means System, Local, User).
173    FromTier(ConfigTier),
174
175    /// From a specific tier downward (e.g., FromTier(User) means User only).
176    FromTierDownward(ConfigTier),
177
178    /// Custom selection of tiers.
179    Custom {
180        /// Include User tier.
181        user: bool,
182        /// Include Local tier.
183        local: bool,
184        /// Include System tier.
185        system: bool,
186    },
187}
188
189impl TierSearchMode {
190    /// Returns true if the User tier should be searched.
191    #[must_use]
192    pub const fn includes_user(&self) -> bool {
193        matches!(
194            self,
195            Self::UserOnly | Self::UserAndLocal | Self::All | Self::FromTierDownward(_)
196        ) || matches!(self, Self::Custom { user: true, .. })
197            || matches!(self, Self::FromTier(ConfigTier::User))
198    }
199
200    /// Returns true if the Local tier should be searched.
201    #[must_use]
202    pub const fn includes_local(&self) -> bool {
203        matches!(self, Self::All | Self::FromTier(ConfigTier::Local))
204            || matches!(self, Self::Custom { local: true, .. })
205            || matches!(self, Self::FromTier(ConfigTier::System | ConfigTier::User))
206    }
207
208    /// Returns true if the System tier should be searched.
209    #[must_use]
210    pub const fn includes_system(&self) -> bool {
211        matches!(self, Self::All)
212            || matches!(self, Self::Custom { system: true, .. })
213            || matches!(self, Self::FromTier(ConfigTier::System))
214    }
215
216    /// Returns an iterator of tiers to search in priority order (User → Local → System).
217    #[must_use]
218    pub const fn tiers(&self) -> TierIterator {
219        TierIterator::new(*self)
220    }
221
222    /// Returns the default tier for this mode.
223    #[must_use]
224    pub const fn default_tier(&self) -> ConfigTier {
225        match self {
226            Self::FromTier(tier) => *tier,
227            _ => ConfigTier::User,
228        }
229    }
230}
231
232/// Iterator over configuration tiers in priority order.
233///
234/// Yields tiers in the order they should be searched and merged
235/// (highest priority first).
236#[derive(Debug, Clone)]
237pub struct TierIterator {
238    /// Search mode determining which tiers to iterate.
239    mode: TierSearchMode,
240    /// Current iteration state.
241    state: u8,
242}
243
244impl TierIterator {
245    /// Create a new iterator for the given mode.
246    #[must_use]
247    pub const fn new(mode: TierSearchMode) -> Self {
248        Self { mode, state: 0 }
249    }
250}
251
252impl Iterator for TierIterator {
253    type Item = ConfigTier;
254
255    fn next(&mut self) -> Option<Self::Item> {
256        let mode = self.mode;
257        let state = self.state;
258        self.state = state.wrapping_add(1);
259
260        match mode {
261            // AllTiers: User → Local → System
262            TierSearchMode::All => match state {
263                0 => Some(ConfigTier::User),
264                1 => Some(ConfigTier::Local),
265                2 => Some(ConfigTier::System),
266                _ => None,
267            },
268
269            // UserOnly
270            TierSearchMode::UserOnly => match state {
271                0 => Some(ConfigTier::User),
272                _ => None,
273            },
274
275            // UserAndLocal: User → Local
276            TierSearchMode::UserAndLocal => match state {
277                0 => Some(ConfigTier::User),
278                1 => Some(ConfigTier::Local),
279                _ => None,
280            },
281
282            // FromTier: tier → tiers above (in priority order: User → Local → System)
283            TierSearchMode::FromTier(start) => match state {
284                0 => Some(start),
285                1 if start < ConfigTier::User => Some(ConfigTier::User),
286                1 if start < ConfigTier::Local => Some(ConfigTier::Local),
287                _ => None,
288            },
289
290            // FromTierDownward: tier → tiers below (in reverse priority: System → Local → User)
291            TierSearchMode::FromTierDownward(start) => match state {
292                0 => Some(start),
293                1 if start > ConfigTier::System => Some(ConfigTier::System),
294                1 if start > ConfigTier::Local => Some(ConfigTier::Local),
295                _ => None,
296            },
297
298            // Custom
299            TierSearchMode::Custom {
300                user,
301                local,
302                system,
303            } => match state {
304                0 if user => Some(ConfigTier::User),
305                1 if local => Some(ConfigTier::Local),
306                2 if system => Some(ConfigTier::System),
307                _ => None,
308            },
309        }
310    }
311}
312
313/// Rule for configuration fragment directories (conf.d style).
314///
315/// Fragment directories contain multiple small configuration files
316/// that are merged together.
317///
318/// # Example
319///
320/// ```
321/// use cfgmatic_paths::{FragmentRule, TierSearchMode};
322///
323/// let rule = FragmentRule::new("conf.d", "*.conf")
324///     .tiers(TierSearchMode::All);
325/// ```
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct FragmentRule {
328    /// Name of the fragment directory (e.g., "conf.d").
329    pub dir_name: String,
330
331    /// Pattern for files in the fragment directory.
332    pub pattern: FilePattern,
333
334    /// Which tiers to search for fragments.
335    pub tiers: TierSearchMode,
336}
337
338impl FragmentRule {
339    /// Create a new fragment rule.
340    ///
341    /// # Example
342    ///
343    /// ```
344    /// use cfgmatic_paths::FragmentRule;
345    ///
346    /// let rule = FragmentRule::new("conf.d", "*.conf");
347    /// ```
348    #[must_use]
349    pub fn new(dir_name: impl Into<String>, pattern: impl Into<String>) -> Self {
350        Self {
351            dir_name: dir_name.into(),
352            pattern: FilePattern::glob(pattern),
353            tiers: TierSearchMode::default(),
354        }
355    }
356
357    /// Set which tiers to search.
358    #[must_use]
359    pub const fn tiers(mut self, tiers: TierSearchMode) -> Self {
360        self.tiers = tiers;
361        self
362    }
363}
364
365/// Set of configuration file rules.
366///
367/// Defines all configuration files for an application, including
368/// main files, fragments, and legacy files.
369///
370/// # Example
371///
372/// ```
373/// use cfgmatic_paths::{ConfigRuleSet, ConfigFileRule, FragmentRule};
374///
375/// let rules = ConfigRuleSet::builder()
376///     .main_file(ConfigFileRule::toml("config").required(true))
377///     .main_file(ConfigFileRule::extensions("config", &["yaml"]))
378///     .fragments(FragmentRule::new("conf.d", "*.conf"))
379///     .build();
380/// ```
381#[derive(Debug, Clone, Default)]
382pub struct ConfigRuleSet {
383    /// Main configuration files.
384    pub main_files: Vec<ConfigFileRule>,
385
386    /// Fragment directory rule (optional).
387    pub fragments: Option<FragmentRule>,
388}
389
390impl ConfigRuleSet {
391    /// Create a new empty rule set.
392    #[must_use]
393    pub fn new() -> Self {
394        Self::default()
395    }
396
397    /// Create a new builder for rule sets.
398    #[must_use]
399    pub fn builder() -> ConfigRuleSetBuilder {
400        ConfigRuleSetBuilder::new()
401    }
402
403    /// Add a main file rule.
404    pub fn add_main_file(&mut self, rule: ConfigFileRule) {
405        self.main_files.push(rule);
406    }
407
408    /// Set the fragment rule.
409    pub fn set_fragments(&mut self, fragments: FragmentRule) {
410        self.fragments = Some(fragments);
411    }
412
413    /// Get all main file rules.
414    #[must_use]
415    pub fn main_files(&self) -> &[ConfigFileRule] {
416        &self.main_files
417    }
418
419    /// Get the fragment rule if set.
420    #[must_use]
421    pub const fn fragments(&self) -> Option<&FragmentRule> {
422        self.fragments.as_ref()
423    }
424}
425
426/// Builder for creating configuration rule sets.
427#[derive(Debug, Clone, Default)]
428pub struct ConfigRuleSetBuilder {
429    /// Rules being built.
430    rules: ConfigRuleSet,
431}
432
433impl ConfigRuleSetBuilder {
434    /// Create a new builder.
435    #[must_use]
436    pub fn new() -> Self {
437        Self {
438            rules: ConfigRuleSet::new(),
439        }
440    }
441
442    /// Add a main file rule.
443    #[must_use]
444    pub fn main_file(mut self, rule: ConfigFileRule) -> Self {
445        self.rules.add_main_file(rule);
446        self
447    }
448
449    /// Set the fragment rule.
450    #[must_use]
451    pub fn fragments(mut self, fragments: FragmentRule) -> Self {
452        self.rules.set_fragments(fragments);
453        self
454    }
455
456    /// Build the rule set.
457    #[must_use]
458    pub fn build(self) -> ConfigRuleSet {
459        self.rules
460    }
461}
462
463/// Result of rule-based configuration discovery.
464///
465/// Contains all discovered configuration files grouped by rule,
466/// along with fragment information.
467///
468/// # Example
469///
470/// ```
471/// use cfgmatic_paths::{PathsBuilder, ConfigRuleSet, ConfigFileRule, FragmentRule};
472///
473/// let finder = PathsBuilder::new("myapp").build();
474///
475/// let rules = ConfigRuleSet::builder()
476///     .main_file(ConfigFileRule::toml("config"))
477///     .fragments(FragmentRule::new("conf.d", "*.conf"))
478///     .build();
479///
480/// let discovery = finder.discover_with_rules(&rules);
481///
482/// // Get all file paths for loading
483/// for path in discovery.all_paths() {
484///     println!("Found: {}", path.display());
485/// }
486///
487/// // Check if required files are present
488/// if let Some(missing) = discovery.missing_required() {
489///     eprintln!("Missing required file: {:?}", missing);
490/// }
491/// ```
492#[derive(Debug, Clone)]
493pub struct RuleBasedDiscovery {
494    /// The rule set that was used for discovery.
495    pub rules: ConfigRuleSet,
496
497    /// Discovered main files grouped by rule.
498    pub main_files: Vec<RuleMatchResult>,
499
500    /// Discovered fragment files.
501    pub fragments: Vec<RuleMatchResult>,
502}
503
504impl RuleBasedDiscovery {
505    /// Check if any files were found.
506    #[must_use]
507    pub fn is_empty(&self) -> bool {
508        self.main_files.iter().all(|r| r.matches.is_empty())
509            && self.fragments.iter().all(|r| r.matches.is_empty())
510    }
511
512    /// Get the total count of all discovered files.
513    #[must_use]
514    pub fn file_count(&self) -> usize {
515        let main_count: usize = self.main_files.iter().map(|r| r.matches.len()).sum();
516        let fragment_count: usize = self.fragments.iter().map(|r| r.matches.len()).sum();
517        main_count + fragment_count
518    }
519
520    /// Get all file paths from main files, sorted by priority (highest first).
521    ///
522    /// Returns paths in merge order: lowest priority first, highest priority last.
523    /// This allows sequential merging where later files override earlier ones.
524    #[must_use]
525    pub fn main_paths(&self) -> Vec<std::path::PathBuf> {
526        let mut paths = Vec::new();
527        for result in &self.main_files {
528            for candidate in &result.matches {
529                paths.push(candidate.path.clone());
530            }
531        }
532        // Sort by tier priority (lowest first for merge order)
533        paths.sort_by_key(|p| {
534            self.main_files
535                .iter()
536                .flat_map(|r| &r.matches)
537                .find(|c| &c.path == p)
538                .map_or(std::cmp::Reverse(0), |c| {
539                    std::cmp::Reverse(u8::from(c.tier))
540                })
541        });
542        paths
543    }
544
545    /// Get all file paths from fragments, sorted by priority (highest first).
546    ///
547    /// Returns paths in merge order: lowest priority first, highest priority last.
548    #[must_use]
549    pub fn fragment_paths(&self) -> Vec<std::path::PathBuf> {
550        let mut paths = Vec::new();
551        for result in &self.fragments {
552            for candidate in &result.matches {
553                paths.push(candidate.path.clone());
554            }
555        }
556        // Sort by tier priority (lowest first for merge order)
557        paths.sort_by_key(|p| {
558            self.fragments
559                .iter()
560                .flat_map(|r| &r.matches)
561                .find(|c| &c.path == p)
562                .map_or(std::cmp::Reverse(0), |c| {
563                    std::cmp::Reverse(u8::from(c.tier))
564                })
565        });
566        paths
567    }
568
569    /// Get all discovered file paths (both main and fragments).
570    ///
571    /// Returns paths in merge order: main files first (by tier), then fragments.
572    #[must_use]
573    pub fn all_paths(&self) -> Vec<std::path::PathBuf> {
574        let mut paths = self.main_paths();
575        paths.extend(self.fragment_paths());
576        paths
577    }
578
579    /// Get candidates for main files sorted by merge priority.
580    #[must_use]
581    pub fn main_candidates(&self) -> Vec<&ConfigCandidate> {
582        let mut candidates: Vec<&ConfigCandidate> = self
583            .main_files
584            .iter()
585            .flat_map(|r| r.matches.iter())
586            .collect();
587        candidates.sort_by_key(|c| std::cmp::Reverse(u8::from(c.tier)));
588        candidates
589    }
590
591    /// Get candidates for fragments sorted by merge priority.
592    #[must_use]
593    pub fn fragment_candidates(&self) -> Vec<&ConfigCandidate> {
594        let mut candidates: Vec<&ConfigCandidate> = self
595            .fragments
596            .iter()
597            .flat_map(|r| r.matches.iter())
598            .collect();
599        candidates.sort_by_key(|c| std::cmp::Reverse(u8::from(c.tier)));
600        candidates
601    }
602
603    /// Check if a required rule has no matching files.
604    ///
605    /// Returns the first required rule that has no matches.
606    #[must_use]
607    pub fn missing_required(&self) -> Option<&ConfigFileRule> {
608        self.rules.main_files.iter().find(|rule| {
609            rule.required && {
610                !self
611                    .main_files
612                    .iter()
613                    .any(|r| &r.rule == *rule && !r.matches.is_empty())
614            }
615        })
616    }
617
618    /// Get all existing files (filter out non-existent candidates).
619    #[must_use]
620    pub fn existing_files(&self) -> Vec<&ConfigCandidate> {
621        self.main_candidates()
622            .into_iter()
623            .chain(self.fragment_candidates())
624            .filter(|c| c.status.exists())
625            .collect()
626    }
627}
628
629/// Result of searching for files by a single rule.
630///
631/// Contains the rule that was used and all matching files found.
632#[derive(Debug, Clone)]
633pub struct RuleMatchResult {
634    /// The rule that was used for matching.
635    pub rule: ConfigFileRule,
636
637    /// Files that matched the rule.
638    pub matches: Vec<ConfigCandidate>,
639}
640
641/// Re-export for convenience at the crate level.
642pub use crate::core::discovery::ConfigCandidate;