1fn parse_pattern(pattern: &str) -> (Option<&str>, &str) {
14 let pattern = pattern.trim();
15
16 let methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE"];
18
19 for method in &methods {
20 if pattern.starts_with(method) {
21 let rest = &pattern[method.len()..];
22 if rest.starts_with(' ') || rest.starts_with('\t') {
24 let path_pattern = rest.trim_start();
25 return (Some(method), path_pattern);
26 }
27 }
28 }
29
30 (None, pattern)
31}
32
33pub fn matches_pattern(path: &str, pattern: &str) -> bool {
37 matches_pattern_with_method(None, path, pattern)
38}
39
40pub fn matches_pattern_with_method(method: Option<&str>, path: &str, pattern: &str) -> bool {
47 let (pattern_method, path_pattern) = parse_pattern(pattern);
48
49 if let Some(required_method) = pattern_method {
51 if let Some(actual_method) = method {
52 if required_method != actual_method {
53 return false;
54 }
55 } else {
56 return false;
58 }
59 }
60
61 matches_path_pattern(path, path_pattern)
63}
64
65fn matches_path_pattern(path: &str, pattern: &str) -> bool {
67 let segments: Vec<&str> = pattern.split('*').collect();
69
70 if segments.len() == 1 {
71 return path == pattern;
73 }
74
75 let mut current_pos = 0;
76
77 for (i, segment) in segments.iter().enumerate() {
78 if i == 0 {
79 if !segment.is_empty() && !path.starts_with(segment) {
81 return false;
82 }
83 current_pos = segment.len();
84 } else if i == segments.len() - 1 {
85 if !segment.is_empty() && !path.ends_with(segment) {
87 return false;
88 }
89 if !segment.is_empty() {
91 if let Some(pos) = path[current_pos..].find(segment) {
92 if current_pos + pos + segment.len() != path.len() {
93 return false;
94 }
95 } else {
96 return false;
97 }
98 }
99 } else {
100 if let Some(pos) = path[current_pos..].find(segment) {
102 current_pos += pos + segment.len();
103 } else {
104 return false;
105 }
106 }
107 }
108
109 true
110}
111
112pub fn should_cache_path(
118 method: &str,
119 path: &str,
120 include_paths: &[String],
121 exclude_paths: &[String],
122) -> bool {
123 if !exclude_paths.is_empty() {
125 for pattern in exclude_paths {
126 if matches_pattern_with_method(Some(method), path, pattern) {
127 return false;
128 }
129 }
130 }
131
132 if include_paths.is_empty() {
134 return true;
135 }
136
137 for pattern in include_paths {
139 if matches_pattern_with_method(Some(method), path, pattern) {
140 return true;
141 }
142 }
143
144 false
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_exact_match() {
153 assert!(matches_pattern("/api/users", "/api/users"));
154 assert!(!matches_pattern("/api/users", "/api/posts"));
155 }
156
157 #[test]
158 fn test_wildcard_at_end() {
159 assert!(matches_pattern("/api/users", "/api/*"));
160 assert!(matches_pattern("/api/users/123", "/api/*"));
161 assert!(!matches_pattern("/apiv2/users", "/api/*"));
162 }
163
164 #[test]
165 fn test_wildcard_at_start() {
166 assert!(matches_pattern("/api/users", "*/users"));
167 assert!(matches_pattern("/v1/api/users", "*/users"));
168 assert!(!matches_pattern("/api/posts", "*/users"));
169 }
170
171 #[test]
172 fn test_wildcard_in_middle() {
173 assert!(matches_pattern("/api/v1/users", "/api/*/users"));
174 assert!(matches_pattern("/api/v2/users", "/api/*/users"));
175 assert!(!matches_pattern("/api/v1/posts", "/api/*/users"));
176 }
177
178 #[test]
179 fn test_multiple_wildcards() {
180 assert!(matches_pattern("/api/v1/users/123", "/api/*/users/*"));
181 assert!(matches_pattern("/api/v2/users/456", "/api/*/users/*"));
182 assert!(!matches_pattern("/api/v1/posts/123", "/api/*/users/*"));
183 }
184
185 #[test]
186 fn test_wildcard_only() {
187 assert!(matches_pattern("/anything", "*"));
188 assert!(matches_pattern("/api/users/123", "*"));
189 }
190
191 #[test]
192 fn test_should_cache_path_empty_filters() {
193 assert!(should_cache_path("GET", "/api/users", &[], &[]));
195 assert!(should_cache_path("POST", "/anything", &[], &[]));
196 }
197
198 #[test]
199 fn test_should_cache_path_include_only() {
200 let include = vec!["/api/*".to_string(), "/public/*".to_string()];
201 let exclude = vec![];
202
203 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
204 assert!(should_cache_path("GET", "/public/index.html", &include, &exclude));
205 assert!(!should_cache_path("GET", "/private/data", &include, &exclude));
206 }
207
208 #[test]
209 fn test_should_cache_path_exclude_only() {
210 let include = vec![];
211 let exclude = vec!["/admin/*".to_string(), "/private/*".to_string()];
212
213 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
214 assert!(!should_cache_path("GET", "/admin/dashboard", &include, &exclude));
215 assert!(!should_cache_path("GET", "/private/data", &include, &exclude));
216 }
217
218 #[test]
219 fn test_should_cache_path_exclude_overrides_include() {
220 let include = vec!["/api/*".to_string()];
221 let exclude = vec!["/api/admin/*".to_string()];
222
223 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
224 assert!(!should_cache_path("GET", "/api/admin/users", &include, &exclude));
225 }
226
227 #[test]
228 fn test_method_pattern_matching() {
229 assert!(matches_pattern_with_method(Some("POST"), "/api/users", "POST /api/users"));
231 assert!(!matches_pattern_with_method(Some("GET"), "/api/users", "POST /api/users"));
232
233 assert!(matches_pattern_with_method(Some("POST"), "/api/users", "POST /api/*"));
235 assert!(matches_pattern_with_method(Some("POST"), "/api/posts", "POST /api/*"));
236 assert!(!matches_pattern_with_method(Some("POST"), "/not-api/posts", "POST /api/*"));
237 assert!(!matches_pattern_with_method(Some("GET"), "/api/users", "POST /api/*"));
238
239 assert!(matches_pattern_with_method(Some("GET"), "/api/users", "/api/*"));
241 assert!(matches_pattern_with_method(Some("POST"), "/api/users", "/api/*"));
242
243 assert!(matches_pattern_with_method(Some("POST"), "/anything", "POST *"));
245 assert!(matches_pattern_with_method(Some("POST"), "/api/users/123", "POST *"));
246 assert!(!matches_pattern_with_method(Some("GET"), "/anything", "POST *"));
247 }
248
249 #[test]
250 fn test_should_cache_with_method_filters() {
251 let include = vec!["/api/*".to_string()];
252 let exclude = vec!["POST /api/*".to_string(), "PUT /api/*".to_string()];
253
254 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
256 assert!(!should_cache_path("POST", "/api/users", &include, &exclude));
258 assert!(!should_cache_path("PUT", "/api/users", &include, &exclude));
260 assert!(should_cache_path("DELETE", "/api/users", &include, &exclude));
262 }
263
264 #[test]
265 fn test_exclude_all_posts() {
266 let include = vec![];
267 let exclude = vec!["POST *".to_string()];
268
269 assert!(!should_cache_path("POST", "/api/users", &include, &exclude));
271 assert!(!should_cache_path("POST", "/anything", &include, &exclude));
272
273 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
275 assert!(should_cache_path("PUT", "/api/users", &include, &exclude));
276 }
277
278 #[test]
279 fn test_include_only_get_requests() {
280 let include = vec!["GET *".to_string()];
281 let exclude = vec![];
282
283 assert!(should_cache_path("GET", "/api/users", &include, &exclude));
285 assert!(should_cache_path("GET", "/anything", &include, &exclude));
286
287 assert!(!should_cache_path("POST", "/api/users", &include, &exclude));
289 assert!(!should_cache_path("PUT", "/api/users", &include, &exclude));
290 }
291}