1fn parse_pattern(pattern: &str) -> (Option<&str>, &str) {
12 let pattern = pattern.trim();
13
14 let methods = [
16 "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE",
17 ];
18
19 for method in &methods {
20 if let Some(rest) = pattern.strip_prefix(method) {
21 if rest.starts_with(' ') || rest.starts_with('\t') {
23 let path_pattern = rest.trim_start();
24 return (Some(method), path_pattern);
25 }
26 }
27 }
28
29 (None, pattern)
30}
31
32pub fn matches_pattern(path: &str, pattern: &str) -> bool {
36 matches_pattern_with_method(None, path, pattern)
37}
38
39pub fn matches_pattern_with_method(method: Option<&str>, path: &str, pattern: &str) -> bool {
46 let (pattern_method, path_pattern) = parse_pattern(pattern);
47
48 if let Some(required_method) = pattern_method {
50 if let Some(actual_method) = method {
51 if required_method != actual_method {
52 return false;
53 }
54 } else {
55 return false;
57 }
58 }
59
60 matches_path_pattern(path, path_pattern)
62}
63
64fn matches_path_pattern(path: &str, pattern: &str) -> bool {
66 let segments: Vec<&str> = pattern.split('*').collect();
68
69 if segments.len() == 1 {
70 return path == pattern;
72 }
73
74 let mut current_pos = 0;
75
76 for (i, segment) in segments.iter().enumerate() {
77 if i == 0 {
78 if !segment.is_empty() && !path.starts_with(segment) {
80 return false;
81 }
82 current_pos = segment.len();
83 } else if i == segments.len() - 1 {
84 if !segment.is_empty() && !path.ends_with(segment) {
86 return false;
87 }
88 if !segment.is_empty() {
90 if let Some(pos) = path[current_pos..].find(segment) {
91 if current_pos + pos + segment.len() != path.len() {
92 return false;
93 }
94 } else {
95 return false;
96 }
97 }
98 } else {
99 if let Some(pos) = path[current_pos..].find(segment) {
101 current_pos += pos + segment.len();
102 } else {
103 return false;
104 }
105 }
106 }
107
108 true
109}
110
111pub fn should_cache_path(
117 method: &str,
118 path: &str,
119 include_paths: &[String],
120 exclude_paths: &[String],
121) -> bool {
122 if !exclude_paths.is_empty() {
124 for pattern in exclude_paths {
125 if matches_pattern_with_method(Some(method), path, pattern) {
126 return false;
127 }
128 }
129 }
130
131 if include_paths.is_empty() {
133 return true;
134 }
135
136 for pattern in include_paths {
138 if matches_pattern_with_method(Some(method), path, pattern) {
139 return true;
140 }
141 }
142
143 false
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_exact_match() {
152 assert!(matches_pattern("/api/users", "/api/users"));
153 assert!(!matches_pattern("/api/users", "/api/posts"));
154 }
155
156 #[test]
157 fn test_wildcard_at_end() {
158 assert!(matches_pattern("/api/users", "/api/*"));
159 assert!(matches_pattern("/api/users/123", "/api/*"));
160 assert!(!matches_pattern("/apiv2/users", "/api/*"));
161 }
162
163 #[test]
164 fn test_wildcard_at_start() {
165 assert!(matches_pattern("/api/users", "*/users"));
166 assert!(matches_pattern("/v1/api/users", "*/users"));
167 assert!(!matches_pattern("/api/posts", "*/users"));
168 }
169
170 #[test]
171 fn test_wildcard_in_middle() {
172 assert!(matches_pattern("/api/v1/users", "/api/*/users"));
173 assert!(matches_pattern("/api/v2/users", "/api/*/users"));
174 assert!(!matches_pattern("/api/v1/posts", "/api/*/users"));
175 }
176
177 #[test]
178 fn test_multiple_wildcards() {
179 assert!(matches_pattern("/api/v1/users/123", "/api/*/users/*"));
180 assert!(matches_pattern("/api/v2/users/456", "/api/*/users/*"));
181 assert!(!matches_pattern("/api/v1/posts/123", "/api/*/users/*"));
182 }
183
184 #[test]
185 fn test_wildcard_only() {
186 assert!(matches_pattern("/anything", "*"));
187 assert!(matches_pattern("/api/users/123", "*"));
188 }
189
190 #[test]
191 fn test_should_cache_path_empty_filters() {
192 assert!(should_cache_path("GET", "/api/users", &[], &[]));
194 assert!(should_cache_path("POST", "/anything", &[], &[]));
195 }
196
197 #[test]
198 fn test_should_cache_path_include_only() {
199 let include = vec!["/api/*".to_string(), "/public/*".to_string()];
200 let exclude = vec![];
201
202 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
203 assert!(should_cache_path(
204 "GET",
205 "/public/index.html",
206 &include,
207 &exclude
208 ));
209 assert!(!should_cache_path(
210 "GET",
211 "/private/data",
212 &include,
213 &exclude
214 ));
215 }
216
217 #[test]
218 fn test_should_cache_path_exclude_only() {
219 let include = vec![];
220 let exclude = vec!["/admin/*".to_string(), "/private/*".to_string()];
221
222 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
223 assert!(!should_cache_path(
224 "GET",
225 "/admin/dashboard",
226 &include,
227 &exclude
228 ));
229 assert!(!should_cache_path(
230 "GET",
231 "/private/data",
232 &include,
233 &exclude
234 ));
235 }
236
237 #[test]
238 fn test_should_cache_path_exclude_overrides_include() {
239 let include = vec!["/api/*".to_string()];
240 let exclude = vec!["/api/admin/*".to_string()];
241
242 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
243 assert!(!should_cache_path(
244 "GET",
245 "/api/admin/users",
246 &include,
247 &exclude
248 ));
249 }
250
251 #[test]
252 fn test_method_pattern_matching() {
253 assert!(matches_pattern_with_method(
255 Some("POST"),
256 "/api/users",
257 "POST /api/users"
258 ));
259 assert!(!matches_pattern_with_method(
260 Some("GET"),
261 "/api/users",
262 "POST /api/users"
263 ));
264
265 assert!(matches_pattern_with_method(
267 Some("POST"),
268 "/api/users",
269 "POST /api/*"
270 ));
271 assert!(matches_pattern_with_method(
272 Some("POST"),
273 "/api/posts",
274 "POST /api/*"
275 ));
276 assert!(!matches_pattern_with_method(
277 Some("POST"),
278 "/not-api/posts",
279 "POST /api/*"
280 ));
281 assert!(!matches_pattern_with_method(
282 Some("GET"),
283 "/api/users",
284 "POST /api/*"
285 ));
286
287 assert!(matches_pattern_with_method(
289 Some("GET"),
290 "/api/users",
291 "/api/*"
292 ));
293 assert!(matches_pattern_with_method(
294 Some("POST"),
295 "/api/users",
296 "/api/*"
297 ));
298
299 assert!(matches_pattern_with_method(
301 Some("POST"),
302 "/anything",
303 "POST *"
304 ));
305 assert!(matches_pattern_with_method(
306 Some("POST"),
307 "/api/users/123",
308 "POST *"
309 ));
310 assert!(!matches_pattern_with_method(
311 Some("GET"),
312 "/anything",
313 "POST *"
314 ));
315 }
316
317 #[test]
318 fn test_should_cache_with_method_filters() {
319 let include = vec!["/api/*".to_string()];
320 let exclude = vec!["POST /api/*".to_string(), "PUT /api/*".to_string()];
321
322 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
324 assert!(!should_cache_path("POST", "/api/users", &include, &exclude));
326 assert!(!should_cache_path("PUT", "/api/users", &include, &exclude));
328 assert!(should_cache_path(
330 "DELETE",
331 "/api/users",
332 &include,
333 &exclude
334 ));
335 }
336
337 #[test]
338 fn test_exclude_all_posts() {
339 let include = vec![];
340 let exclude = vec!["POST *".to_string()];
341
342 assert!(!should_cache_path("POST", "/api/users", &include, &exclude));
344 assert!(!should_cache_path("POST", "/anything", &include, &exclude));
345
346 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
348 assert!(should_cache_path("PUT", "/api/users", &include, &exclude));
349 }
350
351 #[test]
352 fn test_include_only_get_requests() {
353 let include = vec!["GET *".to_string()];
354 let exclude = vec![];
355
356 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
358 assert!(should_cache_path("GET", "/anything", &include, &exclude));
359
360 assert!(!should_cache_path("POST", "/api/users", &include, &exclude));
362 assert!(!should_cache_path("PUT", "/api/users", &include, &exclude));
363 }
364}