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;