1use alloc::vec::Vec;
2
3use crate::engine::{self, MatchEngine};
4use crate::glob::{Candidate, Glob};
5
6#[derive(Clone, Debug)]
32pub struct GlobMap<T> {
33 engine: MatchEngine,
34 values: Vec<T>,
35}
36
37impl<T> GlobMap<T> {
38 pub fn get(&self, path: impl AsRef<str>) -> Option<&T> {
40 self.engine
41 .first_match(path.as_ref())
42 .map(|idx| &self.values[idx])
43 }
44
45 pub fn get_candidate(&self, candidate: &Candidate<'_>) -> Option<&T> {
47 self.engine
48 .first_match(candidate.path())
49 .map(|idx| &self.values[idx])
50 }
51
52 pub fn get_matches(&self, path: impl AsRef<str>) -> Vec<&T> {
58 let mut indices = Vec::new();
59 self.engine.matches_into(path.as_ref(), &mut indices);
60 indices.iter().map(|&idx| &self.values[idx]).collect()
61 }
62
63 pub fn get_matches_candidate(&self, candidate: &Candidate<'_>) -> Vec<&T> {
65 self.get_matches(candidate.path())
66 }
67
68 pub fn is_match(&self, path: impl AsRef<str>) -> bool {
70 self.engine.is_match(path.as_ref())
71 }
72
73 pub fn len(&self) -> usize {
75 self.engine.len()
76 }
77
78 pub fn is_empty(&self) -> bool {
80 self.engine.is_empty()
81 }
82}
83
84#[derive(Clone, Debug)]
86pub struct GlobMapBuilder<T> {
87 entries: Vec<(Glob, T)>,
88}
89
90impl<T> Default for GlobMapBuilder<T> {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96impl<T> GlobMapBuilder<T> {
97 pub fn new() -> Self {
99 Self {
100 entries: Vec::new(),
101 }
102 }
103
104 pub fn insert(&mut self, glob: Glob, value: T) -> &mut Self {
106 self.entries.push((glob, value));
107 self
108 }
109
110 pub fn build(self) -> Result<GlobMap<T>, crate::error::Error> {
116 let (patterns, values): (Vec<Glob>, Vec<T>) = self.entries.into_iter().unzip();
117 let engine = engine::build_engine(patterns)?;
118 Ok(GlobMap { engine, values })
119 }
120}
121
122#[cfg(test)]
123#[allow(clippy::unwrap_used)]
124mod tests {
125 use alloc::string::String;
126
127 use super::*;
128
129 fn build_map(entries: &[(&str, &str)]) -> GlobMap<String> {
130 let mut builder = GlobMapBuilder::new();
131 for &(pat, val) in entries {
132 builder.insert(Glob::new(pat).unwrap(), String::from(val));
133 }
134 builder.build().unwrap()
135 }
136
137 #[test]
138 fn get_returns_first_match() {
139 let map = build_map(&[("*.rs", "rust"), ("**/*.rs", "rust-deep")]);
140 assert_eq!(map.get("foo.rs").map(String::as_str), Some("rust"));
142 }
143
144 #[test]
145 fn get_returns_none_on_no_match() {
146 let map = build_map(&[("*.rs", "rust")]);
147 assert_eq!(map.get("foo.js"), None);
148 }
149
150 #[test]
151 fn get_matches_returns_all() {
152 let map = build_map(&[
153 ("*.rs", "rust"),
154 ("**/*.rs", "rust-deep"),
155 ("*.toml", "toml"),
156 ]);
157 let matches: Vec<&str> = map
158 .get_matches("foo.rs")
159 .into_iter()
160 .map(String::as_str)
161 .collect();
162 assert!(matches.contains(&"rust"));
163 assert!(matches.contains(&"rust-deep"));
164 assert!(!matches.contains(&"toml"));
165 }
166
167 #[test]
168 fn multiple_patterns_correct_priority() {
169 let map = build_map(&[
170 ("**/*.rs", "catch-all-rs"),
171 ("src/**", "src-dir"),
172 ("src/**/*.rs", "src-rs"),
173 ]);
174 assert_eq!(
176 map.get("src/main.rs").map(String::as_str),
177 Some("catch-all-rs")
178 );
179 assert_eq!(map.get("src/main.js").map(String::as_str), Some("src-dir"));
181 }
182
183 #[test]
184 fn empty_map_returns_none() {
185 let map = build_map(&[]);
186 assert_eq!(map.get("anything"), None);
187 assert!(map.is_empty());
188 assert_eq!(map.len(), 0);
189 }
190
191 #[test]
192 fn brace_expansion_works() {
193 let map = build_map(&[("*.{rs,toml}", "rust-or-toml"), ("*.js", "javascript")]);
194 assert_eq!(map.get("main.rs").map(String::as_str), Some("rust-or-toml"));
195 assert_eq!(
196 map.get("Cargo.toml").map(String::as_str),
197 Some("rust-or-toml")
198 );
199 assert_eq!(map.get("app.js").map(String::as_str), Some("javascript"));
200 assert_eq!(map.get("style.css"), None);
201 }
202
203 #[test]
204 fn compound_suffix_works() {
205 let map = build_map(&[("**/*.test.js", "test"), ("**/*.js", "js")]);
206 assert_eq!(map.get("foo.test.js").map(String::as_str), Some("test"));
207 assert_eq!(map.get("foo.js").map(String::as_str), Some("js"));
208 }
209
210 #[test]
211 fn candidate_based_matching() {
212 let map = build_map(&[("**/*.rs", "rust")]);
213 let c = Candidate::new("src\\main.rs");
214 assert_eq!(map.get_candidate(&c).map(String::as_str), Some("rust"));
215 }
216
217 #[test]
218 fn get_matches_candidate() {
219 let map = build_map(&[("**/*.rs", "rust"), ("src/**", "src")]);
220 let c = Candidate::new("src\\main.rs");
221 let matches: Vec<&str> = map
222 .get_matches_candidate(&c)
223 .iter()
224 .map(|s| s.as_str())
225 .collect();
226 assert!(matches.contains(&"rust"));
227 assert!(matches.contains(&"src"));
228 }
229
230 #[test]
231 fn is_match_delegates() {
232 let map = build_map(&[("*.rs", "rust")]);
233 assert!(map.is_match("foo.rs"));
234 assert!(!map.is_match("foo.js"));
235 }
236
237 #[test]
238 fn len_and_is_empty() {
239 let map = build_map(&[("*.rs", "rust"), ("*.toml", "toml")]);
240 assert_eq!(map.len(), 2);
241 assert!(!map.is_empty());
242 }
243
244 #[test]
245 fn literal_pattern_in_map() {
246 let map = build_map(&[("Cargo.toml", "cargo"), ("*.rs", "rust")]);
247 assert_eq!(map.get("Cargo.toml").map(String::as_str), Some("cargo"));
248 assert_eq!(map.get("foo.rs").map(String::as_str), Some("rust"));
249 }
250
251 #[test]
252 fn suffix_pattern_in_map() {
253 let map = build_map(&[("**/foo.txt", "foo"), ("*.rs", "rust")]);
254 assert_eq!(map.get("a/b/foo.txt").map(String::as_str), Some("foo"));
255 assert_eq!(map.get("foo.txt").map(String::as_str), Some("foo"));
256 }
257
258 #[test]
259 fn prefix_pattern_in_map() {
260 let map = build_map(&[("src/**", "source"), ("*.rs", "rust")]);
261 assert_eq!(map.get("src/main.rs").map(String::as_str), Some("source"));
262 assert_eq!(map.get("main.rs").map(String::as_str), Some("rust"));
263 }
264
265 #[test]
266 fn first_match_priority_across_strategies() {
267 let map = build_map(&[("Cargo.toml", "exact"), ("**/*.toml", "any-toml")]);
270 assert_eq!(map.get("Cargo.toml").map(String::as_str), Some("exact"));
271 assert_eq!(map.get("other.toml").map(String::as_str), Some("any-toml"));
272 }
273
274 #[test]
275 fn always_check_pattern_in_map() {
276 let map = build_map(&[("*.rs", "rust"), ("*", "catch-all")]);
278 assert_eq!(map.get("foo.rs").map(String::as_str), Some("rust"));
279 assert_eq!(map.get("anything").map(String::as_str), Some("catch-all"));
280 }
281}