1use std::collections::HashMap;
41use std::sync::Arc;
42
43use rainier_container::Container;
44use rainier_http::{IntoResponse, Method, Request, Response};
45use rainier_middleware::{
46 ConcreteMiddleware, Destination, IntoMiddlewareStack, Middleware, MiddlewareStack, Pipeline,
47 ReadyPipeline,
48};
49use rainier_support::{BoxedFuture, Error, Result};
50
51use crate::handler::{IntoRouteHandler, RouteHandler};
52use crate::route::{normalise_uri, ParamConstraint, Route};
53
54#[derive(Debug, Clone)]
57pub struct MatchedRoute {
58 pub name: Option<String>,
60 pub uri: String,
62 pub methods: Vec<Method>,
64}
65
66#[derive(Debug, Default, Clone)]
69pub struct GroupAttributes {
70 prefix: String,
71 name_prefix: String,
72 middleware: MiddlewareStack,
73 constraints: HashMap<String, ParamConstraint>,
74}
75
76impl GroupAttributes {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
84 self.prefix = prefix.into();
85 self
86 }
87
88 pub fn name(mut self, prefix: impl Into<String>) -> Self {
91 self.name_prefix = prefix.into();
92 self
93 }
94
95 pub fn middleware(mut self, middleware: impl IntoMiddlewareStack) -> Self {
106 self.middleware = self.middleware.with_stack(middleware.into_middleware_stack());
107 self
108 }
109
110 pub fn where_param(mut self, param: impl Into<String>, constraint: ParamConstraint) -> Self {
113 self.constraints.insert(param.into(), constraint);
114 self
115 }
116}
117
118#[derive(Default)]
120pub struct Router {
121 routes: Vec<Route>,
122 fallback: Option<Arc<dyn RouteHandler>>,
123}
124
125impl Router {
126 pub fn new() -> Self {
128 Self::default()
129 }
130
131 pub fn add<H, Args>(
135 &mut self,
136 methods: Vec<Method>,
137 uri: impl Into<String>,
138 handler: H,
139 ) -> &mut Route
140 where
141 H: IntoRouteHandler<Args>,
142 {
143 self.add_erased(methods, uri, handler.into_route_handler())
144 }
145
146 pub fn add_erased(
152 &mut self,
153 methods: Vec<Method>,
154 uri: impl Into<String>,
155 handler: Arc<dyn RouteHandler>,
156 ) -> &mut Route {
157 self.routes.push(Route::new(methods, uri, handler));
158 self.routes.last_mut().expect("just pushed")
159 }
160
161 pub fn get<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
163 where
164 H: IntoRouteHandler<Args>,
165 {
166 self.add(vec![Method::GET], uri, handler)
167 }
168
169 pub fn post<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
171 where
172 H: IntoRouteHandler<Args>,
173 {
174 self.add(vec![Method::POST], uri, handler)
175 }
176
177 pub fn put<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
179 where
180 H: IntoRouteHandler<Args>,
181 {
182 self.add(vec![Method::PUT], uri, handler)
183 }
184
185 pub fn patch<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
187 where
188 H: IntoRouteHandler<Args>,
189 {
190 self.add(vec![Method::PATCH], uri, handler)
191 }
192
193 pub fn delete<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
195 where
196 H: IntoRouteHandler<Args>,
197 {
198 self.add(vec![Method::DELETE], uri, handler)
199 }
200
201 pub fn options<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
203 where
204 H: IntoRouteHandler<Args>,
205 {
206 self.add(vec![Method::OPTIONS], uri, handler)
207 }
208
209 pub fn any<H, Args>(&mut self, uri: impl Into<String>, handler: H) -> &mut Route
211 where
212 H: IntoRouteHandler<Args>,
213 {
214 self.add(
215 vec![
216 Method::GET,
217 Method::POST,
218 Method::PUT,
219 Method::PATCH,
220 Method::DELETE,
221 Method::OPTIONS,
222 ],
223 uri,
224 handler,
225 )
226 }
227
228 pub fn redirect(&mut self, from: impl Into<String>, to: impl Into<String>) -> &mut Route {
230 let to = to.into();
231 self.get(from, move || {
232 let to = to.clone();
233 async move { rainier_http::Redirect::permanent(to) }
234 })
235 }
236
237 pub fn fallback<H, Args>(&mut self, handler: H)
241 where
242 H: IntoRouteHandler<Args>,
243 {
244 self.fallback = Some(handler.into_route_handler());
245 }
246
247 pub fn group(&mut self, attributes: GroupAttributes, declare: impl FnOnce(&mut Router)) {
256 let start = self.routes.len();
257 declare(self);
258
259 for route in &mut self.routes[start..] {
260 route.prefix_with(&attributes.prefix);
261 route.prepend_middleware(&attributes.middleware);
262 route.prepend_name(&attributes.name_prefix);
263 route.add_constraints(&attributes.constraints);
264 }
265 }
266
267 pub fn merge(&mut self, other: Router) {
269 self.routes.extend(other.routes);
270 if self.fallback.is_none() {
271 self.fallback = other.fallback;
272 }
273 }
274
275 pub fn routes(&self) -> &[Route] {
279 &self.routes
280 }
281
282 pub fn routes_mut(&mut self) -> &mut [Route] {
284 &mut self.routes
285 }
286
287 pub fn len(&self) -> usize {
289 self.routes.len()
290 }
291
292 pub fn is_empty(&self) -> bool {
294 self.routes.is_empty()
295 }
296
297 pub fn uri_for(&self, name: &str) -> Option<&str> {
299 self.routes.iter().find(|route| route.route_name() == Some(name)).map(|route| route.uri())
300 }
301
302 pub fn named_routes(&self) -> Vec<(&str, &str)> {
304 self.routes
305 .iter()
306 .filter_map(|route| route.route_name().map(|name| (name, route.uri())))
307 .collect()
308 }
309
310 pub fn compile(self, container: &Container) -> Result<CompiledRouter> {
320 let mut duplicate_names: HashMap<&str, usize> = HashMap::new();
321 for route in &self.routes {
322 if let Some(name) = route.route_name() {
323 *duplicate_names.entry(name).or_insert(0) += 1;
324 }
325 }
326 if let Some((name, _)) = duplicate_names.into_iter().find(|(_, count)| *count > 1) {
327 return Err(Error::internal(format!(
328 "two routes are both named `{name}` — route names must be unique for URL \
329 generation to be unambiguous"
330 )));
331 }
332
333 let mut compiled = Vec::with_capacity(self.routes.len());
334 for route in self.routes {
335 let pipeline = build_pipeline(&route, container)?;
336 compiled.push(CompiledRoute { route, pipeline });
337 }
338
339 let fallback = self.fallback.map(|handler| {
340 let destination: Arc<dyn Destination> = Arc::new(HandlerDestination(handler));
341 Pipeline::new().then_arc(destination)
342 });
343
344 Ok(CompiledRouter { routes: compiled, fallback })
345 }
346}
347
348fn build_pipeline(route: &Route, container: &Container) -> Result<ReadyPipeline> {
349 let resolved = route.middleware_stack().resolve(container).map_err(|e| {
353 Error::internal(format!(
354 "route `{} {}`: {e}",
355 route.methods().first().map(Method::as_str).unwrap_or("?"),
356 route.uri()
357 ))
358 })?;
359
360 let excluded = route.excluded_middleware();
363 let resolved: Vec<Arc<dyn Middleware>> = resolved
364 .into_iter()
365 .filter(|stage| !excluded.contains(&ConcreteMiddleware::concrete_type_id(&**stage)))
366 .collect();
367
368 let destination: Arc<dyn Destination> =
369 Arc::new(HandlerDestination(Arc::clone(route.handler())));
370 Ok(Pipeline::new().through_all(resolved).then_arc(destination))
371}
372
373struct HandlerDestination(Arc<dyn RouteHandler>);
375
376impl Destination for HandlerDestination {
377 fn call(&self, request: Request) -> BoxedFuture<Response> {
378 self.0.call(request)
379 }
380}
381
382struct CompiledRoute {
383 route: Route,
384 pipeline: ReadyPipeline,
385}
386
387pub struct CompiledRouter {
389 routes: Vec<CompiledRoute>,
390 fallback: Option<ReadyPipeline>,
391}
392
393impl CompiledRouter {
394 pub async fn dispatch(&self, mut request: Request) -> Response {
401 let path = request.path().to_string();
402 let mut allowed: Vec<Method> = Vec::new();
403
404 for compiled in &self.routes {
405 let Some(params) = compiled.route.match_path(&path) else {
406 continue;
407 };
408
409 if !compiled.route.accepts(request.method()) {
410 for method in compiled.route.methods() {
411 if !allowed.contains(method) {
412 allowed.push(method.clone());
413 }
414 }
415 continue;
416 }
417
418 request.set_route_params(params);
422 request.extensions_mut().insert(MatchedRoute {
423 name: compiled.route.route_name().map(str::to_string),
424 uri: compiled.route.uri().to_string(),
425 methods: compiled.route.methods().to_vec(),
426 });
427
428 return compiled.pipeline.run(request).await;
429 }
430
431 if !allowed.is_empty() {
432 let allow = allowed.iter().map(Method::as_str).collect::<Vec<_>>().join(", ");
433 return Error::new(
434 rainier_support::ErrorKind::MethodNotAllowed,
435 format!("The {} method is not supported for this route.", request.method()),
436 )
437 .into_response()
438 .with_header("allow", &allow);
439 }
440
441 match &self.fallback {
442 Some(fallback) => fallback.run(request).await,
443 None => Error::not_found(format!("No route matches {path}")).into_response(),
444 }
445 }
446
447 pub fn describe(&self) -> Vec<RouteSummary> {
449 self.routes
450 .iter()
451 .map(|compiled| RouteSummary {
452 methods: compiled.route.methods().iter().map(|m| m.to_string()).collect(),
453 uri: compiled.route.uri().to_string(),
454 name: compiled.route.route_name().map(str::to_string),
455 middleware: compiled.pipeline.stage_names(),
456 })
457 .collect()
458 }
459
460 pub fn uri_for(&self, name: &str) -> Option<&str> {
462 self.routes
463 .iter()
464 .find(|compiled| compiled.route.route_name() == Some(name))
465 .map(|compiled| compiled.route.uri())
466 }
467
468 pub fn named_routes(&self) -> Vec<(String, String)> {
471 self.routes
472 .iter()
473 .filter_map(|compiled| {
474 compiled
475 .route
476 .route_name()
477 .map(|name| (name.to_string(), compiled.route.uri().to_string()))
478 })
479 .collect()
480 }
481
482 pub fn len(&self) -> usize {
484 self.routes.len()
485 }
486
487 pub fn is_empty(&self) -> bool {
489 self.routes.is_empty()
490 }
491}
492
493impl std::fmt::Debug for CompiledRouter {
494 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495 f.debug_struct("CompiledRouter")
496 .field("routes", &self.len())
497 .field("fallback", &self.fallback.is_some())
498 .finish()
499 }
500}
501
502#[derive(Debug, Clone, PartialEq, Eq)]
504pub struct RouteSummary {
505 pub methods: Vec<String>,
507 pub uri: String,
509 pub name: Option<String>,
511 pub middleware: Vec<&'static str>,
513}
514
515pub async fn not_found() -> Response {
517 Error::not_found("Not Found").into_response()
518}
519
520pub fn normalise(uri: &str) -> String {
523 normalise_uri(uri)
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use crate::handler::Req;
530 use rainier_http::StatusCode;
531 use rainier_middleware::{Middleware, Next};
532
533 async fn index() -> &'static str {
534 "index"
535 }
536 async fn show(request: Req) -> String {
537 format!("show:{}", request.route_param("post").unwrap_or("?"))
538 }
539 async fn store() -> &'static str {
540 "store"
541 }
542
543 async fn body_of(response: Response) -> String {
544 String::from_utf8(response.into_http().into_body().collect().await.unwrap().to_vec())
545 .unwrap()
546 }
547
548 fn get(uri: &str) -> Request {
549 Request::builder().method(Method::GET).uri(uri).build()
550 }
551
552 fn compile(router: Router) -> CompiledRouter {
553 router.compile(&Container::new()).expect("compiles")
554 }
555
556 #[tokio::test]
557 async fn dispatches_to_a_static_route() {
558 let mut router = Router::new();
559 router.get("/posts", index);
560
561 let response = compile(router).dispatch(get("/posts")).await;
562 assert_eq!(body_of(response).await, "index");
563 }
564
565 #[tokio::test]
566 async fn captures_route_parameters() {
567 let mut router = Router::new();
568 router.get("/posts/{post}", show);
569
570 let response = compile(router).dispatch(get("/posts/7")).await;
571 assert_eq!(body_of(response).await, "show:7");
572 }
573
574 #[tokio::test]
575 async fn an_unmatched_path_is_a_404() {
576 let mut router = Router::new();
577 router.get("/posts", index);
578
579 let response = compile(router).dispatch(get("/nope")).await;
580 assert_eq!(response.status(), StatusCode::NOT_FOUND);
581 }
582
583 #[tokio::test]
584 async fn a_matched_path_with_the_wrong_method_is_a_405_listing_what_is_allowed() {
585 let mut router = Router::new();
586 router.get("/posts", index);
587 router.post("/posts", store);
588
589 let request = Request::builder().method(Method::DELETE).uri("/posts").build();
590 let response = compile(router).dispatch(request).await;
591
592 assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
593 let allow = response.header("allow").unwrap().to_string();
594 assert!(allow.contains("GET"), "{allow}");
595 assert!(allow.contains("POST"), "{allow}");
596 }
597
598 #[tokio::test]
599 async fn the_first_declared_route_wins() {
600 let mut router = Router::new();
601 router.get("/posts/create", || async { "create form" });
602 router.get("/posts/{post}", show);
603
604 let compiled = compile(router);
605 assert_eq!(body_of(compiled.dispatch(get("/posts/create")).await).await, "create form");
606 assert_eq!(body_of(compiled.dispatch(get("/posts/9")).await).await, "show:9");
607 }
608
609 #[tokio::test]
610 async fn a_constraint_lets_a_later_route_win() {
611 let mut router = Router::new();
612 router.get("/posts/{post}", show).where_number("post");
613 router.get("/posts/create", || async { "create form" });
614
615 let compiled = compile(router);
616 assert_eq!(body_of(compiled.dispatch(get("/posts/create")).await).await, "create form");
618 assert_eq!(body_of(compiled.dispatch(get("/posts/9")).await).await, "show:9");
619 }
620
621 #[tokio::test]
622 async fn the_fallback_catches_everything_unmatched() {
623 let mut router = Router::new();
624 router.get("/posts", index);
625 router.fallback(|| async { "nothing here" });
626
627 let response = compile(router).dispatch(get("/anywhere/else")).await;
628 assert_eq!(body_of(response).await, "nothing here");
629 }
630
631 #[tokio::test]
632 async fn groups_apply_a_prefix_and_a_name() {
633 let mut router = Router::new();
634 router.group(GroupAttributes::new().prefix("api/v1").name("api."), |router| {
635 router.get("/posts", index).name("posts.index");
636 });
637
638 let compiled = compile(router);
639 assert_eq!(compiled.uri_for("api.posts.index"), Some("/api/v1/posts"));
640 assert_eq!(compiled.dispatch(get("/api/v1/posts")).await.status(), StatusCode::OK);
641 assert_eq!(compiled.dispatch(get("/posts")).await.status(), StatusCode::NOT_FOUND);
642 }
643
644 #[tokio::test]
645 async fn groups_nest_outermost_first() {
646 let mut router = Router::new();
647 router.group(GroupAttributes::new().prefix("api").name("api."), |router| {
648 router.group(GroupAttributes::new().prefix("v1").name("v1."), |router| {
649 router.get("/posts", index).name("posts.index");
650 });
651 });
652
653 let compiled = compile(router);
654 assert_eq!(compiled.uri_for("api.v1.posts.index"), Some("/api/v1/posts"));
655 }
656
657 #[tokio::test]
658 async fn group_constraints_apply_to_every_route_that_lacks_its_own() {
659 let mut router = Router::new();
660 router.group(
661 GroupAttributes::new().where_param("post", ParamConstraint::Number),
662 |router| {
663 router.get("/posts/{post}", show);
664 },
665 );
666
667 let compiled = compile(router);
668 assert_eq!(compiled.dispatch(get("/posts/7")).await.status(), StatusCode::OK);
669 assert_eq!(compiled.dispatch(get("/posts/abc")).await.status(), StatusCode::NOT_FOUND);
670 }
671
672 struct Tag(&'static str);
673
674 #[async_trait::async_trait]
675 impl Middleware for Tag {
676 async fn handle(&self, request: Request, next: Next) -> Response {
677 next.run(request).await.with_added_header("x-tag", self.0)
678 }
679 fn name(&self) -> &'static str {
680 "Tag"
681 }
682 }
683
684 struct Audit;
686
687 #[async_trait::async_trait]
688 impl Middleware for Audit {
689 async fn handle(&self, request: Request, next: Next) -> Response {
690 next.run(request).await.with_added_header("x-tag", "audit")
691 }
692 }
693
694 #[tokio::test]
695 async fn route_and_group_middleware_both_run() {
696 let mut router = Router::new();
697 router.group(GroupAttributes::new().middleware(Tag("group")), |router| {
698 router.get("/posts", index).middleware(Tag("route"));
699 });
700
701 let compiled = router.compile(&Container::new()).unwrap();
702 let response = compiled.dispatch(get("/posts")).await;
703 let tags: Vec<_> =
704 response.headers().get_all("x-tag").iter().map(|v| v.to_str().unwrap()).collect();
705 assert_eq!(tags.len(), 2);
706 }
707
708 #[tokio::test]
709 async fn a_tuple_attaches_several_in_order() {
710 let mut router = Router::new();
711 router.get("/posts", index).middleware((Tag("first"), Audit));
712
713 let compiled = router.compile(&Container::new()).unwrap();
714 let response = compiled.dispatch(get("/posts")).await;
715 let tags: Vec<_> =
716 response.headers().get_all("x-tag").iter().map(|v| v.to_str().unwrap()).collect();
717 assert_eq!(tags, vec!["audit", "first"], "the outermost runs last on the way out");
718 }
719
720 #[tokio::test]
721 async fn a_route_can_opt_out_of_group_middleware_by_type() {
722 let mut router = Router::new();
723 router.group(GroupAttributes::new().middleware((Tag("group"), Audit)), |router| {
724 router.get("/open", index).without_middleware::<Audit>();
725 router.get("/closed", index);
726 });
727
728 let compiled = router.compile(&Container::new()).unwrap();
729
730 let open = compiled.dispatch(get("/open")).await;
731 let open_tags: Vec<_> =
732 open.headers().get_all("x-tag").iter().map(|v| v.to_str().unwrap()).collect();
733 assert_eq!(open_tags, vec!["group"], "only the excluded type is dropped");
734
735 let closed = compiled.dispatch(get("/closed")).await;
736 assert_eq!(closed.headers().get_all("x-tag").iter().count(), 2);
737 }
738
739 #[test]
740 fn a_deferred_stage_that_cannot_be_built_names_the_route() {
741 let mut router = Router::new();
745 router.get("/posts", index).middleware(
746 MiddlewareStack::new()
747 .deferred(|_| Err::<Audit, _>(Error::internal("AuthManager is not bound"))),
748 );
749
750 let err = router.compile(&Container::new()).err().expect("compilation should fail");
751 assert!(err.message().contains("/posts"), "{}", err.message());
752 assert!(err.message().contains("Audit"), "{}", err.message());
753 assert!(err.message().contains("AuthManager"), "{}", err.message());
754 }
755
756 #[test]
757 fn duplicate_route_names_are_rejected() {
758 let mut router = Router::new();
759 router.get("/a", index).name("same");
760 router.get("/b", index).name("same");
761
762 let err = router.compile(&Container::new()).err().expect("should fail");
763 assert!(err.message().contains("`same`"), "{}", err.message());
764 }
765
766 #[tokio::test]
767 async fn the_matched_route_is_readable_from_the_request() {
768 async fn reveal(request: Req) -> String {
769 request
770 .extension::<MatchedRoute>()
771 .map(|route| format!("{}|{:?}", route.uri, route.name))
772 .unwrap_or_default()
773 }
774
775 let mut router = Router::new();
776 router.get("/posts/{post}", reveal).name("posts.show");
777
778 let response = compile(router).dispatch(get("/posts/1")).await;
779 assert_eq!(body_of(response).await, "/posts/{post}|Some(\"posts.show\")");
780 }
781
782 #[tokio::test]
783 async fn redirect_routes_send_a_location() {
784 let mut router = Router::new();
785 router.redirect("/old", "/new");
786
787 let response = compile(router).dispatch(get("/old")).await;
788 assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY);
789 assert_eq!(response.header("location"), Some("/new"));
790 }
791
792 #[tokio::test]
793 async fn merging_keeps_both_route_sets() {
794 let mut web = Router::new();
795 web.get("/", index).name("home");
796
797 let mut api = Router::new();
798 api.get("/api/posts", index).name("api.posts");
799
800 web.merge(api);
801 let compiled = compile(web);
802 assert_eq!(compiled.len(), 2);
803 assert_eq!(compiled.dispatch(get("/api/posts")).await.status(), StatusCode::OK);
804 }
805
806 #[tokio::test]
807 async fn describe_reports_the_table() {
808 let mut router = Router::new();
809 router.get("/posts", index).name("posts.index").middleware(Tag("route"));
810
811 let compiled = router.compile(&Container::new()).unwrap();
812 let rows = compiled.describe();
813 assert_eq!(rows.len(), 1);
814 assert_eq!(rows[0].uri, "/posts");
815 assert_eq!(rows[0].name.as_deref(), Some("posts.index"));
816 assert!(rows[0].methods.contains(&"GET".to_string()));
817 assert_eq!(rows[0].middleware, vec!["Tag"]);
818 }
819
820 #[tokio::test]
821 async fn head_is_served_wherever_get_is() {
822 let mut router = Router::new();
823 router.get("/posts", index);
824
825 let request = Request::builder().method(Method::HEAD).uri("/posts").build();
826 assert_eq!(compile(router).dispatch(request).await.status(), StatusCode::OK);
827 }
828}