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}