phantom_frame/
path_matcher.rs

1/// Path matching module with wildcard support
2///
3/// Supports wildcard patterns where * can appear anywhere in the pattern
4/// Example patterns: "/api/*", "/*/users", "/api/*/data"
5/// Also supports method prefixes: "POST /api/*", "GET *", "PUT /hello"
6/// Returns (method, path_pattern)
7/// Examples:
8///   "POST /api/*" -> (Some("POST"), "/api/*")
9///   "/api/*" -> (None, "/api/*")
10///   "GET *" -> (Some("GET"), "*")
11fn parse_pattern(pattern: &str) -> (Option<&str>, &str) {
12    let pattern = pattern.trim();
13    
14    // Check if pattern starts with an HTTP method
15    let methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE"];
16    
17    for method in &methods {
18        if let Some(rest) = pattern.strip_prefix(method) {
19            // Must be followed by whitespace
20            if rest.starts_with(' ') || rest.starts_with('\t') {
21                let path_pattern = rest.trim_start();
22                return (Some(method), path_pattern);
23            }
24        }
25    }
26    
27    (None, pattern)
28}
29
30/// Check if a path matches a wildcard pattern
31/// * can appear anywhere and matches any sequence of characters
32///   If method is provided, pattern can optionally specify a method prefix like "POST /api/*"
33pub fn matches_pattern(path: &str, pattern: &str) -> bool {
34    matches_pattern_with_method(None, path, pattern)
35}
36
37/// Check if a request (method + path) matches a pattern
38/// Pattern can be just a path or "METHOD /path"
39/// Examples:
40///   matches_pattern_with_method(Some("POST"), "/api/users", "POST /api/*") -> true
41///   matches_pattern_with_method(Some("GET"), "/api/users", "POST /api/*") -> false
42///   matches_pattern_with_method(Some("GET"), "/api/users", "/api/*") -> true (no method constraint)
43pub fn matches_pattern_with_method(method: Option<&str>, path: &str, pattern: &str) -> bool {
44    let (pattern_method, path_pattern) = parse_pattern(pattern);
45    
46    // If pattern specifies a method, it must match
47    if let Some(required_method) = pattern_method {
48        if let Some(actual_method) = method {
49            if required_method != actual_method {
50                return false;
51            }
52        } else {
53            // Pattern requires a method but none was provided
54            return false;
55        }
56    }
57    
58    // Now match the path part using the existing logic
59    matches_path_pattern(path, path_pattern)
60}
61
62/// Internal function to match just the path against a pattern
63fn matches_path_pattern(path: &str, pattern: &str) -> bool {
64    // Split pattern by * to get segments
65    let segments: Vec<&str> = pattern.split('*').collect();
66    
67    if segments.len() == 1 {
68        // No wildcards, exact match
69        return path == pattern;
70    }
71    
72    let mut current_pos = 0;
73    
74    for (i, segment) in segments.iter().enumerate() {
75        if i == 0 {
76            // First segment must match at the start
77            if !segment.is_empty() && !path.starts_with(segment) {
78                return false;
79            }
80            current_pos = segment.len();
81        } else if i == segments.len() - 1 {
82            // Last segment must match at the end
83            if !segment.is_empty() && !path.ends_with(segment) {
84                return false;
85            }
86            // Also ensure that the last segment appears after current_pos
87            if !segment.is_empty() {
88                if let Some(pos) = path[current_pos..].find(segment) {
89                    if current_pos + pos + segment.len() != path.len() {
90                        return false;
91                    }
92                } else {
93                    return false;
94                }
95            }
96        } else {
97            // Middle segments must appear in order
98            if let Some(pos) = path[current_pos..].find(segment) {
99                current_pos += pos + segment.len();
100            } else {
101                return false;
102            }
103        }
104    }
105    
106    true
107}
108
109/// Check if a request should be cached based on include and exclude patterns
110/// - If include_paths is empty, all paths are included
111/// - If exclude_paths is empty, no paths are excluded
112/// - exclude_paths overrides include_paths
113/// - Patterns can include method prefixes: "POST /api/*", "GET *", etc.
114pub fn should_cache_path(
115    method: &str,
116    path: &str,
117    include_paths: &[String],
118    exclude_paths: &[String],
119) -> bool {
120    // Check exclude patterns first (they override includes)
121    if !exclude_paths.is_empty() {
122        for pattern in exclude_paths {
123            if matches_pattern_with_method(Some(method), path, pattern) {
124                return false;
125            }
126        }
127    }
128    
129    // If include_paths is empty, include everything (that wasn't excluded)
130    if include_paths.is_empty() {
131        return true;
132    }
133    
134    // Check if path matches any include pattern
135    for pattern in include_paths {
136        if matches_pattern_with_method(Some(method), path, pattern) {
137            return true;
138        }
139    }
140    
141    false
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_exact_match() {
150        assert!(matches_pattern("/api/users", "/api/users"));
151        assert!(!matches_pattern("/api/users", "/api/posts"));
152    }
153
154    #[test]
155    fn test_wildcard_at_end() {
156        assert!(matches_pattern("/api/users", "/api/*"));
157        assert!(matches_pattern("/api/users/123", "/api/*"));
158        assert!(!matches_pattern("/apiv2/users", "/api/*"));
159    }
160
161    #[test]
162    fn test_wildcard_at_start() {
163        assert!(matches_pattern("/api/users", "*/users"));
164        assert!(matches_pattern("/v1/api/users", "*/users"));
165        assert!(!matches_pattern("/api/posts", "*/users"));
166    }
167
168    #[test]
169    fn test_wildcard_in_middle() {
170        assert!(matches_pattern("/api/v1/users", "/api/*/users"));
171        assert!(matches_pattern("/api/v2/users", "/api/*/users"));
172        assert!(!matches_pattern("/api/v1/posts", "/api/*/users"));
173    }
174
175    #[test]
176    fn test_multiple_wildcards() {
177        assert!(matches_pattern("/api/v1/users/123", "/api/*/users/*"));
178        assert!(matches_pattern("/api/v2/users/456", "/api/*/users/*"));
179        assert!(!matches_pattern("/api/v1/posts/123", "/api/*/users/*"));
180    }
181
182    #[test]
183    fn test_wildcard_only() {
184        assert!(matches_pattern("/anything", "*"));
185        assert!(matches_pattern("/api/users/123", "*"));
186    }
187
188    #[test]
189    fn test_should_cache_path_empty_filters() {
190        // Empty include and exclude should cache everything
191        assert!(should_cache_path("GET", "/api/users", &[], &[]));
192        assert!(should_cache_path("POST", "/anything", &[], &[]));
193    }
194
195    #[test]
196    fn test_should_cache_path_include_only() {
197        let include = vec!["/api/*".to_string(), "/public/*".to_string()];
198        let exclude = vec![];
199        
200        assert!(should_cache_path("GET", "/api/users", &include, &exclude));
201        assert!(should_cache_path("GET", "/public/index.html", &include, &exclude));
202        assert!(!should_cache_path("GET", "/private/data", &include, &exclude));
203    }
204
205    #[test]
206    fn test_should_cache_path_exclude_only() {
207        let include = vec![];
208        let exclude = vec!["/admin/*".to_string(), "/private/*".to_string()];
209        
210        assert!(should_cache_path("GET", "/api/users", &include, &exclude));
211        assert!(!should_cache_path("GET", "/admin/dashboard", &include, &exclude));
212        assert!(!should_cache_path("GET", "/private/data", &include, &exclude));
213    }
214
215    #[test]
216    fn test_should_cache_path_exclude_overrides_include() {
217        let include = vec!["/api/*".to_string()];
218        let exclude = vec!["/api/admin/*".to_string()];
219        
220        assert!(should_cache_path("GET", "/api/users", &include, &exclude));
221        assert!(!should_cache_path("GET", "/api/admin/users", &include, &exclude));
222    }
223
224    #[test]
225    fn test_method_pattern_matching() {
226        // Test exact method match
227        assert!(matches_pattern_with_method(Some("POST"), "/api/users", "POST /api/users"));
228        assert!(!matches_pattern_with_method(Some("GET"), "/api/users", "POST /api/users"));
229        
230        // Test method with wildcard
231        assert!(matches_pattern_with_method(Some("POST"), "/api/users", "POST /api/*"));
232        assert!(matches_pattern_with_method(Some("POST"), "/api/posts", "POST /api/*"));
233        assert!(!matches_pattern_with_method(Some("POST"), "/not-api/posts", "POST /api/*"));
234        assert!(!matches_pattern_with_method(Some("GET"), "/api/users", "POST /api/*"));
235        
236        // Test wildcard method matching (pattern without method should match any)
237        assert!(matches_pattern_with_method(Some("GET"), "/api/users", "/api/*"));
238        assert!(matches_pattern_with_method(Some("POST"), "/api/users", "/api/*"));
239        
240        // Test "POST *" pattern
241        assert!(matches_pattern_with_method(Some("POST"), "/anything", "POST *"));
242        assert!(matches_pattern_with_method(Some("POST"), "/api/users/123", "POST *"));
243        assert!(!matches_pattern_with_method(Some("GET"), "/anything", "POST *"));
244    }
245
246    #[test]
247    fn test_should_cache_with_method_filters() {
248        let include = vec!["/api/*".to_string()];
249        let exclude = vec!["POST /api/*".to_string(), "PUT /api/*".to_string()];
250        
251        // GET should be cached
252        assert!(should_cache_path("GET", "/api/users", &include, &exclude));
253        // POST should not be cached (excluded)
254        assert!(!should_cache_path("POST", "/api/users", &include, &exclude));
255        // PUT should not be cached (excluded)
256        assert!(!should_cache_path("PUT", "/api/users", &include, &exclude));
257        // DELETE should be cached (not excluded)
258        assert!(should_cache_path("DELETE", "/api/users", &include, &exclude));
259    }
260
261    #[test]
262    fn test_exclude_all_posts() {
263        let include = vec![];
264        let exclude = vec!["POST *".to_string()];
265        
266        // All POST requests should be excluded
267        assert!(!should_cache_path("POST", "/api/users", &include, &exclude));
268        assert!(!should_cache_path("POST", "/anything", &include, &exclude));
269        
270        // Other methods should be cached
271        assert!(should_cache_path("GET", "/api/users", &include, &exclude));
272        assert!(should_cache_path("PUT", "/api/users", &include, &exclude));
273    }
274
275    #[test]
276    fn test_include_only_get_requests() {
277        let include = vec!["GET *".to_string()];
278        let exclude = vec![];
279        
280        // Only GET requests should be included
281        assert!(should_cache_path("GET", "/api/users", &include, &exclude));
282        assert!(should_cache_path("GET", "/anything", &include, &exclude));
283        
284        // Other methods should not be cached
285        assert!(!should_cache_path("POST", "/api/users", &include, &exclude));
286        assert!(!should_cache_path("PUT", "/api/users", &include, &exclude));
287    }
288}