cfgmatic_paths/core/
pattern.rs1use std::path::Path;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum FilePattern {
10 Exact(String),
12
13 Extensions {
15 base: String,
17 extensions: Vec<String>,
19 },
20
21 Glob(String),
23
24 Any(Vec<Self>),
26}
27
28impl FilePattern {
29 pub fn exact(name: impl Into<String>) -> Self {
31 Self::Exact(name.into())
32 }
33
34 pub fn extensions(base: impl Into<String>, exts: &[&str]) -> Self {
36 Self::Extensions {
37 base: base.into(),
38 extensions: exts.iter().map(|&s| s.to_string()).collect(),
39 }
40 }
41
42 pub fn glob(pattern: impl Into<String>) -> Self {
44 Self::Glob(pattern.into())
45 }
46
47 #[must_use]
49 pub fn matches(&self, path: &Path) -> bool {
50 let filename = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
51
52 match self {
53 Self::Exact(name) => filename == name,
54 Self::Extensions { base, extensions } => extensions
55 .iter()
56 .any(|ext| filename == format!("{base}.{ext}")),
57 Self::Glob(pattern) => glob_match(pattern, filename),
58 Self::Any(patterns) => patterns.iter().any(|p| p.matches(path)),
59 }
60 }
61
62 #[must_use]
65 pub fn concrete_filenames(&self) -> Option<Vec<String>> {
66 match self {
67 Self::Exact(name) => Some(vec![name.clone()]),
68 Self::Extensions { base, extensions } => Some(
69 extensions
70 .iter()
71 .map(|ext| format!("{base}.{ext}"))
72 .collect(),
73 ),
74 Self::Glob(_) => None,
75 Self::Any(patterns) => {
76 let mut result = Vec::new();
77 for pattern in patterns {
78 if let Some(names) = pattern.concrete_filenames() {
79 result.extend(names);
80 } else {
81 return None;
83 }
84 }
85 Some(result)
86 }
87 }
88 }
89
90 pub fn has_recursive_glob(&self) -> bool {
92 match self {
93 Self::Glob(pattern) => pattern.contains("**"),
94 Self::Any(patterns) => patterns.iter().any(Self::has_recursive_glob),
95 _ => false,
96 }
97 }
98}
99
100impl Default for FilePattern {
101 fn default() -> Self {
102 Self::exact("config")
103 }
104}
105
106fn glob_match(pattern: &str, text: &str) -> bool {
108 let pattern = pattern.replace("**", "*");
110
111 glob_match_inner(&pattern, text)
113}
114
115fn glob_match_inner(pattern: &str, text: &str) -> bool {
117 let mut pat_chars = pattern.chars();
118 let mut text_chars = text.chars();
119
120 while let Some(p) = pat_chars.next() {
121 match p {
122 '*' => {
123 let remaining_pattern: String = pat_chars.collect();
125 let remaining_text: String = text_chars.clone().collect();
126
127 if remaining_pattern.is_empty() {
129 return true;
130 }
131
132 for i in 0..=remaining_text.len() {
134 let suffix = &remaining_text[i..];
135 if glob_match_inner(&remaining_pattern, suffix) {
136 return true;
137 }
138 }
139 return false;
140 }
141 '?' => {
142 if text_chars.next().is_none() {
143 return false;
144 }
145 }
146 c => {
147 if text_chars.next() != Some(c) {
148 return false;
149 }
150 }
151 }
152 }
153
154 text_chars.next().is_none()
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_exact_match() {
164 let pattern = FilePattern::exact("config.toml");
165 assert!(pattern.matches(Path::new("/path/config.toml")));
166 assert!(!pattern.matches(Path::new("/path/config.json")));
167 }
168
169 #[test]
170 fn test_extensions_match() {
171 let pattern = FilePattern::extensions("config", &["toml", "json", "yaml"]);
172 assert!(pattern.matches(Path::new("/path/config.toml")));
173 assert!(pattern.matches(Path::new("/path/config.json")));
174 assert!(!pattern.matches(Path::new("/path/config.yml")));
175 assert!(!pattern.matches(Path::new("/path/other.toml")));
176 }
177
178 #[test]
179 fn test_glob_match_simple() {
180 let pattern = FilePattern::glob("*.toml");
181 assert!(pattern.matches(Path::new("/path/config.toml")));
182 assert!(pattern.matches(Path::new("/path/app.toml")));
183 assert!(!pattern.matches(Path::new("/path/config.json")));
184 }
185
186 #[test]
187 fn test_concrete_filenames() {
188 let pattern = FilePattern::extensions("config", &["toml", "json"]);
189 let names = pattern.concrete_filenames().unwrap();
190 assert_eq!(names, vec!["config.toml", "config.json"]);
191
192 let glob = FilePattern::glob("*.toml");
193 assert!(glob.concrete_filenames().is_none());
194 }
195
196 #[test]
197 fn test_has_recursive_glob() {
198 assert!(FilePattern::glob("**/*.toml").has_recursive_glob());
199 assert!(!FilePattern::glob("*.toml").has_recursive_glob());
200 assert!(!FilePattern::exact("config.toml").has_recursive_glob());
201 }
202
203 #[test]
204 fn test_any_pattern() {
205 let pattern = FilePattern::Any(vec![
206 FilePattern::exact("config.toml"),
207 FilePattern::exact("config.json"),
208 ]);
209 assert!(pattern.matches(Path::new("/path/config.toml")));
210 assert!(pattern.matches(Path::new("/path/config.json")));
211 assert!(!pattern.matches(Path::new("/path/config.yaml")));
212 }
213}