1use std::collections::HashMap;
2
3#[derive(Debug, Default)]
5pub struct Router {
6 root: Node,
7}
8
9#[derive(Debug, Default)]
11struct Node {
12 static_children: HashMap<String, Node>,
14 param_child: Option<Box<ParamNode>>,
16 wildcard_child: Option<Box<WildcardNode>>,
19 methods: HashMap<String, RouteEntry>,
21}
22
23#[derive(Debug)]
25struct ParamNode {
26 name: String,
28 node: Node,
30}
31
32#[derive(Debug)]
37struct WildcardNode {
38 name: String,
40 node: Node,
42}
43
44#[derive(Debug, Clone)]
46pub struct RouteEntry {
47 pub operation_index: usize,
49}
50
51#[derive(Debug)]
53pub enum RouteMatch {
54 Found {
56 entry: RouteEntry,
57 params: Vec<(String, String)>,
58 },
59 MethodNotAllowed { allowed: Vec<String> },
61 NotFound,
63}
64
65#[derive(Debug, Clone)]
67enum Segment {
68 Static(String),
69 Param(String),
70 Wildcard(String),
72}
73
74impl Router {
75 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn insert(&mut self, path: &str, method: &str, entry: RouteEntry) {
85 let segments = parse_path_template(path);
86 let node = self.traverse_or_create(&segments);
87 node.methods.insert(method.to_uppercase(), entry);
88 }
89
90 pub fn lookup(&self, path: &str, method: &str) -> RouteMatch {
95 let normalized = normalize_path(path);
96 let segments: Vec<&str> = normalized.split('/').filter(|s| !s.is_empty()).collect();
97
98 let mut params = Vec::new();
99 match self.traverse_and_match(&self.root, &segments, &mut params) {
100 Some(node) => {
101 if let Some(entry) = node.methods.get(&method.to_uppercase()) {
102 RouteMatch::Found {
103 entry: entry.clone(),
104 params,
105 }
106 } else if node.methods.is_empty() {
107 RouteMatch::NotFound
108 } else {
109 let mut allowed: Vec<String> = node.methods.keys().cloned().collect();
110 allowed.sort();
111 RouteMatch::MethodNotAllowed { allowed }
112 }
113 }
114 None => RouteMatch::NotFound,
115 }
116 }
117
118 fn traverse_or_create(&mut self, segments: &[Segment]) -> &mut Node {
120 let mut current = &mut self.root;
121
122 for segment in segments {
123 current = match segment {
124 Segment::Static(name) => current.static_children.entry(name.clone()).or_default(),
125 Segment::Param(name) => {
126 if current.param_child.is_none() {
127 current.param_child = Some(Box::new(ParamNode {
128 name: name.clone(),
129 node: Node::default(),
130 }));
131 }
132 &mut current.param_child.as_mut().expect("just set above").node
133 }
134 Segment::Wildcard(name) => {
135 if current.wildcard_child.is_none() {
136 current.wildcard_child = Some(Box::new(WildcardNode {
137 name: name.clone(),
138 node: Node::default(),
139 }));
140 }
141 &mut current
142 .wildcard_child
143 .as_mut()
144 .expect("just set above")
145 .node
146 }
147 };
148 }
149
150 current
151 }
152
153 fn traverse_and_match<'a>(
156 &'a self,
157 node: &'a Node,
158 segments: &[&str],
159 params: &mut Vec<(String, String)>,
160 ) -> Option<&'a Node> {
161 if segments.is_empty() {
162 return Some(node);
163 }
164
165 let segment = segments[0];
166 let remaining = &segments[1..];
167
168 if let Some(child) = node.static_children.get(segment) {
170 if let Some(result) = self.traverse_and_match(child, remaining, params) {
171 return Some(result);
172 }
173 }
174
175 if let Some(param_child) = &node.param_child {
177 let param_len = params.len();
178 params.push((param_child.name.clone(), segment.to_string()));
179
180 if let Some(result) = self.traverse_and_match(¶m_child.node, remaining, params) {
181 return Some(result);
182 }
183
184 params.truncate(param_len);
186 }
187
188 if let Some(wildcard_child) = &node.wildcard_child {
190 let joined = std::iter::once(segment)
191 .chain(remaining.iter().copied())
192 .collect::<Vec<_>>()
193 .join("/");
194 let param_len = params.len();
195 params.push((wildcard_child.name.clone(), joined));
196
197 if let Some(result) = self.traverse_and_match(&wildcard_child.node, &[], params) {
198 return Some(result);
199 }
200
201 params.truncate(param_len);
202 }
203
204 None
205 }
206}
207
208fn parse_path_template(path: &str) -> Vec<Segment> {
210 path.split('/')
211 .filter(|s| !s.is_empty())
212 .map(|s| {
213 if s.starts_with('{') && s.ends_with('}') {
214 let inner = &s[1..s.len() - 1];
215 if let Some(base) = inner.strip_suffix('+') {
216 Segment::Wildcard(base.to_string())
217 } else {
218 Segment::Param(inner.to_string())
219 }
220 } else {
221 Segment::Static(s.to_string())
222 }
223 })
224 .collect()
225}
226
227pub fn normalize_path(path: &str) -> String {
229 let mut normalized = String::with_capacity(path.len());
230 let mut prev_slash = false;
231
232 for ch in path.chars() {
233 if ch == '/' {
234 if !prev_slash {
235 normalized.push('/');
236 }
237 prev_slash = true;
238 } else {
239 normalized.push(ch);
240 prev_slash = false;
241 }
242 }
243
244 if normalized.len() > 1 && normalized.ends_with('/') {
246 normalized.pop();
247 }
248
249 if normalized.is_empty() {
250 "/".to_string()
251 } else {
252 normalized
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
263 fn normalize_strips_trailing_slash() {
264 assert_eq!(normalize_path("/users/"), "/users");
265 }
266
267 #[test]
268 fn normalize_collapses_double_slashes() {
269 assert_eq!(normalize_path("/users//123"), "/users/123");
270 }
271
272 #[test]
273 fn normalize_preserves_root() {
274 assert_eq!(normalize_path("/"), "/");
275 }
276
277 #[test]
278 fn normalize_combined() {
279 assert_eq!(normalize_path("/users//123//orders/"), "/users/123/orders");
280 }
281
282 #[test]
285 fn route_static_path() {
286 let mut router = Router::new();
287 router.insert("/health", "GET", RouteEntry { operation_index: 0 });
288
289 match router.lookup("/health", "GET") {
290 RouteMatch::Found { entry, params } => {
291 assert_eq!(entry.operation_index, 0);
292 assert!(params.is_empty());
293 }
294 _ => panic!("expected Found"),
295 }
296 }
297
298 #[test]
299 fn route_with_parameter() {
300 let mut router = Router::new();
301 router.insert("/users/{id}", "GET", RouteEntry { operation_index: 0 });
302
303 match router.lookup("/users/123", "GET") {
304 RouteMatch::Found { entry, params } => {
305 assert_eq!(entry.operation_index, 0);
306 assert_eq!(params, vec![("id".to_string(), "123".to_string())]);
307 }
308 _ => panic!("expected Found"),
309 }
310 }
311
312 #[test]
313 fn route_with_multiple_parameters() {
314 let mut router = Router::new();
315 router.insert(
316 "/users/{userId}/orders/{orderId}",
317 "GET",
318 RouteEntry { operation_index: 0 },
319 );
320
321 match router.lookup("/users/42/orders/99", "GET") {
322 RouteMatch::Found { entry, params } => {
323 assert_eq!(entry.operation_index, 0);
324 assert_eq!(
325 params,
326 vec![
327 ("userId".to_string(), "42".to_string()),
328 ("orderId".to_string(), "99".to_string()),
329 ]
330 );
331 }
332 _ => panic!("expected Found"),
333 }
334 }
335
336 #[test]
337 fn route_not_found() {
338 let mut router = Router::new();
339 router.insert("/users", "GET", RouteEntry { operation_index: 0 });
340
341 match router.lookup("/posts", "GET") {
342 RouteMatch::NotFound => {}
343 _ => panic!("expected NotFound"),
344 }
345 }
346
347 #[test]
348 fn route_method_not_allowed() {
349 let mut router = Router::new();
350 router.insert("/users", "GET", RouteEntry { operation_index: 0 });
351 router.insert("/users", "POST", RouteEntry { operation_index: 1 });
352
353 match router.lookup("/users", "DELETE") {
354 RouteMatch::MethodNotAllowed { allowed } => {
355 assert!(allowed.contains(&"GET".to_string()));
356 assert!(allowed.contains(&"POST".to_string()));
357 }
358 _ => panic!("expected MethodNotAllowed"),
359 }
360 }
361
362 #[test]
363 fn static_takes_precedence_over_param() {
364 let mut router = Router::new();
365 router.insert("/users/me", "GET", RouteEntry { operation_index: 0 });
366 router.insert("/users/{id}", "GET", RouteEntry { operation_index: 1 });
367
368 match router.lookup("/users/me", "GET") {
370 RouteMatch::Found { entry, params } => {
371 assert_eq!(entry.operation_index, 0);
372 assert!(params.is_empty());
373 }
374 _ => panic!("expected Found for static"),
375 }
376
377 match router.lookup("/users/123", "GET") {
379 RouteMatch::Found { entry, params } => {
380 assert_eq!(entry.operation_index, 1);
381 assert_eq!(params, vec![("id".to_string(), "123".to_string())]);
382 }
383 _ => panic!("expected Found for param"),
384 }
385 }
386
387 #[test]
388 fn route_root_path() {
389 let mut router = Router::new();
390 router.insert("/", "GET", RouteEntry { operation_index: 0 });
391
392 match router.lookup("/", "GET") {
393 RouteMatch::Found { entry, .. } => {
394 assert_eq!(entry.operation_index, 0);
395 }
396 _ => panic!("expected Found for root"),
397 }
398 }
399
400 #[test]
401 fn route_normalizes_request_path() {
402 let mut router = Router::new();
403 router.insert("/users/{id}", "GET", RouteEntry { operation_index: 0 });
404
405 match router.lookup("/users/123/", "GET") {
407 RouteMatch::Found { params, .. } => {
408 assert_eq!(params, vec![("id".to_string(), "123".to_string())]);
409 }
410 _ => panic!("expected Found"),
411 }
412
413 match router.lookup("/users//456", "GET") {
415 RouteMatch::Found { params, .. } => {
416 assert_eq!(params, vec![("id".to_string(), "456".to_string())]);
417 }
418 _ => panic!("expected Found"),
419 }
420 }
421
422 #[test]
423 fn multiple_methods_same_path() {
424 let mut router = Router::new();
425 router.insert("/users", "GET", RouteEntry { operation_index: 0 });
426 router.insert("/users", "POST", RouteEntry { operation_index: 1 });
427 router.insert("/users", "DELETE", RouteEntry { operation_index: 2 });
428
429 match router.lookup("/users", "GET") {
430 RouteMatch::Found { entry, .. } => assert_eq!(entry.operation_index, 0),
431 _ => panic!("expected Found for GET"),
432 }
433
434 match router.lookup("/users", "POST") {
435 RouteMatch::Found { entry, .. } => assert_eq!(entry.operation_index, 1),
436 _ => panic!("expected Found for POST"),
437 }
438
439 match router.lookup("/users", "DELETE") {
440 RouteMatch::Found { entry, .. } => assert_eq!(entry.operation_index, 2),
441 _ => panic!("expected Found for DELETE"),
442 }
443 }
444
445 #[test]
448 fn wildcard_matches_single_segment() {
449 let mut router = Router::new();
450 router.insert("/files/{name+}", "GET", RouteEntry { operation_index: 0 });
451
452 match router.lookup("/files/readme.txt", "GET") {
453 RouteMatch::Found { entry, params } => {
454 assert_eq!(entry.operation_index, 0);
455 assert_eq!(params, vec![("name".to_string(), "readme.txt".to_string())]);
456 }
457 _ => panic!("expected Found"),
458 }
459 }
460
461 #[test]
462 fn wildcard_matches_multiple_segments() {
463 let mut router = Router::new();
464 router.insert("/files/{path+}", "GET", RouteEntry { operation_index: 0 });
465
466 match router.lookup("/files/a/b/c/file.txt", "GET") {
467 RouteMatch::Found { entry, params } => {
468 assert_eq!(entry.operation_index, 0);
469 assert_eq!(
470 params,
471 vec![("path".to_string(), "a/b/c/file.txt".to_string())]
472 );
473 }
474 _ => panic!("expected Found"),
475 }
476 }
477
478 #[test]
479 fn wildcard_with_prefix_param() {
480 let mut router = Router::new();
481 router.insert(
482 "/files/{bucket}/{key+}",
483 "GET",
484 RouteEntry { operation_index: 0 },
485 );
486
487 match router.lookup("/files/my-bucket/folder/sub/file.txt", "GET") {
489 RouteMatch::Found { entry, params } => {
490 assert_eq!(entry.operation_index, 0);
491 assert_eq!(
492 params,
493 vec![
494 ("bucket".to_string(), "my-bucket".to_string()),
495 ("key".to_string(), "folder/sub/file.txt".to_string()),
496 ]
497 );
498 }
499 _ => panic!("expected Found for multi-segment"),
500 }
501
502 match router.lookup("/files/my-bucket/file.txt", "GET") {
504 RouteMatch::Found { entry, params } => {
505 assert_eq!(entry.operation_index, 0);
506 assert_eq!(
507 params,
508 vec![
509 ("bucket".to_string(), "my-bucket".to_string()),
510 ("key".to_string(), "file.txt".to_string()),
511 ]
512 );
513 }
514 _ => panic!("expected Found for single-segment"),
515 }
516 }
517
518 #[test]
519 fn static_takes_precedence_over_wildcard() {
520 let mut router = Router::new();
521 router.insert("/files/special", "GET", RouteEntry { operation_index: 0 });
522 router.insert("/files/{path+}", "GET", RouteEntry { operation_index: 1 });
523
524 match router.lookup("/files/special", "GET") {
526 RouteMatch::Found { entry, params } => {
527 assert_eq!(entry.operation_index, 0);
528 assert!(params.is_empty());
529 }
530 _ => panic!("expected Found for static"),
531 }
532
533 match router.lookup("/files/other/file.txt", "GET") {
535 RouteMatch::Found { entry, params } => {
536 assert_eq!(entry.operation_index, 1);
537 assert_eq!(
538 params,
539 vec![("path".to_string(), "other/file.txt".to_string())]
540 );
541 }
542 _ => panic!("expected Found for wildcard"),
543 }
544 }
545
546 #[test]
547 fn param_takes_precedence_over_wildcard() {
548 let mut router = Router::new();
549 router.insert("/files/{name}", "GET", RouteEntry { operation_index: 0 });
550 router.insert("/files/{path+}", "GET", RouteEntry { operation_index: 1 });
551
552 match router.lookup("/files/readme.txt", "GET") {
554 RouteMatch::Found { entry, params } => {
555 assert_eq!(entry.operation_index, 0);
556 assert_eq!(params, vec![("name".to_string(), "readme.txt".to_string())]);
557 }
558 _ => panic!("expected Found for param"),
559 }
560
561 match router.lookup("/files/a/b.txt", "GET") {
563 RouteMatch::Found { entry, params } => {
564 assert_eq!(entry.operation_index, 1);
565 assert_eq!(params, vec![("path".to_string(), "a/b.txt".to_string())]);
566 }
567 _ => panic!("expected Found for wildcard"),
568 }
569 }
570}