1use crate::alloc::sync::Arc;
25use crate::pattern::route_matcher;
26use crate::{GatewayError, GatewayRequest};
27use alloc::collections::BTreeMap;
28use alloc::string::{String, ToString};
29use alloc::vec::Vec;
30use gateway_internal::path_template::Pattern;
31use http::Method;
32
33#[derive(Debug, Clone, Default)]
38pub struct RouteMetadata {
39 pub auth_required: Option<AuthConfig>,
41}
42
43#[derive(Debug, Clone)]
45pub struct AuthConfig {
46 pub scheme: String,
48 pub location: AuthLocation,
50 pub name: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum AuthLocation {
57 Header,
59 Query,
61 Cookie,
63}
64
65pub type AuthVerifier =
70 Arc<dyn Fn(&GatewayRequest, &RouteMetadata) -> Result<(), GatewayError> + Send + Sync>;
71
72#[derive(Clone)]
79pub struct Router<S> {
80 routes: BTreeMap<String, Vec<RouteEntry<S>>>,
81}
82
83#[derive(Clone)]
85struct RouteEntry<S> {
86 pattern: Pattern,
87 service: S,
88 metadata: RouteMetadata,
89}
90
91impl<S> Router<S> {
92 pub fn new() -> Self {
94 Self {
95 routes: BTreeMap::new(),
96 }
97 }
98
99 pub fn match_request(
111 &self,
112 method: &Method,
113 path: &str,
114 ) -> Option<(&S, BTreeMap<String, String>, &RouteMetadata)> {
115 if let Some(entries) = self.routes.get(method.as_str()) {
116 for entry in entries {
117 if let Some(captured) = route_matcher(&entry.pattern, path) {
118 return Some((&entry.service, captured, &entry.metadata));
119 }
120 }
121 }
122 None
123 }
124}
125
126impl<S> Default for Router<S> {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132pub fn route<S, C>(router: &mut Router<S>, method: Method, pattern: Pattern, service: C)
140where
141 C: Into<S>,
142{
143 route_with_metadata(router, method, pattern, service, RouteMetadata::default())
144}
145
146pub fn route_with_metadata<S, C>(
155 router: &mut Router<S>,
156 method: Method,
157 pattern: Pattern,
158 service: C,
159 metadata: RouteMetadata,
160) where
161 C: Into<S>,
162{
163 router
164 .routes
165 .entry(method.to_string())
166 .or_default()
167 .push(RouteEntry {
168 pattern,
169 service: service.into(),
170 metadata,
171 });
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use gateway_internal::path_template::{Op, OpCode};
178
179 #[derive(Clone)]
180 struct MockService;
181 impl MockService {
182 fn new() -> Self {
183 Self
184 }
185 }
186
187 #[test]
188 fn test_router_insert_and_match() {
189 let mut router: Router<MockService> = Router::new();
190 let pattern = Pattern {
191 ops: vec![Op {
192 code: OpCode::LitPush,
193 operand: 0,
194 }],
195 pool: vec!["foo".to_string()],
196 vars: vec![],
197 stack_size: 1,
198 tail_len: 0,
199 verb: None,
200 };
201 route(&mut router, Method::GET, pattern, MockService::new());
202
203 let res = router.match_request(&Method::GET, "/foo");
204 assert!(res.is_some());
205 }
206
207 #[test]
208 fn test_router_method_mismatch() {
209 let mut router: Router<MockService> = Router::new();
210 let pattern = Pattern {
211 ops: vec![Op {
212 code: OpCode::LitPush,
213 operand: 0,
214 }],
215 pool: vec!["foo".to_string()],
216 vars: vec![],
217 stack_size: 1,
218 tail_len: 0,
219 verb: None,
220 };
221 route(&mut router, Method::GET, pattern, MockService::new());
222
223 let res = router.match_request(&Method::POST, "/foo");
224 assert!(res.is_none());
225 }
226
227 #[test]
228 fn test_router_path_mismatch() {
229 let mut router: Router<MockService> = Router::new();
230 let pattern = Pattern {
231 ops: vec![Op {
232 code: OpCode::LitPush,
233 operand: 0,
234 }],
235 pool: vec!["foo".to_string()],
236 vars: vec![],
237 stack_size: 1,
238 tail_len: 0,
239 verb: None,
240 };
241 route(&mut router, Method::GET, pattern, MockService::new());
242 assert!(router.match_request(&Method::GET, "/bar").is_none());
243 }
244
245 #[test]
246 fn test_router_multiple_routes() {
247 let mut router: Router<MockService> = Router::new();
248 let p1 = Pattern {
249 ops: vec![Op {
250 code: OpCode::LitPush,
251 operand: 0,
252 }],
253 pool: vec!["foo".to_string()],
254 vars: vec![],
255 stack_size: 1,
256 tail_len: 0,
257 verb: None,
258 };
259 let p2 = Pattern {
260 ops: vec![Op {
261 code: OpCode::LitPush,
262 operand: 0,
263 }],
264 pool: vec!["bar".to_string()],
265 vars: vec![],
266 stack_size: 1,
267 tail_len: 0,
268 verb: None,
269 };
270
271 route(&mut router, Method::GET, p1, MockService::new());
272 route(&mut router, Method::GET, p2, MockService::new());
273
274 assert!(router.match_request(&Method::GET, "/foo").is_some());
275 assert!(router.match_request(&Method::GET, "/bar").is_some());
276 }
277
278 #[test]
279 fn test_router_precedence() {
280 let mut router: Router<MockService> = Router::new();
281
282 let p1 = Pattern {
284 ops: vec![Op {
285 code: OpCode::LitPush,
286 operand: 0,
287 }],
288 pool: vec!["foo".to_string()],
289 vars: vec![],
290 stack_size: 1,
291 tail_len: 0,
292 verb: None,
293 };
294 let p2 = Pattern {
296 ops: vec![
297 Op {
298 code: OpCode::Push,
299 operand: 0,
300 },
301 Op {
302 code: OpCode::Capture,
303 operand: 0,
304 },
305 ],
306 pool: vec![],
307 vars: vec!["v".to_string()],
308 stack_size: 1,
309 tail_len: 0,
310 verb: None,
311 };
312
313 route(&mut router, Method::GET, p1, MockService::new());
315 route(&mut router, Method::GET, p2, MockService::new());
316
317 let (_s, c, _) = router.match_request(&Method::GET, "/foo").unwrap();
318 assert!(c.is_empty()); let (_s, c, _) = router.match_request(&Method::GET, "/bar").unwrap();
321 assert!(!c.is_empty()); }
323
324 #[test]
325 fn test_router_metadata() {
326 let mut router: Router<MockService> = Router::new();
327 let pattern = Pattern {
328 ops: vec![Op {
329 code: OpCode::LitPush,
330 operand: 0,
331 }],
332 pool: vec!["foo".to_string()],
333 vars: vec![],
334 stack_size: 1,
335 tail_len: 0,
336 verb: None,
337 };
338 let meta = RouteMetadata {
339 auth_required: Some(AuthConfig {
340 scheme: "ApiKey".to_string(),
341 location: AuthLocation::Header,
342 name: "X-API-Key".to_string(),
343 }),
344 };
345
346 route_with_metadata(
347 &mut router,
348 Method::GET,
349 pattern,
350 MockService::new(),
351 meta.clone(),
352 );
353
354 let (_, _, matched_meta) = router.match_request(&Method::GET, "/foo").unwrap();
355 assert!(matched_meta.auth_required.is_some());
356 let auth = matched_meta.auth_required.as_ref().unwrap();
357 assert_eq!(auth.name, "X-API-Key");
358 }
359}