Skip to main content

cfgmatic_paths/builder/
discovery.rs

1//! Configuration discovery helpers for [`PathFinder`](super::PathFinder).
2
3use std::path::{Path, PathBuf};
4
5use crate::core::{
6    ConfigCandidate, ConfigDiscovery, ConfigTier, DiscoveryOptions, FilePattern, PathStatus,
7    SourceType,
8};
9use crate::env::StdEnv;
10
11use super::{PathFinder, scan};
12
13impl PathFinder {
14    /// Discover configuration with full diagnostics.
15    ///
16    /// Searches all configuration locations and returns comprehensive
17    /// information about what was found, including the preferred path
18    /// for creating new configurations.
19    ///
20    /// # Examples
21    ///
22    /// ```
23    /// use cfgmatic_paths::PathsBuilder;
24    ///
25    /// let finder = PathsBuilder::new("myapp").build();
26    /// let discovery = finder.discover_config();
27    ///
28    /// println!("Preferred path: {}", discovery.preferred_path.display());
29    /// if let Some(found) = &discovery.found_path {
30    ///     println!("Found config at: {}", found.display());
31    /// }
32    ///
33    /// for candidate in discovery.candidates {
34    ///     println!("  - {:?}: {} ({:?})",
35    ///         candidate.tier,
36    ///         candidate.path.display(),
37    ///         candidate.status
38    ///     );
39    /// }
40    /// ```
41    #[must_use]
42    pub fn discover_config(&self) -> ConfigDiscovery {
43        self.discover_config_with_options(&DiscoveryOptions::default())
44    }
45
46    /// Discover configuration with custom options.
47    ///
48    /// Allows customization of the discovery process including
49    /// file patterns, fragment discovery, and legacy path inclusion.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use cfgmatic_paths::{DiscoveryOptions, FilePattern, PathsBuilder};
55    ///
56    /// let finder = PathsBuilder::new("myapp").build();
57    ///
58    /// let options = DiscoveryOptions::new()
59    ///     .with_pattern(FilePattern::extensions("config", &["toml", "yaml"]))
60    ///     .with_fragments(true)
61    ///     .with_fragment_dir("conf.d");
62    ///
63    /// let discovery = finder.discover_config_with_options(&options);
64    /// ```
65    #[must_use]
66    pub fn discover_config_with_options(&self, options: &DiscoveryOptions) -> ConfigDiscovery {
67        let mut candidates = Vec::new();
68        let mut fragments = Vec::new();
69        let mut found_path: Option<PathBuf> = None;
70
71        let tiers = [
72            (self.dir_finder.user_dirs(&StdEnv), ConfigTier::User),
73            (self.dir_finder.local_dirs(&StdEnv), ConfigTier::Local),
74            (self.dir_finder.system_dirs(&StdEnv), ConfigTier::System),
75        ];
76
77        for (dirs, tier) in tiers {
78            self.build_candidates_for_tier(
79                &dirs,
80                tier,
81                options,
82                &mut candidates,
83                &mut fragments,
84                &mut found_path,
85            );
86        }
87
88        candidates.sort_by(|a, b| b.tier.cmp(&a.tier));
89
90        ConfigDiscovery {
91            preferred_path: self.preferred_config_path(),
92            found_path,
93            candidates,
94            fragments,
95        }
96    }
97
98    /// Find all configuration files matching a pattern.
99    ///
100    /// Searches all configuration directories for files matching the
101    /// given pattern and returns them as candidates with status information.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use cfgmatic_paths::{FilePattern, PathsBuilder};
107    ///
108    /// let finder = PathsBuilder::new("myapp").build();
109    /// let pattern = FilePattern::extensions("config", &["toml", "yaml", "json"]);
110    ///
111    /// let configs = finder.find_config_files(&pattern);
112    /// for config in configs {
113    ///     if config.exists() {
114    ///         println!("Found: {}", config.path.display());
115    ///     }
116    /// }
117    /// ```
118    #[must_use]
119    pub fn find_config_files(&self, pattern: &FilePattern) -> Vec<ConfigCandidate> {
120        let mut candidates = Vec::new();
121
122        let tiers = [
123            (self.dir_finder.user_dirs(&StdEnv), ConfigTier::User),
124            (self.dir_finder.local_dirs(&StdEnv), ConfigTier::Local),
125            (self.dir_finder.system_dirs(&StdEnv), ConfigTier::System),
126        ];
127
128        for (dirs, tier) in tiers {
129            self.find_files_in_dirs(&dirs, tier, pattern, &mut candidates);
130        }
131
132        candidates.sort_by(|a, b| b.tier.cmp(&a.tier));
133        candidates
134    }
135
136    /// Find configuration fragments from conf.d-style directories.
137    ///
138    /// Searches for fragment directories (like `/etc/myapp/conf.d/`) and
139    /// returns all matching configuration files within them.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use cfgmatic_paths::{FilePattern, PathsBuilder};
145    ///
146    /// let finder = PathsBuilder::new("myapp").build();
147    /// let pattern = FilePattern::glob("*.conf");
148    ///
149    /// let fragments = finder.find_fragments(&pattern, "conf.d");
150    /// for frag in &fragments {
151    ///     println!("Fragment: {}", frag.display());
152    /// }
153    /// ```
154    #[must_use]
155    pub fn find_fragments(&self, pattern: &FilePattern, fragment_dir_name: &str) -> Vec<PathBuf> {
156        let mut fragments = Vec::new();
157
158        let dirs_by_tier: [(Vec<PathBuf>, ConfigTier); 3] = [
159            (self.dir_finder.user_dirs(&StdEnv), ConfigTier::User),
160            (self.dir_finder.local_dirs(&StdEnv), ConfigTier::Local),
161            (self.dir_finder.system_dirs(&StdEnv), ConfigTier::System),
162        ];
163
164        for (dirs, _tier) in dirs_by_tier {
165            for base_dir in dirs {
166                let conf_d = base_dir.join(fragment_dir_name);
167                if self.fs.is_dir(&conf_d) {
168                    for entry in self.fs.read_dir(&conf_d) {
169                        if pattern.matches(&entry) && self.fs.is_file(&entry) {
170                            fragments.push(entry);
171                        }
172                    }
173                }
174            }
175        }
176
177        fragments.sort();
178        fragments
179    }
180
181    /// Get all config directories where a file could be placed.
182    ///
183    /// Returns all directories in priority order where configuration
184    /// files could be located, regardless of whether they exist.
185    ///
186    /// # Examples
187    ///
188    /// ```
189    /// use cfgmatic_paths::PathsBuilder;
190    ///
191    /// let finder = PathsBuilder::new("myapp").build();
192    /// let dirs = finder.config_directories();
193    ///
194    /// for dir in dirs {
195    ///     println!("Config directory: {}", dir.display());
196    /// }
197    /// ```
198    #[must_use]
199    pub fn config_directories(&self) -> Vec<PathBuf> {
200        [
201            self.dir_finder.user_dirs(&StdEnv),
202            self.dir_finder.local_dirs(&StdEnv),
203            self.dir_finder.system_dirs(&StdEnv),
204        ]
205        .into_iter()
206        .flatten()
207        .collect()
208    }
209
210    /// Get the path status of a specific configuration path.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use cfgmatic_paths::PathsBuilder;
216    ///
217    /// let finder = PathsBuilder::new("myapp").build();
218    /// let path = finder.preferred_config_file("config.toml");
219    ///
220    /// let status = finder.path_status(&path);
221    /// println!("Path status: {:?}", status);
222    /// ```
223    #[must_use]
224    pub fn path_status(&self, path: &Path) -> PathStatus {
225        if !self.fs.exists(path) {
226            PathStatus::NotFound
227        } else if self.fs.is_file(path) {
228            PathStatus::File
229        } else {
230            PathStatus::Directory
231        }
232    }
233
234    /// Build candidates for a specific tier.
235    fn build_candidates_for_tier(
236        &self,
237        dirs: &[PathBuf],
238        tier: ConfigTier,
239        options: &DiscoveryOptions,
240        candidates: &mut Vec<ConfigCandidate>,
241        fragments: &mut Vec<PathBuf>,
242        found_path: &mut Option<PathBuf>,
243    ) {
244        for dir in dirs {
245            let status = if self.fs.exists(dir) {
246                if self.fs.is_dir(dir) {
247                    PathStatus::Directory
248                } else {
249                    PathStatus::File
250                }
251            } else {
252                PathStatus::NotFound
253            };
254
255            let source_type = if candidates.is_empty() && found_path.is_none() {
256                SourceType::MainFile
257            } else {
258                SourceType::Legacy
259            };
260
261            candidates.push(ConfigCandidate::new(dir.clone(), status, tier, source_type));
262
263            if found_path.is_none() && status.exists() {
264                *found_path = Some(dir.clone());
265            }
266
267            if status == PathStatus::Directory {
268                self.check_dir_for_configs(dir, tier, options, candidates, found_path);
269            }
270
271            if options.include_fragments {
272                let conf_d = dir.join(&options.fragment_dir);
273                if self.fs.is_dir(&conf_d) {
274                    candidates.push(ConfigCandidate::new(
275                        conf_d.clone(),
276                        PathStatus::Directory,
277                        tier,
278                        SourceType::FragmentsDir,
279                    ));
280
281                    for entry in self.fs.read_dir(&conf_d) {
282                        if self.fs.is_file(&entry)
283                            && (fragments.is_empty() || !fragments.contains(&entry))
284                        {
285                            fragments.push(entry);
286                        }
287                    }
288                }
289            }
290        }
291    }
292
293    /// Check a directory for configuration files.
294    fn check_dir_for_configs(
295        &self,
296        dir: &Path,
297        tier: ConfigTier,
298        options: &DiscoveryOptions,
299        candidates: &mut Vec<ConfigCandidate>,
300        found_path: &mut Option<PathBuf>,
301    ) {
302        if let Some(filenames) = options.pattern.concrete_filenames() {
303            for filename in filenames {
304                let file_path = dir.join(&filename);
305                let status = self.path_status(&file_path);
306
307                candidates.push(ConfigCandidate::new(
308                    file_path.clone(),
309                    status,
310                    tier,
311                    SourceType::MainFile,
312                ));
313
314                if found_path.is_none() && status.exists() {
315                    *found_path = Some(file_path);
316                }
317            }
318        }
319    }
320
321    /// Find files matching a pattern in a list of directories.
322    pub(super) fn find_files_in_dirs(
323        &self,
324        dirs: &[PathBuf],
325        tier: ConfigTier,
326        pattern: &FilePattern,
327        candidates: &mut Vec<ConfigCandidate>,
328    ) {
329        candidates.extend(scan::collect_matching_candidates(
330            self.fs.as_ref(),
331            dirs,
332            tier,
333            pattern,
334            SourceType::MainFile,
335            |path| self.path_status(path),
336        ));
337    }
338}