cfgmatic_paths/
builder.rs1mod discovery;
4mod rules;
5mod scan;
6
7use crate::core::{AppType, ConfigTier};
8use crate::env::StdEnv;
9use crate::platform::{DirectoryFinder, DirectoryInfo};
10use crate::{Fs, StdFs};
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13
14#[derive(Debug, Clone)]
29pub struct PathsBuilder {
30 app_name: String,
32 app_type: AppType,
34 #[cfg(windows)]
36 company_name: Option<String>,
37 #[cfg(all(target_os = "macos", feature = "macos-gui"))]
39 bundle_id: Option<String>,
40 legacy_rc: bool,
42}
43
44impl PathsBuilder {
45 pub fn new(app_name: impl Into<String>) -> Self {
47 Self {
48 app_name: app_name.into(),
49 app_type: AppType::default(),
50 #[cfg(windows)]
51 company_name: None,
52 #[cfg(all(target_os = "macos", feature = "macos-gui"))]
53 bundle_id: None,
54 legacy_rc: true,
55 }
56 }
57
58 #[must_use]
60 pub const fn app_type(mut self, app_type: AppType) -> Self {
61 self.app_type = app_type;
62 self
63 }
64
65 #[must_use]
67 pub const fn legacy_rc(mut self, enabled: bool) -> Self {
68 self.legacy_rc = enabled;
69 self
70 }
71
72 #[cfg(windows)]
74 pub fn company_name(mut self, name: impl Into<String>) -> Self {
75 self.company_name = Some(name.into());
76 self
77 }
78
79 #[cfg(all(target_os = "macos", feature = "macos-gui"))]
81 #[must_use]
82 pub fn bundle_id(mut self, id: impl Into<String>) -> Self {
83 self.bundle_id = Some(id.into());
84 self
85 }
86
87 #[must_use]
89 pub fn build(self) -> PathFinder {
90 let preferred_fallback = PathBuf::from(".config").join(&self.app_name);
91 let dir_finder = self.build_directory_finder();
92 PathFinder {
93 dir_finder,
94 fs: Arc::new(StdFs),
95 preferred_fallback,
96 }
97 }
98
99 fn build_directory_finder(self) -> Box<dyn DirectoryFinder> {
101 cfg_if::cfg_if! {
102 if #[cfg(all(target_os = "macos", feature = "macos-gui"))] {
103 use crate::platform::MacOSGuiDirectoryFinder;
104 use crate::platform::UnixDirectoryFinder;
105 if self.app_type == AppType::Gui {
106 let bundle_id = self.bundle_id.unwrap_or_else(|| {
107 format!("com.example.{}", self.app_name)
108 });
109 Box::new(MacOSGuiDirectoryFinder::new(bundle_id))
110 } else {
111 Box::new(UnixDirectoryFinder::new(self.app_name).legacy_rc(self.legacy_rc))
113 }
114 } else if #[cfg(windows)] {
115 use crate::platform::WindowsDirectoryFinder;
116 let company_name = self.company_name.unwrap_or_else(|| {
117 self.app_name.clone()
118 });
119 Box::new(WindowsDirectoryFinder::new(self.app_name, company_name))
120 } else {
121 use crate::platform::UnixDirectoryFinder;
122 Box::new(UnixDirectoryFinder::new(self.app_name).legacy_rc(self.legacy_rc))
124 }
125 }
126 }
127}
128
129pub struct PathFinder {
135 dir_finder: Box<dyn DirectoryFinder>,
137 fs: Arc<dyn Fs>,
139 preferred_fallback: PathBuf,
141}
142
143impl PathFinder {
144 #[must_use]
146 pub fn user_dirs(&self) -> Vec<PathBuf> {
147 self.dir_finder.user_dirs(&StdEnv)
148 }
149
150 #[must_use]
152 pub fn local_dirs(&self) -> Vec<PathBuf> {
153 self.dir_finder.local_dirs(&StdEnv)
154 }
155
156 #[must_use]
158 pub fn system_dirs(&self) -> Vec<PathBuf> {
159 self.dir_finder.system_dirs(&StdEnv)
160 }
161
162 #[must_use]
164 pub fn all_dirs(&self) -> Vec<DirectoryInfo> {
165 [
166 self.dirs_with_tier(self.user_dirs(), ConfigTier::User),
167 self.dirs_with_tier(self.local_dirs(), ConfigTier::Local),
168 self.dirs_with_tier(self.system_dirs(), ConfigTier::System),
169 ]
170 .into_iter()
171 .flatten()
172 .collect()
173 }
174
175 #[must_use]
177 pub fn user_config_dir(&self) -> Option<PathBuf> {
178 self.user_dirs().into_iter().next()
179 }
180
181 pub fn ensure_user_config_dir(&self) -> std::io::Result<PathBuf> {
189 let path = self.user_config_dir().ok_or_else(|| {
190 std::io::Error::new(
191 std::io::ErrorKind::NotFound,
192 "No user config directory found",
193 )
194 })?;
195 self.fs.create_dir_all(&path)?;
196 Ok(path)
197 }
198
199 #[must_use]
215 pub fn preferred_config_path(&self) -> PathBuf {
216 self.user_dirs()
217 .into_iter()
218 .next()
219 .unwrap_or_else(|| self.preferred_fallback.clone())
220 }
221
222 #[must_use]
234 pub fn preferred_config_file(&self, filename: impl AsRef<Path>) -> PathBuf {
235 self.preferred_config_path().join(filename)
236 }
237
238 fn dirs_with_tier(&self, paths: Vec<PathBuf>, tier: ConfigTier) -> Vec<DirectoryInfo> {
240 paths
241 .into_iter()
242 .map(|path| DirectoryInfo {
243 exists: self.fs.is_dir(&path),
244 path,
245 tier,
246 })
247 .collect()
248 }
249}
250
251impl std::fmt::Debug for PathFinder {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 f.debug_struct("PathFinder")
254 .field("user_dirs", &self.user_dirs())
255 .field("local_dirs", &self.local_dirs())
256 .field("system_dirs", &self.system_dirs())
257 .finish()
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::FilePattern;
265 use std::collections::{BTreeMap, BTreeSet};
266 use std::iter;
267 use std::path::Path;
268
269 struct EmptyDirectoryFinder;
270
271 impl DirectoryFinder for EmptyDirectoryFinder {
272 fn user_dirs(&self, _env: &dyn crate::Env) -> Vec<PathBuf> {
273 Vec::new()
274 }
275
276 fn local_dirs(&self, _env: &dyn crate::Env) -> Vec<PathBuf> {
277 Vec::new()
278 }
279
280 fn system_dirs(&self, _env: &dyn crate::Env) -> Vec<PathBuf> {
281 Vec::new()
282 }
283 }
284
285 #[derive(Default)]
286 struct MemoryFs {
287 files: BTreeSet<PathBuf>,
288 dirs: BTreeMap<PathBuf, Vec<PathBuf>>,
289 }
290
291 impl Fs for MemoryFs {
292 fn exists(&self, path: &Path) -> bool {
293 self.files.contains(path) || self.dirs.contains_key(path)
294 }
295
296 fn is_file(&self, path: &Path) -> bool {
297 self.files.contains(path)
298 }
299
300 fn is_dir(&self, path: &Path) -> bool {
301 self.dirs.contains_key(path)
302 }
303
304 fn create_dir_all(&self, _path: &Path) -> std::io::Result<()> {
305 Ok(())
306 }
307
308 fn read_dir(&self, path: &Path) -> Vec<PathBuf> {
309 self.dirs.get(path).cloned().unwrap_or_default()
310 }
311 }
312
313 #[test]
314 fn test_preferred_config_path_uses_app_specific_fallback() {
315 let finder = PathFinder {
316 dir_finder: Box::new(EmptyDirectoryFinder),
317 fs: Arc::new(MemoryFs::default()),
318 preferred_fallback: PathBuf::from(".config").join("custom-app"),
319 };
320
321 assert_eq!(
322 finder.preferred_config_path(),
323 PathBuf::from(".config").join("custom-app")
324 );
325 }
326
327 #[test]
328 fn test_find_files_in_dirs_uses_shared_scanner() {
329 let config_dir = PathBuf::from("/config");
330 let config_file = config_dir.join("app.toml");
331
332 let fs = MemoryFs {
333 files: iter::once(config_file.clone()).collect(),
334 dirs: iter::once((config_dir.clone(), vec![config_file.clone()])).collect(),
335 };
336
337 let finder = PathFinder {
338 dir_finder: Box::new(EmptyDirectoryFinder),
339 fs: Arc::new(fs),
340 preferred_fallback: PathBuf::from(".config").join("app"),
341 };
342
343 let mut candidates = Vec::new();
344 finder.find_files_in_dirs(
345 &[config_dir],
346 ConfigTier::User,
347 &FilePattern::glob("*.toml"),
348 &mut candidates,
349 );
350
351 assert_eq!(candidates.len(), 1);
352 assert!(
353 candidates
354 .iter()
355 .all(|candidate| candidate.path == config_file)
356 );
357 }
358}