1use crate::pattern::route_matcher;
25use alloc::collections::BTreeMap;
26use alloc::string::{String, ToString};
27use alloc::vec::Vec;
28use gateway_internal::path_template::Pattern;
29use http::Method;
30
31#[derive(Debug, Clone, Default)]
36pub struct RouteMetadata {
37 pub auth_required: Option<AuthConfig>,
39}
40
41#[derive(Debug, Clone)]
43pub struct AuthConfig {
44 pub scheme: String,
46 pub location: AuthLocation,
48 pub name: String,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum AuthLocation {
55 Header,
57 Query,
59 Cookie,
61}
62
63#[derive(Clone)]
70pub struct Router<S> {
71 routes: BTreeMap<String, Vec<RouteEntry<S>>>,
72}
73
74#[derive(Clone)]
76struct RouteEntry<S> {
77 pattern: Pattern,
78 service: S,
79 metadata: RouteMetadata,
80}
81
82impl<S> Router<S> {
83 pub fn new() -> Self {
85 Self {
86 routes: BTreeMap::new(),
87 }
88 }
89
90 pub fn match_request(
102 &self,
103 method: &Method,
104 path: &str,
105 ) -> Option<(&S, BTreeMap<String, String>, &RouteMetadata)> {
106 if let Some(entries) = self.routes.get(method.as_str()) {
107 for entry in entries {
108 if let Some(captured) = route_matcher(&entry.pattern, path) {
109 return Some((&entry.service, captured, &entry.metadata));
110 }
111 }
112 }
113 None
114 }
115}
116
117impl<S> Default for Router<S> {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123pub fn route<S, C>(router: &mut Router<S>, method: Method, pattern: Pattern, service: C)
131where
132 C: Into<S>,
133{
134 route_with_metadata(router, method, pattern, service, RouteMetadata::default())
135}
136
137pub fn route_with_metadata<S, C>(
146 router: &mut Router<S>,
147 method: Method,
148 pattern: Pattern,
149 service: C,
150 metadata: RouteMetadata,
151) where
152 C: Into<S>,
153{
154 router
155 .routes
156 .entry(method.to_string())
157 .or_default()
158 .push(RouteEntry {
159 pattern,
160 service: service.into(),
161 metadata,
162 });
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use gateway_internal::path_template::{Op, OpCode};
169
170 #[derive(Clone)]
171 struct MockService;
172 impl MockService {
173 fn new() -> Self {
174 Self
175 }
176 }
177
178 #[test]
179 fn test_router_insert_and_match() {
180 let mut router: Router<MockService> = Router::new();
181 let pattern = Pattern {
182 ops: vec![Op {
183 code: OpCode::LitPush,
184 operand: 0,
185 }],
186 pool: vec!["foo".to_string()],
187 vars: vec![],
188 stack_size: 1,
189 tail_len: 0,
190 verb: None,
191 };
192 route(&mut router, Method::GET, pattern, MockService::new());
193
194 let res = router.match_request(&Method::GET, "/foo");
195 assert!(res.is_some());
196 }
197
198 #[test]
199 fn test_router_method_mismatch() {
200 let mut router: Router<MockService> = Router::new();
201 let pattern = Pattern {
202 ops: vec![Op {
203 code: OpCode::LitPush,
204 operand: 0,
205 }],
206 pool: vec!["foo".to_string()],
207 vars: vec![],
208 stack_size: 1,
209 tail_len: 0,
210 verb: None,
211 };
212 route(&mut router, Method::GET, pattern, MockService::new());
213
214 let res = router.match_request(&Method::POST, "/foo");
215 assert!(res.is_none());
216 }
217
218 #[test]
219 fn test_router_path_mismatch() {
220 let mut router: Router<MockService> = Router::new();
221 let pattern = Pattern {
222 ops: vec![Op {
223 code: OpCode::LitPush,
224 operand: 0,
225 }],
226 pool: vec!["foo".to_string()],
227 vars: vec![],
228 stack_size: 1,
229 tail_len: 0,
230 verb: None,
231 };
232 route(&mut router, Method::GET, pattern, MockService::new());
233 assert!(router.match_request(&Method::GET, "/bar").is_none());
234 }
235
236 #[test]
237 fn test_router_multiple_routes() {
238 let mut router: Router<MockService> = Router::new();
239 let p1 = Pattern {
240 ops: vec![Op {
241 code: OpCode::LitPush,
242 operand: 0,
243 }],
244 pool: vec!["foo".to_string()],
245 vars: vec![],
246 stack_size: 1,
247 tail_len: 0,
248 verb: None,
249 };
250 let p2 = Pattern {
251 ops: vec![Op {
252 code: OpCode::LitPush,
253 operand: 0,
254 }],
255 pool: vec!["bar".to_string()],
256 vars: vec![],
257 stack_size: 1,
258 tail_len: 0,
259 verb: None,
260 };
261
262 route(&mut router, Method::GET, p1, MockService::new());
263 route(&mut router, Method::GET, p2, MockService::new());
264
265 assert!(router.match_request(&Method::GET, "/foo").is_some());
266 assert!(router.match_request(&Method::GET, "/bar").is_some());
267 }
268
269 #[test]
270 fn test_router_precedence() {
271 let mut router: Router<MockService> = Router::new();
272
273 let p1 = Pattern {
275 ops: vec![Op {
276 code: OpCode::LitPush,
277 operand: 0,
278 }],
279 pool: vec!["foo".to_string()],
280 vars: vec![],
281 stack_size: 1,
282 tail_len: 0,
283 verb: None,
284 };
285 let p2 = Pattern {
287 ops: vec![
288 Op {
289 code: OpCode::Push,
290 operand: 0,
291 },
292 Op {
293 code: OpCode::Capture,
294 operand: 0,
295 },
296 ],
297 pool: vec![],
298 vars: vec!["v".to_string()],
299 stack_size: 1,
300 tail_len: 0,
301 verb: None,
302 };
303
304 route(&mut router, Method::GET, p1, MockService::new());
306 route(&mut router, Method::GET, p2, MockService::new());
307
308 let (_s, c, _) = router.match_request(&Method::GET, "/foo").unwrap();
309 assert!(c.is_empty()); let (_s, c, _) = router.match_request(&Method::GET, "/bar").unwrap();
312 assert!(!c.is_empty()); }
314
315 #[test]
316 fn test_router_metadata() {
317 let mut router: Router<MockService> = Router::new();
318 let pattern = Pattern {
319 ops: vec![Op {
320 code: OpCode::LitPush,
321 operand: 0,
322 }],
323 pool: vec!["foo".to_string()],
324 vars: vec![],
325 stack_size: 1,
326 tail_len: 0,
327 verb: None,
328 };
329 let meta = RouteMetadata {
330 auth_required: Some(AuthConfig {
331 scheme: "ApiKey".to_string(),
332 location: AuthLocation::Header,
333 name: "X-API-Key".to_string(),
334 }),
335 };
336
337 route_with_metadata(
338 &mut router,
339 Method::GET,
340 pattern,
341 MockService::new(),
342 meta.clone(),
343 );
344
345 let (_, _, matched_meta) = router.match_request(&Method::GET, "/foo").unwrap();
346 assert!(matched_meta.auth_required.is_some());
347 let auth = matched_meta.auth_required.as_ref().unwrap();
348 assert_eq!(auth.name, "X-API-Key");
349 }
350}