Skip to main content

rainier_routing/
router.rs

1//! The [`Router`] — where routes are declared — and [`CompiledRouter`], what
2//! actually serves them.
3//!
4//! The split is deliberate. Declaring a route records the middleware it wants;
5//! compiling flattens each route's group and own middleware into one pipeline,
6//! **once**, at boot. A request then costs a match and a pipeline run, with no
7//! per-request middleware assembly.
8//!
9//! Middleware is attached by value, not by name — see
10//! [`MiddlewareStack`]. Compiling is still
11//! where a [deferred](rainier_middleware::MiddlewareStack::deferred) stage is
12//! built, so middleware that needs the container fails the boot rather than the
13//! first request that reaches it.
14//!
15//! ```
16//! use rainier_container::Container;
17//! use rainier_http::{Method, Request, Response};
18//! use rainier_routing::{GroupAttributes, Router};
19//!
20//! async fn index() -> &'static str { "all posts" }
21//! async fn show() -> &'static str { "one post" }
22//!
23//! # #[tokio::main] async fn main() -> rainier_support::Result<()> {
24//! let mut router = Router::new();
25//! router.get("/posts", index).name("posts.index");
26//!
27//! router.group(GroupAttributes::new().prefix("api").name("api."), |router| {
28//!     router.get("/posts/{post}", show).name("posts.show").where_number("post");
29//! });
30//!
31//! let compiled = router.compile(&Container::new())?;
32//!
33//! let response = compiled
34//!     .dispatch(Request::builder().method(Method::GET).uri("/api/posts/7").build())
35//!     .await;
36//! assert_eq!(response.status(), 200);
37//! # Ok(()) }
38//! ```
39
40use 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/// Details of the route that matched, placed in the request's extensions so
55/// middleware and handlers can inspect it.
56#[derive(Debug, Clone)]
57pub struct MatchedRoute {
58    /// The route's name, if it has one.
59    pub name: Option<String>,
60    /// The URI pattern (not the concrete path).
61    pub uri: String,
62    /// The methods the route answers.
63    pub methods: Vec<Method>,
64}
65
66/// Attributes shared by every route declared inside a
67/// [`group`](Router::group).
68#[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    /// No attributes.
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Prepend a URI prefix to every route in the group.
83    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
84        self.prefix = prefix.into();
85        self
86    }
87
88    /// Prepend a name prefix (conventionally ending in `.`) to every *named*
89    /// route in the group.
90    pub fn name(mut self, prefix: impl Into<String>) -> Self {
91        self.name_prefix = prefix.into();
92        self
93    }
94
95    /// Apply middleware to every route in the group, outside their own.
96    ///
97    /// Takes the middleware itself, so a group is a function returning a
98    /// [`MiddlewareStack`] rather than a name in a registry:
99    ///
100    /// ```ignore
101    /// router.group(GroupAttributes::new().prefix("api").middleware(kernel::api()), |router| {
102    ///     router.get("/posts", index);
103    /// });
104    /// ```
105    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    /// Apply a parameter constraint to every route in the group that does not
111    /// set its own.
112    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/// The route table under declaration.
119#[derive(Default)]
120pub struct Router {
121    routes: Vec<Route>,
122    fallback: Option<Arc<dyn RouteHandler>>,
123}
124
125impl Router {
126    /// An empty router.
127    pub fn new() -> Self {
128        Self::default()
129    }
130
131    // --- verbs -------------------------------------------------------------
132
133    /// Register a route for `methods`, returning it for further configuration.
134    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    /// Register a route from an already-erased handler.
147    ///
148    /// The path resource routes and other generated routes take, where the
149    /// handler was built dynamically rather than inferred from a function
150    /// signature.
151    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    /// `GET` (and `HEAD`).
162    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    /// `POST`.
170    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    /// `PUT`.
178    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    /// `PATCH`.
186    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    /// `DELETE`.
194    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    /// `OPTIONS`.
202    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    /// Every common method.
210    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    /// A permanent redirect from `from` to `to`.
229    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    /// The handler for requests that match nothing.
238    ///
239    /// Without one, an unmatched request is a plain `404`.
240    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    // --- groups ------------------------------------------------------------
248
249    /// Declare routes that share a prefix, a name prefix, middleware, or
250    /// parameter constraints.
251    ///
252    /// Groups nest: the attributes are applied to every route the closure
253    /// added, *after* any inner group has applied its own — so the outer
254    /// prefix ends up outermost and the outer middleware runs first.
255    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    /// Merge another router's routes into this one, keeping their order.
268    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    // --- inspection --------------------------------------------------------
276
277    /// Every declared route, in declaration order.
278    pub fn routes(&self) -> &[Route] {
279        &self.routes
280    }
281
282    /// Mutable access to the declared routes.
283    pub fn routes_mut(&mut self) -> &mut [Route] {
284        &mut self.routes
285    }
286
287    /// How many routes are declared.
288    pub fn len(&self) -> usize {
289        self.routes.len()
290    }
291
292    /// Whether no routes are declared.
293    pub fn is_empty(&self) -> bool {
294        self.routes.is_empty()
295    }
296
297    /// The URI pattern of a named route.
298    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    /// Every named route, as `(name, uri)`.
303    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    // --- compilation -------------------------------------------------------
311
312    /// Build every route's pipeline.
313    ///
314    /// Takes no registry: a route holds its middleware, so there is nothing to
315    /// look up. It takes the **container**, because this is where every
316    /// [deferred](rainier_middleware::MiddlewareStack::deferred) stage is
317    /// built — the point in the lifecycle where the container is populated and
318    /// no request has arrived yet.
319    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    // Deferred stages are built here, which is why a failure names the route:
350    // "the container has no AuthManager" is a great deal less useful without
351    // knowing which route asked for one.
352    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    // Exclusion happens after resolution because a deferred stage has no
361    // identity until it is built — and by type, because a value has no other.
362    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
373/// Adapts a [`RouteHandler`] to the pipeline's [`Destination`].
374struct 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
387/// A router with every route's middleware resolved and pipeline built.
388pub struct CompiledRouter {
389    routes: Vec<CompiledRoute>,
390    fallback: Option<ReadyPipeline>,
391}
392
393impl CompiledRouter {
394    /// Match `request` to a route and run it.
395    ///
396    /// Routes are tried in **declaration order** and the first match wins, as
397    /// in Laravel. That means `/posts/create` must be declared before
398    /// `/posts/{post}`, and it is why declaration order is preserved rather
399    /// than the table being sorted by specificity.
400    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            // Route parameters and the matched route go in *before* the
419            // pipeline runs, so middleware can read them — an authorisation
420            // middleware needs `{post}` as much as the handler does.
421            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    /// A `route:list`-style summary: `(methods, uri, name, middleware)`.
448    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    /// The URI pattern of a named route.
461    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    /// Every named route, as `(name, uri)` — what the URL generator is built
469    /// from.
470    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    /// How many routes are served.
483    pub fn len(&self) -> usize {
484        self.routes.len()
485    }
486
487    /// Whether nothing is served.
488    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/// One row of a `route:list`.
503#[derive(Debug, Clone, PartialEq, Eq)]
504pub struct RouteSummary {
505    /// The methods the route answers.
506    pub methods: Vec<String>,
507    /// The URI pattern.
508    pub uri: String,
509    /// The route's name, if any.
510    pub name: Option<String>,
511    /// The middleware in its pipeline, outermost first.
512    pub middleware: Vec<&'static str>,
513}
514
515/// A `404` handler, useful as an explicit fallback.
516pub async fn not_found() -> Response {
517    Error::not_found("Not Found").into_response()
518}
519
520/// Normalise a URI the way route declaration does. Re-exported for callers
521/// building paths by hand.
522pub 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        // `create` fails the numeric constraint, so matching falls through.
617        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    /// A second middleware type, so exclusion has something to discriminate on.
685    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        // The replacement for "unknown middleware alias": the only way a route
742        // can now fail to compile is middleware that needs something the
743        // container does not have.
744        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}