1use crate::config::MatchRule;
6
7pub fn matches_route(
18 rule: &MatchRule,
19 path: &str,
20 method: &str,
21 headers: &[(String, String)],
22 host: &str,
23) -> bool {
24 if let Some(ref pattern) = rule.path {
26 if !match_path(pattern, path) {
27 return false;
28 }
29 }
30
31 if !rule.methods.is_empty() {
33 let method_upper = method.to_uppercase();
34 if !rule
35 .methods
36 .iter()
37 .any(|m| m.to_uppercase() == method_upper)
38 {
39 return false;
40 }
41 }
42
43 for (required_key, required_value) in &rule.headers {
45 let key_lower = required_key.to_lowercase();
46 let found = headers
47 .iter()
48 .any(|(k, v)| k.to_lowercase() == key_lower && v == required_value);
49 if !found {
50 return false;
51 }
52 }
53
54 if let Some(ref required_host) = rule.host {
56 if !host.eq_ignore_ascii_case(required_host) {
57 return false;
58 }
59 }
60
61 true
62}
63
64fn match_path(pattern: &str, path: &str) -> bool {
75 if pattern == path {
76 return true;
77 }
78
79 if let Some(prefix) = pattern.strip_suffix("/*") {
81 return path == prefix || path.starts_with(&format!("{}/", prefix));
82 }
83
84 let pattern_parts: Vec<&str> = pattern.split('/').collect();
86 let path_parts: Vec<&str> = path.split('/').collect();
87
88 if pattern_parts.len() != path_parts.len() {
89 return false;
90 }
91
92 pattern_parts
93 .iter()
94 .zip(path_parts.iter())
95 .all(|(p, s)| *p == "*" || p == s)
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use std::collections::HashMap;
102
103 fn rule(path: Option<&str>, methods: &[&str]) -> MatchRule {
104 MatchRule {
105 path: path.map(String::from),
106 methods: methods.iter().map(|s| s.to_string()).collect(),
107 headers: HashMap::new(),
108 host: None,
109 }
110 }
111
112 #[test]
113 fn test_exact_path() {
114 let r = rule(Some("/api/v1/users"), &[]);
115 assert!(matches_route(&r, "/api/v1/users", "GET", &[], ""));
116 assert!(!matches_route(&r, "/api/v1/other", "GET", &[], ""));
117 }
118
119 #[test]
120 fn test_wildcard_path() {
121 let r = rule(Some("/api/v1/*"), &[]);
122 assert!(matches_route(&r, "/api/v1/users", "GET", &[], ""));
123 assert!(matches_route(&r, "/api/v1/users/123", "GET", &[], ""));
124 assert!(matches_route(&r, "/api/v1", "GET", &[], ""));
125 assert!(!matches_route(&r, "/api/v2/users", "GET", &[], ""));
126 }
127
128 #[test]
129 fn test_segment_wildcard() {
130 let r = rule(Some("/api/*/users"), &[]);
131 assert!(matches_route(&r, "/api/v1/users", "GET", &[], ""));
132 assert!(matches_route(&r, "/api/v2/users", "GET", &[], ""));
133 assert!(!matches_route(&r, "/api/v1/posts", "GET", &[], ""));
134 }
135
136 #[test]
137 fn test_method_filter() {
138 let r = rule(Some("/api/*"), &["GET", "POST"]);
139 assert!(matches_route(&r, "/api/users", "GET", &[], ""));
140 assert!(matches_route(&r, "/api/users", "POST", &[], ""));
141 assert!(!matches_route(&r, "/api/users", "DELETE", &[], ""));
142 }
143
144 #[test]
145 fn test_header_filter() {
146 let mut r = rule(Some("/api/*"), &[]);
147 r.headers
148 .insert("x-api-version".to_string(), "1".to_string());
149
150 let headers = vec![("x-api-version".to_string(), "1".to_string())];
151 assert!(matches_route(&r, "/api/users", "GET", &headers, ""));
152
153 let headers = vec![("x-api-version".to_string(), "2".to_string())];
154 assert!(!matches_route(&r, "/api/users", "GET", &headers, ""));
155 }
156
157 #[test]
158 fn test_host_filter() {
159 let mut r = rule(Some("/api/*"), &[]);
160 r.host = Some("example.com".to_string());
161
162 assert!(matches_route(&r, "/api/users", "GET", &[], "example.com"));
163 assert!(!matches_route(&r, "/api/users", "GET", &[], "other.com"));
164 }
165}