1use crate::Format;
4use crate::error::Result;
5use crate::file::{ConfigFile, ConfigFiles};
6use cfgmatic_paths::{ConfigTier, PathFinder, PathsBuilder};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
23pub struct FileFinder {
24 app_name: String,
25 formats: Vec<Format>,
26 base_name: Option<String>,
27 require_existence: bool,
28 follow_symlinks: bool,
29 search_depth: usize,
30}
31
32impl FileFinder {
33 pub fn new(app_name: impl Into<String>) -> Self {
35 Self {
36 app_name: app_name.into(),
37 formats: vec![Format::Toml, Format::Json],
38 base_name: Some("config".to_string()),
39 require_existence: true,
40 follow_symlinks: false,
41 search_depth: 3,
42 }
43 }
44
45 #[must_use]
47 pub fn formats(mut self, formats: &[Format]) -> Self {
48 self.formats = formats.to_vec();
49 self
50 }
51
52 #[must_use]
56 pub fn base_name(mut self, name: impl Into<String>) -> Self {
57 self.base_name = Some(name.into());
58 self
59 }
60
61 #[must_use]
63 pub fn any_name(mut self) -> Self {
64 self.base_name = None;
65 self
66 }
67
68 #[must_use]
70 pub const fn require_existence(mut self, require: bool) -> Self {
71 self.require_existence = require;
72 self
73 }
74
75 #[must_use]
77 pub const fn follow_symlinks(mut self, follow: bool) -> Self {
78 self.follow_symlinks = follow;
79 self
80 }
81
82 #[must_use]
84 pub const fn search_depth(mut self, depth: usize) -> Self {
85 self.search_depth = depth;
86 self
87 }
88
89 #[must_use]
91 pub fn build(self) -> FileFinderState {
92 let path_finder = PathsBuilder::new(&self.app_name).build();
93 FileFinderState {
94 config: self,
95 path_finder,
96 }
97 }
98
99 pub fn find(self) -> Result<ConfigFiles> {
107 self.build().find()
108 }
109
110 pub fn find_first(self) -> Result<Option<ConfigFile>> {
116 let files = self.find()?;
117 Ok(files.first().cloned())
118 }
119}
120
121pub struct FileFinderState {
123 config: FileFinder,
124 path_finder: PathFinder,
125}
126
127impl FileFinderState {
128 pub fn find(&self) -> Result<ConfigFiles> {
134 let mut files = ConfigFiles::new();
135
136 self.search_in_tier(&mut files, ConfigTier::User, self.path_finder.user_dirs())?;
138 self.search_in_tier(&mut files, ConfigTier::Local, self.path_finder.local_dirs())?;
139 self.search_in_tier(
140 &mut files,
141 ConfigTier::System,
142 self.path_finder.system_dirs(),
143 )?;
144
145 Ok(files)
146 }
147
148 fn search_in_tier(
150 &self,
151 files: &mut ConfigFiles,
152 tier: ConfigTier,
153 dirs: Vec<PathBuf>,
154 ) -> Result<()> {
155 for dir in dirs {
156 if !dir.exists() {
157 continue;
158 }
159
160 if self.config.base_name.is_some() {
161 self.search_named_files(files, &dir, tier);
163 } else {
164 self.search_any_files(files, &dir, tier, 0)?;
166 }
167 }
168 Ok(())
169 }
170
171 fn search_named_files(&self, files: &mut ConfigFiles, dir: &Path, tier: ConfigTier) {
173 let Some(base_name) = self.config.base_name.as_ref() else {
174 return;
175 };
176
177 for format in &self.config.formats {
178 let file_name = format!("{}.{}", base_name, format.extension());
179 let path = dir.join(&file_name);
180
181 if self.should_include_file(&path)
182 && let Some(file) = ConfigFile::new(path, tier)
183 {
184 files.push(file);
185 }
186 }
187 }
188
189 fn search_any_files(
191 &self,
192 files: &mut ConfigFiles,
193 dir: &Path,
194 tier: ConfigTier,
195 depth: usize,
196 ) -> Result<()> {
197 if depth > self.config.search_depth {
198 return Ok(());
199 }
200
201 let Ok(entries) = fs::read_dir(dir) else {
202 return Ok(());
203 };
204
205 for entry in entries.flatten() {
206 let path = entry.path();
207
208 if path.is_file() {
209 if let Some(format) = Format::from_path(&path)
210 && self.config.formats.contains(&format)
211 && self.should_include_file(&path)
212 && let Some(file) = ConfigFile::new(path, tier)
213 {
214 files.push(file);
215 }
216 } else if path.is_dir()
217 && self.config.follow_symlinks
218 && depth < self.config.search_depth
219 {
220 self.search_any_files(files, &path, tier, depth + 1)?;
222 }
223 }
224
225 Ok(())
226 }
227
228 fn should_include_file(&self, path: &Path) -> bool {
230 if !self.config.require_existence {
231 return true;
232 }
233
234 let Ok(metadata) = fs::symlink_metadata(path) else {
235 return false;
236 };
237
238 if metadata.is_symlink() && !self.config.follow_symlinks {
239 return false;
240 }
241
242 path.exists()
243 }
244}
245
246pub fn find_files(app_name: impl Into<String>) -> Result<ConfigFiles> {
263 FileFinder::new(app_name).find()
264}
265
266pub fn find_first_file(app_name: impl Into<String>) -> Result<Option<ConfigFile>> {
272 FileFinder::new(app_name).find_first()
273}
274
275pub fn load_first<T: serde::de::DeserializeOwned>(
281 app_name: impl Into<String>,
282) -> Result<Option<T>> {
283 match find_first_file(app_name)? {
284 Some(mut file) => Ok(Some(file.parse()?)),
285 None => Ok(None),
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_finder_builder() {
295 let finder = FileFinder::new("myapp")
296 .formats(&[Format::Toml])
297 .base_name("app")
298 .require_existence(false);
299
300 assert_eq!(finder.formats.len(), 1);
301 assert_eq!(finder.base_name, Some("app".to_string()));
302 assert!(!finder.require_existence);
303 }
304
305 #[test]
306 fn test_find_first_nonexistent() -> Result<()> {
307 let result = FileFinder::new("nonexistent_app_12345").find_first()?;
308 assert!(result.is_none());
309 Ok(())
310 }
311}