Skip to main content

churust_core/
router.rs

1//! The trie-based [`Router`], its [`Match`] result, and the [`RouteBuilder`]
2//! DSL used inside [`AppBuilder::routing`](crate::AppBuilder::routing).
3//!
4//! Routes are matched segment by segment against static text, `{param}`
5//! captures, and a trailing `{name...}` wildcard. Static segments win over
6//! parameters, which win over wildcards.
7
8use crate::handler::{boxed, BoxHandler, IntoHandler};
9use http::Method;
10use std::collections::HashMap;
11
12/// Render an `Allow` header value from the methods explicitly registered for a
13/// path, adding the ones the dispatcher answers implicitly.
14///
15/// The single source of truth for `Allow`, whichever response carries it. RFC
16/// 9110 §15.5.6 (`405`) and §9.3.7 (`OPTIONS`) describe the same fact about the
17/// same resource, and generating it in two places is how they came to disagree:
18/// a path with one `GET` route told `OPTIONS` it supported `GET, HEAD, OPTIONS`
19/// and simultaneously told a `DELETE` that it supported only `GET`.
20///
21/// Returns `None` when nothing is registered — an empty `Allow` tells a client
22/// nothing, and the right answer there is `404` rather than `405`.
23///
24/// ```
25/// use churust_core::router::allow_header_value;
26/// use http::Method;
27///
28/// // A GET route also answers HEAD and OPTIONS.
29/// assert_eq!(allow_header_value(vec![Method::GET]).as_deref(), Some("GET, HEAD, OPTIONS"));
30/// // Without a GET there is no HEAD to synthesize.
31/// assert_eq!(allow_header_value(vec![Method::POST]).as_deref(), Some("OPTIONS, POST"));
32/// assert_eq!(allow_header_value(vec![]), None);
33/// ```
34pub fn allow_header_value(mut methods: Vec<Method>) -> Option<String> {
35    if methods.is_empty() {
36        return None;
37    }
38    // RFC 9110 §9.3.2: HEAD is available wherever GET is, and the dispatcher
39    // synthesizes it. Advertising it where there is no GET would be a promise
40    // the server cannot keep.
41    if methods.contains(&Method::GET) && !methods.contains(&Method::HEAD) {
42        methods.push(Method::HEAD);
43    }
44    // The dispatcher answers OPTIONS for any path that has any route.
45    if !methods.contains(&Method::OPTIONS) {
46        methods.push(Method::OPTIONS);
47    }
48    // Sorted so the header is deterministic: two responses describing one
49    // resource should be byte-identical, and tests can compare them directly.
50    methods.sort_by(|a, b| a.as_str().cmp(b.as_str()));
51    methods.dedup();
52    Some(
53        methods
54            .iter()
55            .map(Method::as_str)
56            .collect::<Vec<_>>()
57            .join(", "),
58    )
59}
60
61/// The outcome of routing a `(method, path)` pair against the [`Router`].
62///
63/// Returned by [`Router::route`]. The framework translates each variant into a
64/// response: [`Found`](Match::Found) runs the handler,
65/// [`MethodNotAllowed`](Match::MethodNotAllowed) yields `405` with an `Allow`
66/// header, and [`NotFound`](Match::NotFound) yields `404`.
67///
68/// ```
69/// use churust_core::{boxed, Call, IntoHandler, Router, Match};
70/// # use bytes::Bytes;
71/// # use http::HeaderMap;
72/// # fn probe(p: &str) -> Call {
73/// #     Call::new(Method::GET, p.parse().unwrap(), HeaderMap::new(), Bytes::new())
74/// # }
75/// use http::Method;
76///
77/// let mut router = Router::new();
78/// router.add(Method::GET, "/users/{id}", boxed((|_c: Call| async { "ok" }).into_handler()));
79///
80/// match router.route(&Method::GET, "/users/7", &probe("/users/7")) {
81///     Match::Found { params, .. } => assert_eq!(params.get("id").unwrap(), "7"),
82///     _ => panic!("expected a match"),
83/// }
84/// assert!(matches!(router.route(&Method::GET, "/nope", &probe("/nope")), Match::NotFound));
85/// assert!(matches!(router.route(&Method::POST, "/users/7", &probe("/users/7")), Match::MethodNotAllowed { .. }));
86/// ```
87///
88/// Marked `#[non_exhaustive]`: a `match` on this enum outside `churust-core`
89/// must carry a `_` arm, so that a future routing outcome is not a breaking
90/// change.
91#[non_exhaustive]
92pub enum Match {
93    /// A handler matched the path and method. Carries the matched handler and
94    /// the captured path parameters (`{name}` -> value).
95    Found {
96        /// The handler registered for this `(path, method)`.
97        handler: BoxHandler,
98        /// The captured path parameters, keyed by name.
99        params: crate::call::Params,
100    },
101    /// The path matched a route, but not for this method. `allow` lists the
102    /// methods that *are* registered (used to build the `Allow` header).
103    MethodNotAllowed {
104        /// The methods registered for the matched path.
105        allow: Vec<Method>,
106    },
107    /// No route matched the path at all.
108    NotFound,
109    /// A path segment could not be percent-decoded — a malformed escape or
110    /// bytes that are not valid UTF-8. The dispatcher turns this into
111    /// `400 Bad Request`.
112    BadPath,
113}
114
115#[derive(Default)]
116struct Node {
117    statics: HashMap<String, Node>,
118    param: Option<(String, Box<Node>)>,      // {name}
119    wildcard: Option<(String, BoxHandlers)>, // {name...} terminal
120    handlers: BoxHandlers,
121}
122
123/// One registered route: a handler plus the guards that must pass for it to
124/// serve. Several may share a method; the first whose guards pass wins.
125struct Candidate {
126    guards: Vec<crate::guard::BoxGuard>,
127    handler: BoxHandler,
128}
129
130#[derive(Default)]
131struct BoxHandlers(HashMap<Method, Vec<Candidate>>);
132
133impl BoxHandlers {
134    /// The first candidate whose guards all pass.
135    fn pick(&self, method: &Method, call: &crate::call::Call) -> Option<&BoxHandler> {
136        self.0
137            .get(method)?
138            .iter()
139            .find_map(|c| c.guards.iter().all(|g| g.check(call)).then_some(&c.handler))
140    }
141
142    /// Methods with at least one registered route, guards ignored. Used for
143    /// `Allow` on an `OPTIONS` request, which describes the resource rather
144    /// than any particular request.
145    fn methods(&self) -> Vec<Method> {
146        self.0.keys().cloned().collect()
147    }
148
149    /// Methods that could actually serve *this* request.
150    ///
151    /// A method whose every candidate fails its guards does not count: the
152    /// route does not match, so the answer is `404`, not `405`. Reporting it
153    /// as allowed would tell a client to retry a request that can never work.
154    fn methods_matching(&self, call: &crate::call::Call) -> Vec<Method> {
155        self.0
156            .iter()
157            .filter(|(_, cands)| cands.iter().any(|c| c.guards.iter().all(|g| g.check(call))))
158            .map(|(m, _)| m.clone())
159            .collect()
160    }
161
162    fn push(&mut self, method: Method, pattern: &str, handler: BoxHandler) {
163        let entries = self.0.entry(method.clone()).or_default();
164        // Only an unguarded duplicate is a mistake: guarded siblings are the
165        // whole point of guards.
166        if entries.iter().any(|c| c.guards.is_empty()) {
167            panic!("duplicate route: {method} {pattern} is already registered");
168        }
169        entries.push(Candidate {
170            guards: Vec::new(),
171            handler,
172        });
173    }
174}
175
176impl std::fmt::Debug for Node {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        f.debug_struct("Node").finish_non_exhaustive()
179    }
180}
181
182/// A compiled, trie-based router mapping `(method, path)` to a handler.
183///
184/// Build one with [`Router::new`], register routes with [`Router::add`], and
185/// look them up with [`Router::route`]. Inside an application you usually do not
186/// touch the `Router` directly — the
187/// [`RouteBuilder`] DSL in
188/// [`AppBuilder::routing`](crate::AppBuilder::routing) populates it for you.
189///
190/// Supported pattern syntax (full paths, leading `/`):
191/// - static segments: `/users/list`
192/// - named parameters: `/users/{id}` (captured as `id`)
193/// - trailing wildcard: `/files/{path...}` (captures the remaining path,
194///   slashes included; must be the last segment)
195///
196/// ```
197/// use churust_core::{boxed, Call, IntoHandler, Router, Match};
198/// # use bytes::Bytes;
199/// # use http::HeaderMap;
200/// # fn probe(p: &str) -> Call {
201/// #     Call::new(Method::GET, p.parse().unwrap(), HeaderMap::new(), Bytes::new())
202/// # }
203/// use http::Method;
204///
205/// let mut router = Router::new();
206/// router.add(Method::GET, "/files/{path...}", boxed((|_c: Call| async { "" }).into_handler()));
207/// match router.route(&Method::GET, "/files/a/b/c.txt", &probe("/files/a/b/c.txt")) {
208///     Match::Found { params, .. } => assert_eq!(params.get("path").unwrap(), "a/b/c.txt"),
209///     _ => panic!("expected wildcard match"),
210/// }
211/// ```
212#[derive(Debug, Default)]
213pub struct Router {
214    root: Node,
215    /// Every `(method, pattern)` registered, in registration order.
216    ///
217    /// Kept alongside the trie rather than recovered from it: the trie stores
218    /// segments, so rebuilding a pattern would have to guess at how a parameter
219    /// was spelled, and anything reading this wants the spelling the
220    /// application actually wrote.
221    inventory: Vec<(Method, String)>,
222}
223
224impl Router {
225    /// Create an empty router. Equivalent to [`Router::default`].
226    pub fn new() -> Self {
227        Self::default()
228    }
229
230    /// Insert `handler` for `method` at `pattern` (the full path, e.g.
231    /// `/users/{id}`).
232    ///
233    /// Registering different methods at the same path is fine. Registering the
234    /// same `(method, path)` twice does *not* replace the earlier handler: an
235    /// unguarded duplicate is a mistake this router refuses to guess about, so
236    /// it panics. Several routes may share a `(method, path)` when guards tell
237    /// them apart — [`RouteBuilder::guard`] attaches one to the registration
238    /// just made — and those resolve first-match-wins in registration order, so
239    /// an unguarded route registered last is the fallback.
240    ///
241    /// # Panics
242    ///
243    /// Panics if a `{name...}` wildcard segment is not the final segment of the
244    /// pattern; if an unguarded route is already registered for this
245    /// `(method, path)`; or if a `{name}` at this position was already
246    /// registered under a different name, since one trie node holds one
247    /// parameter name and the second route's handler would look up a capture
248    /// that is not there.
249    ///
250    /// ```
251    /// use churust_core::{boxed, Call, IntoHandler, Router, Match};
252    /// # use bytes::Bytes;
253    /// # use http::HeaderMap;
254    /// # fn probe(p: &str) -> Call {
255    /// #     Call::new(Method::GET, p.parse().unwrap(), HeaderMap::new(), Bytes::new())
256    /// # }
257    /// use http::Method;
258    ///
259    /// let mut router = Router::new();
260    /// router.add(Method::GET, "/ping", boxed((|_c: Call| async { "pong" }).into_handler()));
261    /// assert!(matches!(router.route(&Method::GET, "/ping", &probe("/ping")), Match::Found { .. }));
262    /// ```
263    pub fn add(&mut self, method: Method, pattern: &str, handler: BoxHandler) {
264        // Recorded before the trie insert, which may panic on a duplicate or a
265        // misplaced wildcard. A router that panicked is not going to be read.
266        self.inventory.push((method.clone(), pattern.to_string()));
267
268        let mut node = &mut self.root;
269        let segments: Vec<&str> = split_segments(pattern);
270        for (i, seg) in segments.iter().enumerate() {
271            if let Some(name) = seg.strip_prefix('{').and_then(|s| s.strip_suffix("...}")) {
272                // wildcard must be terminal
273                assert!(
274                    i == segments.len() - 1,
275                    "wildcard `{{{name}...}}` must be last segment"
276                );
277                let entry = node
278                    .wildcard
279                    .get_or_insert_with(|| (name.to_string(), BoxHandlers::default()));
280                entry.1.push(method, pattern, handler);
281                return;
282            } else if let Some(name) = seg.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
283                let entry = node
284                    .param
285                    .get_or_insert_with(|| (name.to_string(), Box::new(Node::default())));
286                // One node, one parameter name. Registering `/users/{id}` and
287                // then `/users/{name}/profile` used to silently reuse `id` for
288                // both, so the second route's handler looked up `name` and found
289                // nothing — a 400 at request time with no clue why. Failing here
290                // matches how this router already treats a misplaced wildcard
291                // and a duplicate route.
292                assert!(
293                    entry.0 == name,
294                    "conflicting path parameter names at the same position: \
295                     `{{{}}}` and `{{{name}}}` (registering {method} {pattern})",
296                    entry.0
297                );
298                node = entry.1.as_mut();
299            } else {
300                node = node.statics.entry(seg.to_string()).or_default();
301            }
302        }
303        node.handlers.push(method, pattern, handler);
304    }
305
306    /// Route `path` for `method`, returning a [`Match`].
307    ///
308    /// Matching prefers static segments over `{param}` captures, and falls back
309    /// to a `{name...}` wildcard at the deepest matchable ancestor. A path that
310    /// matches a node with no handler for `method` yields
311    /// [`Match::MethodNotAllowed`]; a path that matches no node yields
312    /// [`Match::NotFound`].
313    ///
314    /// ```
315    /// use churust_core::{boxed, Call, IntoHandler, Router, Match};
316    /// # use bytes::Bytes;
317    /// # use http::HeaderMap;
318    /// # fn probe(p: &str) -> Call {
319    /// #     Call::new(Method::GET, p.parse().unwrap(), HeaderMap::new(), Bytes::new())
320    /// # }
321    /// use http::Method;
322    ///
323    /// let mut router = Router::new();
324    /// router.add(Method::GET, "/", boxed((|_c: Call| async { "home" }).into_handler()));
325    /// assert!(matches!(router.route(&Method::GET, "/", &probe("/")), Match::Found { .. }));
326    /// assert!(matches!(router.route(&Method::GET, "/missing", &probe("/missing")), Match::NotFound));
327    /// ```
328    pub fn route(&self, method: &Method, path: &str, call: &crate::call::Call) -> Match {
329        // Split first, decode second. Decoding before splitting would let %2F
330        // manufacture separators and forge extra path segments.
331        let decoded = match decode_segments(path) {
332            Some(d) => d,
333            None => return Match::BadPath,
334        };
335        let segments: Vec<&str> = decoded.iter().map(String::as_str).collect();
336        let mut params = crate::call::Params::new();
337
338        // 1. Exact search. Static beats param, and a leaf that cannot serve
339        //    this request is passed over rather than settled for.
340        let found = Self::walk_matching(&self.root, &segments, 0, &mut params, &mut |node, p| {
341            node.handlers
342                .pick(method, call)
343                .map(|h| (h.clone(), p.clone()))
344        });
345        if let Some((handler, params)) = found {
346            return Match::Found { handler, params };
347        }
348
349        //    Nothing served it. `Allow` then has to describe every leaf that
350        //    matched structurally, not just the first — they are all the same
351        //    resource as far as the client is concerned.
352        let mut exact_allow: Vec<Method> = Vec::new();
353        params.clear();
354        Self::walk_matching(&self.root, &segments, 0, &mut params, &mut |node,
355                                                                         _|
356         -> Option<
357            (),
358        > {
359            for m in node.handlers.methods_matching(call) {
360                if !exact_allow.contains(&m) {
361                    exact_allow.push(m);
362                }
363            }
364            // Never accept: visiting every matching leaf is the point.
365            None
366        });
367
368        // 2. Wildcard fallback. Reaching a node that has no handler for this
369        //    method is not the end of the search — a trailing `{name...}` at a
370        //    shallower depth may still serve the request. Without this, any
371        //    static route sharing a wildcard's prefix hides it entirely.
372        //
373        //    The walk above may have written captures into `params`. They
374        //    belong to the branch just abandoned and must not reach the
375        //    wildcard handler.
376        params.clear();
377        match Self::walk_wildcard(&self.root, &segments, 0, method, call, &mut params) {
378            Some(found @ Match::Found { .. }) => found,
379            Some(Match::MethodNotAllowed { allow: wild_allow }) => {
380                let mut allow = exact_allow;
381                for m in wild_allow {
382                    if !allow.contains(&m) {
383                        allow.push(m);
384                    }
385                }
386                Match::MethodNotAllowed { allow }
387            }
388            _ if !exact_allow.is_empty() => Match::MethodNotAllowed { allow: exact_allow },
389            _ => Match::NotFound,
390        }
391    }
392
393    /// Attach a guard to the most recently registered route at
394    /// `(method, pattern)`.
395    ///
396    /// Used by [`RouteBuilder::guard`]; registration order is what makes "most
397    /// recent" unambiguous.
398    fn attach_guard(&mut self, method: &Method, pattern: &str, g: crate::guard::BoxGuard) {
399        let mut node = &mut self.root;
400        let segments: Vec<&str> = split_segments(pattern);
401        for (i, seg) in segments.iter().enumerate() {
402            if let Some(_name) = seg.strip_prefix('{').and_then(|s| s.strip_suffix("...}")) {
403                debug_assert!(i == segments.len() - 1);
404                if let Some(entry) = node.wildcard.as_mut() {
405                    if let Some(c) = entry.1 .0.get_mut(method).and_then(|v| v.last_mut()) {
406                        c.guards.push(g);
407                    }
408                }
409                return;
410            } else if seg.starts_with('{') && seg.ends_with('}') {
411                match node.param.as_mut() {
412                    Some(entry) => node = entry.1.as_mut(),
413                    None => return,
414                }
415            } else {
416                match node.statics.get_mut(*seg) {
417                    Some(child) => node = child,
418                    None => return,
419                }
420            }
421        }
422        if let Some(c) = node.handlers.0.get_mut(method).and_then(|v| v.last_mut()) {
423            c.guards.push(g);
424        }
425    }
426
427    /// Wrap the most recently registered route at `(method, pattern)` so it
428    /// carries a per-route body limit.
429    fn attach_limit(&mut self, method: &Method, pattern: &str, max_body_bytes: usize) {
430        let mut node = &mut self.root;
431        let segments: Vec<&str> = split_segments(pattern);
432        for (i, seg) in segments.iter().enumerate() {
433            if seg
434                .strip_prefix('{')
435                .and_then(|s| s.strip_suffix("...}"))
436                .is_some()
437            {
438                debug_assert!(i == segments.len() - 1);
439                if let Some(entry) = node.wildcard.as_mut() {
440                    if let Some(c) = entry.1 .0.get_mut(method).and_then(|v| v.last_mut()) {
441                        let inner = c.handler.clone();
442                        c.handler = std::sync::Arc::new(LimitedHandler {
443                            max_body_bytes,
444                            inner,
445                        });
446                    }
447                }
448                return;
449            } else if seg.starts_with('{') && seg.ends_with('}') {
450                match node.param.as_mut() {
451                    Some(entry) => node = entry.1.as_mut(),
452                    None => return,
453                }
454            } else {
455                match node.statics.get_mut(*seg) {
456                    Some(child) => node = child,
457                    None => return,
458                }
459            }
460        }
461        if let Some(c) = node.handlers.0.get_mut(method).and_then(|v| v.last_mut()) {
462            let inner = c.handler.clone();
463            c.handler = std::sync::Arc::new(LimitedHandler {
464                max_body_bytes,
465                inner,
466            });
467        }
468    }
469
470    /// The methods registered for `path`, including any a trailing wildcard
471    /// would serve. Empty when the path matches no route at all.
472    ///
473    /// Used by the dispatcher to build the `Allow` header for an `OPTIONS`
474    /// request that has no handler of its own.
475    ///
476    /// ```
477    /// use churust_core::{boxed, Call, IntoHandler, Router};
478    /// use http::Method;
479    ///
480    /// let mut router = Router::new();
481    /// router.add(Method::GET, "/x", boxed((|_c: Call| async { "" }).into_handler()));
482    /// assert_eq!(router.methods_for("/x"), vec![Method::GET]);
483    /// assert!(router.methods_for("/nope").is_empty());
484    /// ```
485    pub fn methods_for(&self, path: &str) -> Vec<Method> {
486        let decoded = match decode_segments(path) {
487            Some(d) => d,
488            None => return Vec::new(),
489        };
490        let segments: Vec<&str> = decoded.iter().map(String::as_str).collect();
491        let mut params = crate::call::Params::new();
492        // Every structurally-matching leaf, not just the first: `OPTIONS`
493        // describes the resource, and two routes of different shapes matching
494        // one path are one resource to the client.
495        let mut out: Vec<Method> = Vec::new();
496        Self::walk_matching(&self.root, &segments, 0, &mut params, &mut |node,
497                                                                         _|
498         -> Option<
499            (),
500        > {
501            for m in node.handlers.methods() {
502                if !out.contains(&m) {
503                    out.push(m);
504                }
505            }
506            None
507        });
508
509        // The wildcard branch is enumerated directly, the same guard-free way
510        // the exact branch above is. It used to be driven by `walk_wildcard`
511        // with a synthetic `TRACE` call standing in for a real one, on the
512        // reasoning that nothing registers `TRACE`, so the walk would report
513        // `MethodNotAllowed` carrying the wildcard's method list. Both halves of
514        // that were wrong. `walk_wildcard` resolves guards against the call it
515        // is given, and a synthetic call has no headers and no authority, so it
516        // fails every guard there is: one `guard::host` on a `{p...}` route left
517        // `methods_for` empty, the dispatcher skipped its `204` arm, and the
518        // request fell through to the `405` path — where the real call *does*
519        // pass the guard, so the reply advertised `Allow: GET, HEAD, OPTIONS`
520        // while refusing the `OPTIONS` it had just named. It also assumed no
521        // application registers `TRACE`; one that does turned the probe into a
522        // `Found`, which the `if let` discarded, losing the list entirely.
523        Self::collect_wildcard_methods(&self.root, &segments, 0, &mut out);
524        out
525    }
526
527    /// Union the methods of every wildcard a path could reach, guards ignored.
528    ///
529    /// Mirrors `walk_wildcard`'s structural descent — statics and params first,
530    /// this node's own wildcard last — but unions instead of choosing, because
531    /// `Allow` describes a resource rather than the fate of one request:
532    /// `walk_wildcard` stops at the first wildcard that can serve, while every
533    /// wildcard reachable along the path is part of what this URL supports, and
534    /// that is exactly what `walk_wildcard` already unions into `Allow` when
535    /// none of them can serve.
536    fn collect_wildcard_methods(node: &Node, segs: &[&str], i: usize, out: &mut Vec<Method>) {
537        if i < segs.len() {
538            if let Some(child) = node.statics.get(segs[i]) {
539                Self::collect_wildcard_methods(child, segs, i + 1, out);
540            }
541            if let Some((_, child)) = &node.param {
542                Self::collect_wildcard_methods(child, segs, i + 1, out);
543            }
544        }
545        if let Some((_, handlers)) = &node.wildcard {
546            // The same refusal `walk_wildcard` applies: a tail whose decoded
547            // segments contain a separator is not a request this wildcard will
548            // ever serve, so advertising its methods would promise a `200` that
549            // routing has already decided against.
550            if segs[i..]
551                .iter()
552                .any(|s| s.contains('/') || s.contains('\\'))
553            {
554                return;
555            }
556            for m in handlers.methods() {
557                if !out.contains(&m) {
558                    out.push(m);
559                }
560            }
561        }
562    }
563
564    /// Every method registered anywhere in this router, deduplicated.
565    ///
566    /// Answers `OPTIONS *`, which RFC 9110 §9.3.7 defines as a question about
567    /// the server rather than about any one resource.
568    ///
569    /// ```
570    /// use churust_core::{boxed, Call, IntoHandler, Router};
571    /// use http::Method;
572    ///
573    /// let mut router = Router::new();
574    /// router.add(Method::GET, "/a", boxed((|_c: Call| async { "" }).into_handler()));
575    /// router.add(Method::POST, "/b", boxed((|_c: Call| async { "" }).into_handler()));
576    /// let mut all = router.all_methods();
577    /// all.sort_by(|a, b| a.as_str().cmp(b.as_str()));
578    /// assert_eq!(all, vec![Method::GET, Method::POST]);
579    /// ```
580    pub fn all_methods(&self) -> Vec<Method> {
581        let mut out = Vec::new();
582        Self::collect_methods(&self.root, &mut out);
583        out
584    }
585
586    /// Every registered route as `(method, pattern)`, in registration order.
587    ///
588    /// The patterns are exactly what was registered, `{id}` and `{path...}`
589    /// included, which is what makes this usable as the inventory an API
590    /// description is generated from. Routes that share a `(method, path)`
591    /// behind different guards appear once per registration, because each is a
592    /// separate route even though a client sees one URL.
593    ///
594    /// ```
595    /// use churust_core::{boxed, Call, IntoHandler, Router};
596    /// use http::Method;
597    ///
598    /// let mut router = Router::new();
599    /// router.add(Method::GET, "/users/{id}", boxed((|_c: Call| async { "" }).into_handler()));
600    /// assert_eq!(router.routes(), &[(Method::GET, "/users/{id}".to_string())]);
601    /// ```
602    pub fn routes(&self) -> &[(Method, String)] {
603        &self.inventory
604    }
605
606    fn collect_methods(node: &Node, out: &mut Vec<Method>) {
607        for m in node.handlers.methods() {
608            if !out.contains(&m) {
609                out.push(m);
610            }
611        }
612        if let Some((_, handlers)) = &node.wildcard {
613            for m in handlers.methods() {
614                if !out.contains(&m) {
615                    out.push(m);
616                }
617            }
618        }
619        for child in node.statics.values() {
620            Self::collect_methods(child, out);
621        }
622        if let Some((_, child)) = &node.param {
623            Self::collect_methods(child, out);
624        }
625    }
626
627    /// Visit every leaf whose *shape* matches, in precedence order, and return
628    /// the first result `accept` produces.
629    ///
630    /// The visitor is what makes this a search rather than a lookup. Returning
631    /// the first structurally-matching leaf regardless of whether it could
632    /// serve meant a second registered route that *did* match was unreachable:
633    /// with `POST /a/{x}` and `GET /{y}/b` registered, `GET /a/b` committed to
634    /// the first and answered `405`. A failing guard had the same effect, so a
635    /// guard on one route silently disabled an unrelated one.
636    ///
637    /// Static still beats param at each step — that is a preference, and it
638    /// survives; it just no longer ends the search. `params` is restored on the
639    /// way out of a branch that is abandoned, so captures never leak from a
640    /// route that did not serve into one that did.
641    fn walk_matching<'a, T>(
642        node: &'a Node,
643        segs: &[&str],
644        i: usize,
645        params: &mut crate::call::Params,
646        accept: &mut impl FnMut(&'a Node, &crate::call::Params) -> Option<T>,
647    ) -> Option<T> {
648        if i == segs.len() {
649            return accept(node, params);
650        }
651        let seg = segs[i];
652        if let Some(child) = node.statics.get(seg) {
653            if let Some(found) = Self::walk_matching(child, segs, i + 1, params, accept) {
654                return Some(found);
655            }
656        }
657        if let Some((name, child)) = &node.param {
658            // Remember any value this name already held: a repeated name at
659            // different depths must not be clobbered by an abandoned branch.
660            let previous = params.get(name).map(str::to_string);
661            params.insert(name.clone(), seg.to_string());
662            if let Some(found) = Self::walk_matching(child, segs, i + 1, params, accept) {
663                return Some(found);
664            }
665            match previous {
666                Some(v) => params.insert(name.clone(), v),
667                None => params.remove(name),
668            }
669        }
670        None
671    }
672
673    #[allow(clippy::too_many_arguments)]
674    fn walk_wildcard(
675        node: &Node,
676        segs: &[&str],
677        i: usize,
678        method: &Method,
679        call: &crate::call::Call,
680        params: &mut crate::call::Params,
681    ) -> Option<Match> {
682        // Descend before considering this node's own wildcard, so the *deepest*
683        // wildcard wins. Consulting it first made a shallow `{p...}` shadow every
684        // deeper one: with `/files/{p...}` and `/files/img/{q...}` registered,
685        // nothing under `/files/img/` could ever reach the second handler, and
686        // nothing reported the shadowing at registration or at match time.
687        //
688        // A `MethodNotAllowed` found deeper is remembered rather than returned:
689        // a shallower wildcard may still have a real handler for this method,
690        // and a `405` must not pre-empt a `200`.
691        let mut method_mismatch: Option<Match> = None;
692        if i < segs.len() {
693            if let Some(child) = node.statics.get(segs[i]) {
694                match Self::walk_wildcard(child, segs, i + 1, method, call, params) {
695                    Some(found @ Match::Found { .. }) => return Some(found),
696                    Some(other) => method_mismatch = Some(other),
697                    None => {}
698                }
699            }
700            if let Some((pname, child)) = &node.param {
701                params.insert(pname.clone(), segs[i].to_string());
702                match Self::walk_wildcard(child, segs, i + 1, method, call, params) {
703                    Some(found @ Match::Found { .. }) => return Some(found),
704                    Some(other) => {
705                        // The capture belongs to a branch we are abandoning.
706                        params.remove(pname);
707                        method_mismatch = Some(other);
708                    }
709                    None => params.remove(pname),
710                }
711            }
712        }
713
714        if let Some((name, handlers)) = &node.wildcard {
715            // Refuse a tail whose segments decoded to something containing a
716            // separator. Segments are split before decoding precisely so `%2F`
717            // cannot manufacture one — rejoining the decoded pieces with `/`
718            // handed that back, making `/files/a%2Fb/c` indistinguishable from
719            // `/files/a/b/c` for every consumer of the capture. `StaticFiles`
720            // has its own guard; nothing else did.
721            //
722            // This `BadPath` refuses the wildcard, not the request. `route`
723            // deliberately does not forward it as the `400` the variant is
724            // named for: the tail being unusable *here* says nothing about the
725            // rest of the trie, and a sibling `{n}` may match the same path
726            // cleanly with the separator inside one capture — which is the
727            // whole point of splitting before decoding. Promoting this into a
728            // `400` would overrule that sibling and report a malformed request
729            // where there is none. The wildcard simply stops matching, so the
730            // request lands on whatever else does, or on `404`.
731            if segs[i..]
732                .iter()
733                .any(|s| s.contains('/') || s.contains('\\'))
734            {
735                return Some(Match::BadPath);
736            }
737            let rest = segs[i..].join("/");
738            params.insert(name.clone(), rest);
739            return Some(match handlers.pick(method, call) {
740                Some(h) => Match::Found {
741                    handler: h.clone(),
742                    params: std::mem::take(params),
743                },
744                None => {
745                    params.remove(name);
746                    let mut allow = handlers.methods_matching(call);
747                    // Both depths reject the method, so `Allow` must describe
748                    // every method either of them would have served.
749                    if let Some(Match::MethodNotAllowed { allow: deeper }) = method_mismatch {
750                        for m in deeper {
751                            if !allow.contains(&m) {
752                                allow.push(m);
753                            }
754                        }
755                    }
756                    Match::MethodNotAllowed { allow }
757                }
758            });
759        }
760        method_mismatch
761    }
762}
763
764/// Seeds a per-route body limit into the call before delegating.
765///
766/// The engine's global cap still bounds what is read off the socket; this is
767/// the per-route tightening that extractors consult, which is the same place
768/// per-route extractor configuration belongs.
769struct LimitedHandler {
770    max_body_bytes: usize,
771    inner: BoxHandler,
772}
773
774impl crate::handler::Handler for LimitedHandler {
775    fn handle(&self, mut call: crate::call::Call) -> crate::handler::HandlerFuture {
776        call.insert(crate::extract::RouteBodyLimit(self.max_body_bytes));
777        let inner = self.inner.clone();
778        Box::pin(async move { inner.handle(call).await })
779    }
780}
781
782/// A handler with a scope's middleware chain wrapped around it.
783///
784/// Built once at route registration rather than consulted per request, so a
785/// route in no scope is exactly as cheap as before.
786struct ScopedHandler {
787    chain: Vec<std::sync::Arc<dyn crate::pipeline::Middleware>>,
788    inner: BoxHandler,
789}
790
791impl crate::handler::Handler for ScopedHandler {
792    fn handle(&self, call: crate::call::Call) -> crate::handler::HandlerFuture {
793        let inner = self.inner.clone();
794        let endpoint: crate::pipeline::Endpoint = std::sync::Arc::new(move |call| {
795            let inner = inner.clone();
796            Box::pin(async move { inner.handle(call).await })
797        });
798        let chain: std::collections::VecDeque<_> = self.chain.iter().cloned().collect();
799        Box::pin(crate::pipeline::Next::new(chain, endpoint).run(call))
800    }
801}
802
803fn split_segments(path: &str) -> Vec<&str> {
804    path.split('/').filter(|s| !s.is_empty()).collect()
805}
806
807/// Split `path` on `/` and percent-decode each segment individually.
808///
809/// The order is the point. Decoding the whole path first would turn `%2F` into
810/// a separator and let a request forge segments it was never routed through.
811/// Returns `None` if any segment is undecodable, which becomes `400`.
812fn decode_segments(path: &str) -> Option<Vec<String>> {
813    split_segments(path)
814        .into_iter()
815        .map(crate::path::decode_path_segment)
816        .collect()
817}
818
819/// The route-definition DSL handed to the closure in
820/// [`AppBuilder::routing`](crate::AppBuilder::routing).
821///
822/// Register handlers with the per-method helpers ([`get`](RouteBuilder::get),
823/// [`post`](RouteBuilder::post), [`put`](RouteBuilder::put),
824/// [`delete`](RouteBuilder::delete)) or the generic [`method`](RouteBuilder::method).
825/// Group related routes under a common prefix with [`route`](RouteBuilder::route),
826/// which nests cleanly. Each handler may be an extractor closure or anything
827/// implementing [`Handler`](crate::Handler).
828///
829/// ```
830/// use churust_core::{Churust, Call, TestClient};
831/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
832/// let app = Churust::server()
833///     .routing(|r| {
834///         r.get("/", |_c: Call| async { "home" });
835///         r.route("/api", |r| {
836///             r.get("/health", |_c: Call| async { "ok" });
837///         });
838///     })
839///     .build();
840/// let res = TestClient::new(app).get("/api/health").send().await;
841/// assert_eq!(res.text(), "ok");
842/// # });
843/// ```
844pub struct RouteBuilder<'r> {
845    router: &'r mut Router,
846    prefix: String,
847    /// Middleware applied to routes registered in this scope, outermost first.
848    /// Nested scopes inherit a clone, so a child cannot affect its parent.
849    chain: Vec<std::sync::Arc<dyn crate::pipeline::Middleware>>,
850    /// The most recent `(method, full path)` registered here, so `guard` knows
851    /// which route it modifies.
852    last: Option<(Method, String)>,
853}
854
855impl<'r> RouteBuilder<'r> {
856    pub(crate) fn new(router: &'r mut Router) -> Self {
857        Self {
858            router,
859            prefix: String::new(),
860            chain: Vec::new(),
861            last: None,
862        }
863    }
864
865    /// Apply `mw` to every route registered in this scope **after** this call,
866    /// including those in nested scopes.
867    ///
868    /// This is v1 design §6's scope-local interception, and the equivalent of
869    /// scope-local interception. Ordering is deliberate: a route registered before
870    /// the `intercept` call is not covered by it, so the reading order of the
871    /// block matches the behaviour.
872    ///
873    /// ```
874    /// use churust_core::{Call, Churust, Middleware, Next, Response, TestClient};
875    /// use async_trait::async_trait;
876    /// use http::StatusCode;
877    ///
878    /// struct RequireKey;
879    /// #[async_trait]
880    /// impl Middleware for RequireKey {
881    ///     async fn handle(&self, call: Call, next: Next) -> Response {
882    ///         if call.header("x-key").is_some() {
883    ///             next.run(call).await
884    ///         } else {
885    ///             Response::new(StatusCode::UNAUTHORIZED)
886    ///         }
887    ///     }
888    /// }
889    ///
890    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
891    /// let app = Churust::server()
892    ///     .routing(|r| {
893    ///         r.get("/open", |_c: Call| async { "open" });
894    ///         r.route("/admin", |r| {
895    ///             r.intercept(RequireKey);
896    ///             r.get("/panel", |_c: Call| async { "panel" });
897    ///         });
898    ///     })
899    ///     .build();
900    ///
901    /// let client = TestClient::new(app);
902    /// assert_eq!(client.get("/open").send().await.status(), StatusCode::OK);
903    /// assert_eq!(
904    ///     client.get("/admin/panel").send().await.status(),
905    ///     StatusCode::UNAUTHORIZED
906    /// );
907    /// # });
908    /// ```
909    pub fn intercept<M: crate::pipeline::Middleware>(&mut self, mw: M) -> &mut Self {
910        self.chain.push(std::sync::Arc::new(mw));
911        self
912    }
913
914    fn full(&self, path: &str) -> String {
915        let mut p = self.prefix.clone();
916        if !path.starts_with('/') {
917            p.push('/');
918        }
919        p.push_str(path);
920        p
921    }
922
923    /// Register a handler for `method` at `path`. Accepts both extractor
924    /// closures (via the `HandlerFn` family) and anything already implementing
925    /// `Handler` — including a pre-boxed `BoxHandler` — through `IntoHandler`.
926    pub fn method<Marker, H>(&mut self, method: Method, path: &str, handler: H) -> &mut Self
927    where
928        H: IntoHandler<Marker>,
929    {
930        let full = self.full(path);
931        let h = boxed(handler.into_handler());
932        // Wrap once, at registration: the scope chain is fixed by then, so the
933        // request path pays nothing for scopes it is not in.
934        let h = if self.chain.is_empty() {
935            h
936        } else {
937            std::sync::Arc::new(ScopedHandler {
938                chain: self.chain.clone(),
939                inner: h,
940            }) as BoxHandler
941        };
942        self.router.add(method.clone(), &full, h);
943        self.last = Some((method, full));
944        self
945    }
946
947    /// Attach a [`Guard`](crate::Guard) to the route just registered.
948    ///
949    /// Routes may share a `(method, path)` when guards distinguish them; the
950    /// first whose guards all pass serves the request, in registration order,
951    /// so an unguarded route registered last is the fallback. A request that
952    /// matches no candidate is a `404`.
953    ///
954    /// ```
955    /// use churust_core::{guard, Call, Churust, TestClient};
956    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
957    /// let app = Churust::server()
958    ///     .routing(|r| {
959    ///         r.get("/", |_c: Call| async { "beta" })
960    ///             .guard(guard::header("x-beta", "1"));
961    ///         r.get("/", |_c: Call| async { "stable" });
962    ///     })
963    ///     .build();
964    /// let client = TestClient::new(app);
965    /// assert_eq!(client.get("/").header("x-beta", "1").send().await.text(), "beta");
966    /// assert_eq!(client.get("/").send().await.text(), "stable");
967    /// # });
968    /// ```
969    ///
970    /// # Panics
971    ///
972    /// Panics if no route has been registered on this builder yet — there would
973    /// be nothing for the guard to apply to.
974    pub fn guard(&mut self, g: crate::guard::BoxGuard) -> &mut Self {
975        let (method, path) = self
976            .last
977            .clone()
978            .expect("guard() must follow a route registration");
979        self.router.attach_guard(&method, &path, g);
980        self
981    }
982
983    /// Cap the request body for the route just registered.
984    ///
985    /// The server-wide [`max_body_bytes`](crate::AppBuilder::max_body_bytes)
986    /// still bounds what is read off the socket; this tightens it for one
987    /// route, which is what lets an upload endpoint be generous while the rest
988    /// of the API stays strict. Body extractors (`Json<T>`, `Form<T>`) enforce
989    /// it and answer `413`.
990    ///
991    /// ```
992    /// use churust_core::{Call, Churust, Form, TestClient};
993    /// use http::StatusCode;
994    /// use serde::Deserialize;
995    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
996    /// #[derive(Deserialize)]
997    /// struct Note { text: String }
998    ///
999    /// let app = Churust::server()
1000    ///     .routing(|r| {
1001    ///         r.post("/tiny", |Form(n): Form<Note>| async move { n.text })
1002    ///             .max_body_bytes(8);
1003    ///     })
1004    ///     .build();
1005    ///
1006    /// let res = TestClient::new(app)
1007    ///     .post("/tiny")
1008    ///     .header("content-type", "application/x-www-form-urlencoded")
1009    ///     .body("text=this-is-far-too-long")
1010    ///     .send()
1011    ///     .await;
1012    /// assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
1013    /// # });
1014    /// ```
1015    ///
1016    /// # Panics
1017    ///
1018    /// Panics if no route has been registered on this builder yet.
1019    pub fn max_body_bytes(&mut self, n: usize) -> &mut Self {
1020        let (method, path) = self
1021            .last
1022            .clone()
1023            .expect("max_body_bytes() must follow a route registration");
1024        self.router.attach_limit(&method, &path, n);
1025        self
1026    }
1027
1028    /// Register a `GET` handler at `path`. Returns `&mut Self` for chaining.
1029    pub fn get<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
1030    where
1031        H: IntoHandler<Marker>,
1032    {
1033        self.method(Method::GET, path, handler)
1034    }
1035    /// Register a `POST` handler at `path`. Returns `&mut Self` for chaining.
1036    pub fn post<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
1037    where
1038        H: IntoHandler<Marker>,
1039    {
1040        self.method(Method::POST, path, handler)
1041    }
1042    /// Register a `PUT` handler at `path`. Returns `&mut Self` for chaining.
1043    pub fn put<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
1044    where
1045        H: IntoHandler<Marker>,
1046    {
1047        self.method(Method::PUT, path, handler)
1048    }
1049    /// Register a `DELETE` handler at `path`. Returns `&mut Self` for chaining.
1050    pub fn delete<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
1051    where
1052        H: IntoHandler<Marker>,
1053    {
1054        self.method(Method::DELETE, path, handler)
1055    }
1056
1057    /// Open a nested scope: every route registered inside `f` has `path`
1058    /// prepended to its pattern. Scopes nest arbitrarily, so prefixes compose.
1059    /// Returns `&mut Self` for chaining.
1060    pub fn route(&mut self, path: &str, f: impl FnOnce(&mut RouteBuilder)) -> &mut Self {
1061        let prefix = self.full(path);
1062        let mut child = RouteBuilder {
1063            router: self.router,
1064            prefix,
1065            chain: self.chain.clone(),
1066            last: None,
1067        };
1068        f(&mut child);
1069        // Forget which route was registered *before* the scope. `guard()` and
1070        // `max_body_bytes()` resolve against `last`, so leaving it set meant a
1071        // `.guard(...)` chained after `route(...)` silently attached to the
1072        // previous sibling — arming the wrong route and leaving the scoped one
1073        // open. With `last` cleared, that chain panics at build instead.
1074        self.last = None;
1075        self
1076    }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081    use super::*;
1082    use crate::call::Call;
1083    use bytes::Bytes;
1084    use http::{HeaderMap, StatusCode, Uri};
1085
1086    fn build() -> Router {
1087        let mut r = Router::new();
1088        {
1089            let mut b = RouteBuilder::new(&mut r);
1090            b.get("/", |_c: Call| async { "root" });
1091            b.route("/users", |b| {
1092                b.get("/{id}", |c: Call| async move {
1093                    format!("user {}", c.param_raw("id").unwrap())
1094                });
1095                b.post("/", |_c: Call| async { (StatusCode::CREATED, "created") });
1096            });
1097            b.get("/files/{path...}", |c: Call| async move {
1098                format!("file {}", c.param_raw("path").unwrap())
1099            });
1100        }
1101        r
1102    }
1103
1104    /// A bare call for tests that exercise routing rather than guards.
1105    fn probe(path: &str) -> Call {
1106        Call::new(
1107            Method::GET,
1108            path.parse::<Uri>().unwrap_or_else(|_| "/".parse().unwrap()),
1109            HeaderMap::new(),
1110            Bytes::new(),
1111        )
1112    }
1113
1114    fn run(r: &Router, m: Method, path: &str) -> Match {
1115        r.route(&m, path, &probe(path))
1116    }
1117
1118    #[tokio::test]
1119    async fn matches_static_and_param() {
1120        let r = build();
1121        match run(&r, Method::GET, "/users/7") {
1122            Match::Found { handler, params } => {
1123                assert_eq!(params.get("id").unwrap(), "7");
1124                let mut c = Call::new(
1125                    Method::GET,
1126                    "/users/7".parse::<Uri>().unwrap(),
1127                    HeaderMap::new(),
1128                    Bytes::new(),
1129                );
1130                c.set_params(params);
1131                let res = handler.handle(c).await;
1132                assert_eq!(res.body, Bytes::from("user 7"));
1133            }
1134            _ => panic!("expected Found"),
1135        }
1136    }
1137
1138    fn build_shadowed() -> Router {
1139        let mut r = Router::new();
1140        {
1141            let mut b = RouteBuilder::new(&mut r);
1142            b.get("/files/{path...}", |c: Call| async move {
1143                format!("wild:{}", c.param_raw("path").unwrap_or(""))
1144            });
1145            b.get("/files/special/x", |_c: Call| async { "static" });
1146            b.post("/files/only-post", |_c: Call| async { "posted" });
1147        }
1148        r
1149    }
1150
1151    #[test]
1152    fn path_params_are_percent_decoded() {
1153        let mut r = Router::new();
1154        {
1155            let mut b = RouteBuilder::new(&mut r);
1156            b.get("/u/{name}", |_c: Call| async { "" });
1157        }
1158        match r.route(&Method::GET, "/u/John%20Doe", &probe("/u/John%20Doe")) {
1159            Match::Found { params, .. } => assert_eq!(params.get("name").unwrap(), "John Doe"),
1160            _ => panic!("expected a match"),
1161        }
1162    }
1163
1164    #[test]
1165    fn encoded_slash_does_not_create_a_segment() {
1166        let mut r = Router::new();
1167        {
1168            let mut b = RouteBuilder::new(&mut r);
1169            b.get("/u/{name}", |_c: Call| async { "" });
1170        }
1171        // Matching at all proves %2F stayed inside one segment. Had it been
1172        // decoded before splitting, this would be two segments and miss.
1173        match r.route(&Method::GET, "/u/a%2Fb", &probe("/u/a%2Fb")) {
1174            Match::Found { params, .. } => assert_eq!(params.get("name").unwrap(), "a/b"),
1175            _ => panic!("%2F must not manufacture a separator"),
1176        }
1177    }
1178
1179    #[test]
1180    fn a_route_with_a_literal_space_is_reachable_encoded() {
1181        let mut r = Router::new();
1182        {
1183            let mut b = RouteBuilder::new(&mut r);
1184            b.get("/a b", |_c: Call| async { "" });
1185        }
1186        assert!(matches!(
1187            r.route(&Method::GET, "/a%20b", &probe("/a%20b")),
1188            Match::Found { .. }
1189        ));
1190    }
1191
1192    #[test]
1193    fn malformed_encoding_is_a_bad_path() {
1194        let mut r = Router::new();
1195        {
1196            let mut b = RouteBuilder::new(&mut r);
1197            b.get("/u/{name}", |_c: Call| async { "" });
1198        }
1199        assert!(matches!(
1200            r.route(&Method::GET, "/u/%zz", &probe("/u/%zz")),
1201            Match::BadPath
1202        ));
1203        assert!(matches!(
1204            r.route(&Method::GET, "/u/%FF", &probe("/u/%FF")),
1205            Match::BadPath
1206        ));
1207    }
1208
1209    #[test]
1210    fn wildcard_captures_decoded_segments() {
1211        let mut r = Router::new();
1212        {
1213            let mut b = RouteBuilder::new(&mut r);
1214            b.get("/f/{rest...}", |_c: Call| async { "" });
1215        }
1216        match r.route(&Method::GET, "/f/a%20b/c", &probe("/f/a%20b/c")) {
1217            Match::Found { params, .. } => assert_eq!(params.get("rest").unwrap(), "a b/c"),
1218            _ => panic!("expected the wildcard to match"),
1219        }
1220    }
1221
1222    #[test]
1223    fn wildcard_is_reachable_through_a_static_sibling() {
1224        let r = build_shadowed();
1225        match run(&r, Method::GET, "/files/special") {
1226            Match::Found { params, .. } => {
1227                assert_eq!(params.get("path").unwrap(), "special");
1228            }
1229            _ => panic!("wildcard should serve /files/special"),
1230        }
1231    }
1232
1233    #[test]
1234    fn exact_match_still_wins_over_wildcard() {
1235        let r = build_shadowed();
1236        match run(&r, Method::GET, "/files/special/x") {
1237            Match::Found { params, .. } => {
1238                assert!(
1239                    !params.contains_key("path"),
1240                    "the static route captured a wildcard param"
1241                );
1242            }
1243            _ => panic!("expected the static route"),
1244        }
1245    }
1246
1247    #[test]
1248    fn allow_header_unions_exact_and_wildcard_methods() {
1249        let r = build_shadowed();
1250        match run(&r, Method::DELETE, "/files/only-post") {
1251            Match::MethodNotAllowed { allow } => {
1252                assert!(allow.contains(&Method::POST), "missing the exact method");
1253                assert!(allow.contains(&Method::GET), "missing the wildcard method");
1254            }
1255            _ => panic!("expected 405"),
1256        }
1257    }
1258
1259    #[test]
1260    fn abandoned_branch_params_do_not_leak_into_the_wildcard() {
1261        let mut r = Router::new();
1262        {
1263            let mut b = RouteBuilder::new(&mut r);
1264            // Matches /u/7/edit structurally, but has no GET handler.
1265            b.post("/u/{id}/edit", |_c: Call| async { "edit" });
1266            b.get("/u/{rest...}", |_c: Call| async { "wild" });
1267        }
1268        match r.route(&Method::GET, "/u/7/edit", &probe("/u/7/edit")) {
1269            Match::Found { params, .. } => {
1270                assert_eq!(params.get("rest").unwrap(), "7/edit");
1271                assert!(
1272                    !params.contains_key("id"),
1273                    "stale `id` leaked from the abandoned walk"
1274                );
1275            }
1276            _ => panic!("the wildcard should have matched"),
1277        }
1278    }
1279
1280    #[test]
1281    fn an_encoded_separator_in_a_wildcard_tail_does_not_override_a_matching_sibling() {
1282        // The wildcard refuses this tail, but a sibling `{n}` matches the path
1283        // cleanly with `n = "a/b"` — that is what splitting before decoding is
1284        // for. The wildcard's opinion about its own tail must not be promoted
1285        // into a verdict on the whole request: only POST is registered at the
1286        // route that does match, so `405` is the honest answer and a `400`
1287        // would describe a request that is in fact perfectly well formed.
1288        let mut r = Router::new();
1289        {
1290            let mut b = RouteBuilder::new(&mut r);
1291            b.get("/files/{p...}", |_c: Call| async { "wild" });
1292            b.post("/files/{n}", |_c: Call| async { "one" });
1293        }
1294        match run(&r, Method::GET, "/files/a%2Fb") {
1295            Match::MethodNotAllowed { allow } => assert_eq!(allow, vec![Method::POST]),
1296            _ => panic!("expected the sibling param route to answer"),
1297        }
1298    }
1299
1300    #[test]
1301    fn unknown_path_is_not_found() {
1302        let r = build();
1303        assert!(matches!(run(&r, Method::GET, "/nope"), Match::NotFound));
1304    }
1305
1306    #[test]
1307    fn known_path_wrong_method_is_405() {
1308        let r = build();
1309        match run(&r, Method::DELETE, "/users/7") {
1310            Match::MethodNotAllowed { allow } => assert!(allow.contains(&Method::GET)),
1311            _ => panic!("expected 405"),
1312        }
1313    }
1314
1315    #[test]
1316    fn wildcard_captures_rest() {
1317        let r = build();
1318        match run(&r, Method::GET, "/files/a/b/c.txt") {
1319            Match::Found { params, .. } => assert_eq!(params.get("path").unwrap(), "a/b/c.txt"),
1320            _ => panic!("expected wildcard Found"),
1321        }
1322    }
1323
1324    // Regression guard for blocking issue #2: a pre-boxed `BoxHandler` must be
1325    // acceptable to the route builder methods, not only closures.
1326    #[tokio::test]
1327    async fn route_builder_accepts_boxed_handler() {
1328        let pre: BoxHandler = boxed((|_c: Call| async { "pre-boxed" }).into_handler());
1329        let mut r = Router::new();
1330        {
1331            let mut b = RouteBuilder::new(&mut r);
1332            b.get("/pre", pre);
1333        }
1334        match run(&r, Method::GET, "/pre") {
1335            Match::Found { handler, .. } => {
1336                let c = Call::new(
1337                    Method::GET,
1338                    "/pre".parse::<Uri>().unwrap(),
1339                    HeaderMap::new(),
1340                    Bytes::new(),
1341                );
1342                let res = handler.handle(c).await;
1343                assert_eq!(res.body, Bytes::from("pre-boxed"));
1344            }
1345            _ => panic!("expected Found"),
1346        }
1347    }
1348}