Skip to main content

cfgmatic_paths/core/
discovery.rs

1//! Configuration discovery types and results.
2
3use crate::core::config_tier::ConfigTier;
4use crate::core::pattern::FilePattern;
5use std::path::PathBuf;
6
7/// Status of a configuration path.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum PathStatus {
10    /// Path exists and is a directory.
11    Directory,
12    /// Path exists and is a file.
13    File,
14    /// Path does not exist.
15    NotFound,
16}
17
18impl PathStatus {
19    /// Returns true if the path exists.
20    #[must_use]
21    pub const fn exists(self) -> bool {
22        matches!(self, Self::Directory | Self::File)
23    }
24
25    /// Returns true if the path is a directory.
26    #[must_use]
27    pub const fn is_dir(self) -> bool {
28        matches!(self, Self::Directory)
29    }
30
31    /// Returns true if the path is a file.
32    #[must_use]
33    pub const fn is_file(self) -> bool {
34        matches!(self, Self::File)
35    }
36}
37
38/// Type of configuration source.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum SourceType {
41    /// Main configuration file.
42    MainFile,
43    /// Configuration directory containing config files.
44    ConfigDir,
45    /// Fragments directory (e.g., conf.d).
46    FragmentsDir,
47    /// Legacy configuration file (e.g., ~/.apprc).
48    Legacy,
49}
50
51/// Information about a configuration path candidate.
52#[derive(Debug, Clone)]
53pub struct ConfigCandidate {
54    /// Full path to the configuration.
55    pub path: PathBuf,
56    /// Existence status of the path.
57    pub status: PathStatus,
58    /// Priority tier of this configuration.
59    pub tier: ConfigTier,
60    /// Type of configuration source.
61    pub source_type: SourceType,
62}
63
64impl ConfigCandidate {
65    /// Create a new config candidate.
66    #[must_use]
67    pub const fn new(
68        path: PathBuf,
69        status: PathStatus,
70        tier: ConfigTier,
71        source_type: SourceType,
72    ) -> Self {
73        Self {
74            path,
75            status,
76            tier,
77            source_type,
78        }
79    }
80
81    /// Returns true if this candidate exists.
82    #[must_use]
83    pub const fn exists(&self) -> bool {
84        self.status.exists()
85    }
86
87    /// Returns true if this candidate is a file.
88    #[must_use]
89    pub const fn is_file(&self) -> bool {
90        self.status.is_file()
91    }
92
93    /// Returns true if this candidate is a directory.
94    #[must_use]
95    pub const fn is_dir(&self) -> bool {
96        self.status.is_dir()
97    }
98}
99
100/// Result of a configuration discovery operation.
101///
102/// Provides comprehensive information about configuration locations,
103/// including the preferred path for new configurations and all
104/// candidates that were searched.
105#[derive(Debug, Clone)]
106pub struct ConfigDiscovery {
107    /// Preferred path for creating a new configuration (highest priority user path).
108    pub preferred_path: PathBuf,
109    /// Path to the first found existing configuration, if any.
110    pub found_path: Option<PathBuf>,
111    /// All candidates searched in priority order.
112    pub candidates: Vec<ConfigCandidate>,
113    /// Found configuration fragments (from conf.d and similar directories).
114    pub fragments: Vec<PathBuf>,
115}
116
117impl ConfigDiscovery {
118    /// Create a new config discovery result.
119    #[must_use]
120    pub const fn new(
121        preferred_path: PathBuf,
122        found_path: Option<PathBuf>,
123        candidates: Vec<ConfigCandidate>,
124        fragments: Vec<PathBuf>,
125    ) -> Self {
126        Self {
127            preferred_path,
128            found_path,
129            candidates,
130            fragments,
131        }
132    }
133
134    /// Returns true if any configuration was found.
135    #[must_use]
136    pub const fn has_config(&self) -> bool {
137        self.found_path.is_some()
138    }
139
140    /// Get all found configuration files (not directories).
141    #[must_use]
142    pub fn found_files(&self) -> Vec<&ConfigCandidate> {
143        self.candidates.iter().filter(|c| c.is_file()).collect()
144    }
145
146    /// Get all found configuration directories.
147    #[must_use]
148    pub fn found_dirs(&self) -> Vec<&ConfigCandidate> {
149        self.candidates.iter().filter(|c| c.is_dir()).collect()
150    }
151
152    /// Get candidates by tier.
153    #[must_use]
154    pub fn by_tier(&self, tier: ConfigTier) -> Vec<&ConfigCandidate> {
155        self.candidates.iter().filter(|c| c.tier == tier).collect()
156    }
157
158    /// Get candidates by source type.
159    #[must_use]
160    pub fn by_source_type(&self, source_type: SourceType) -> Vec<&ConfigCandidate> {
161        self.candidates
162            .iter()
163            .filter(|c| c.source_type == source_type)
164            .collect()
165    }
166
167    /// Create an empty discovery result.
168    #[must_use]
169    pub const fn empty() -> Self {
170        Self {
171            preferred_path: PathBuf::new(),
172            found_path: None,
173            candidates: Vec::new(),
174            fragments: Vec::new(),
175        }
176    }
177}
178
179impl Default for ConfigDiscovery {
180    fn default() -> Self {
181        Self::empty()
182    }
183}
184
185/// Options for configuration file discovery.
186#[derive(Debug, Clone)]
187pub struct DiscoveryOptions {
188    /// File pattern to match.
189    pub pattern: FilePattern,
190    /// Whether to search for fragments (conf.d style).
191    pub include_fragments: bool,
192    /// Fragment directory name (e.g., "conf.d").
193    pub fragment_dir: String,
194    /// Whether to include legacy paths.
195    pub include_legacy: bool,
196}
197
198impl DiscoveryOptions {
199    /// Create new discovery options.
200    #[must_use]
201    pub fn new() -> Self {
202        Self {
203            pattern: FilePattern::default(),
204            include_fragments: true,
205            fragment_dir: String::from("conf.d"),
206            include_legacy: true,
207        }
208    }
209
210    /// Set the file pattern.
211    #[must_use]
212    pub fn with_pattern(mut self, pattern: FilePattern) -> Self {
213        self.pattern = pattern;
214        self
215    }
216
217    /// Enable or disable fragment discovery.
218    #[must_use]
219    pub const fn with_fragments(mut self, include: bool) -> Self {
220        self.include_fragments = include;
221        self
222    }
223
224    /// Set the fragment directory name.
225    #[must_use]
226    pub fn with_fragment_dir(mut self, dir: impl Into<String>) -> Self {
227        self.fragment_dir = dir.into();
228        self
229    }
230
231    /// Enable or disable legacy paths.
232    #[must_use]
233    pub const fn with_legacy(mut self, include: bool) -> Self {
234        self.include_legacy = include;
235        self
236    }
237}
238
239impl Default for DiscoveryOptions {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_path_status() {
251        assert!(PathStatus::Directory.exists());
252        assert!(PathStatus::File.exists());
253        assert!(!PathStatus::NotFound.exists());
254        assert!(PathStatus::Directory.is_dir());
255        assert!(PathStatus::File.is_file());
256    }
257
258    #[test]
259    fn test_config_candidate() {
260        let candidate = ConfigCandidate::new(
261            PathBuf::from("/test/config"),
262            PathStatus::File,
263            ConfigTier::User,
264            SourceType::MainFile,
265        );
266        assert!(candidate.exists());
267        assert!(candidate.is_file());
268        assert!(!candidate.is_dir());
269    }
270
271    #[test]
272    fn test_config_discovery_empty() {
273        let discovery = ConfigDiscovery::empty();
274        assert!(!discovery.has_config());
275        assert!(discovery.found_path.is_none());
276        assert!(discovery.candidates.is_empty());
277        assert!(discovery.fragments.is_empty());
278    }
279
280    #[test]
281    fn test_discovery_options_default() {
282        let options = DiscoveryOptions::new();
283        assert!(options.include_fragments);
284        assert!(options.include_legacy);
285        assert_eq!(options.fragment_dir, "conf.d");
286    }
287
288    #[test]
289    fn test_discovery_options_builder() {
290        let options = DiscoveryOptions::new()
291            .with_pattern(FilePattern::exact("myapp.toml"))
292            .with_fragments(false)
293            .with_fragment_dir("fragments")
294            .with_legacy(false);
295
296        assert!(!options.include_fragments);
297        assert!(!options.include_legacy);
298        assert_eq!(options.fragment_dir, "fragments");
299    }
300}