Skip to main content

actus_controller/
lib.rs

1//! Public API types and utilities for the Actus controller system — the
2//! `Controller` trait, the typed [`Params`] / [`ExtractedParams`], the route
3//! metadata (`Verb`, `ParamDef`, `RouteDef`), and the route-resolution
4//! [`routing`] helpers. This is what user code and the `#[controller]` /
5//! `routes!` / `app_routes!` macros' generated code interact with.
6#![warn(missing_docs)]
7
8pub use async_trait::async_trait;
9use bytes::Bytes;
10use serde_json::Value as JsonValue;
11use std::any::{Any, TypeId};
12use std::collections::HashMap;
13
14// Re-export the controller macro and app_routes! macro from the macros crate.
15pub use actus_controller_macros::{app_routes, controller};
16pub use actus_reply::prelude::*;
17
18// =========================
19// HTTP Verbs
20// =========================
21
22/// An HTTP method. Used in `routes!` verb prefixes and for the `Allow` header
23/// the framework stamps on `405` responses.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Verb {
26    /// The HTTP `GET` method.
27    GET,
28    /// The HTTP `POST` method.
29    POST,
30    /// The HTTP `PUT` method.
31    PUT,
32    /// The HTTP `DELETE` method.
33    DELETE,
34    /// The HTTP `PATCH` method.
35    PATCH,
36    /// The HTTP `HEAD` method.
37    HEAD,
38    /// The HTTP `OPTIONS` method.
39    OPTIONS,
40}
41
42impl Verb {
43    /// The canonical uppercase method token (`"GET"`, `"POST"`, …). Used for
44    /// the `Allow` header on `405` responses, among other things.
45    pub fn as_str(&self) -> &'static str {
46        match self {
47            Verb::GET => "GET",
48            Verb::POST => "POST",
49            Verb::PUT => "PUT",
50            Verb::DELETE => "DELETE",
51            Verb::PATCH => "PATCH",
52            Verb::HEAD => "HEAD",
53            Verb::OPTIONS => "OPTIONS",
54        }
55    }
56}
57
58impl core::fmt::Display for Verb {
59    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60        f.write_str(self.as_str())
61    }
62}
63
64/// Verbs accepted by a route declared without a verb prefix. Reflects the
65/// "verbs are constraints, not identities" stance: an unmarked route imposes
66/// no semantic restriction beyond what HTML forms emit natively.
67/// Restrictive verbs (PUT/DELETE/PATCH) and protocol verbs (HEAD/OPTIONS)
68/// must be opted into explicitly.
69pub const DEFAULT_VERBS: &[Verb] = &[Verb::GET, Verb::POST];
70
71// =========================
72// Controller mode
73// =========================
74
75/// How a controller treats request parameters it didn't declare.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ControllerMode {
78    /// Reject a request that carries parameters the route didn't declare.
79    Strict,
80    /// Allow undeclared extra parameters to pass through.
81    Lax,
82}
83
84// =========================
85// Parameter definitions
86// =========================
87
88/// The declared type of a route parameter — governs how its raw string value
89/// is parsed before reaching the handler.
90#[derive(Debug, Clone, Copy)]
91pub enum ParamType {
92    /// A UTF-8 string.
93    String,
94    /// A signed 64-bit integer (`i64`).
95    Int,
96    /// An unsigned 64-bit integer (`u64`).
97    U64,
98    /// An unsigned 32-bit integer (`u32`).
99    U32,
100    /// A 64-bit float (`f64`).
101    F64,
102    /// A boolean.
103    Bool,
104    /// A repeated parameter collected into `Vec<String>`.
105    StringArray,
106    /// A JSON value (`serde_json::Value`), parsed from the request body.
107    Json,
108    /// The raw request body as `bytes::Bytes`.
109    Bytes,
110}
111
112/// A parameter's default value, applied when the request omits it. Declared
113/// in `routes!` as `name: Type = default`.
114#[derive(Debug, Clone)]
115pub enum ParamDefault {
116    /// `&'static str` (not `String`) so route metadata can live in a
117    /// `static ROUTES: &[RouteDef]` initializer — `String::from(...)` and
118    /// `.to_string()` aren't const on stable Rust. Default-application at
119    /// runtime borrows the str, allocating only if/when needed.
120    String(&'static str),
121    /// A default `i64`.
122    Int(i64),
123    /// A default `u64`.
124    U64(u64),
125    /// A default `u32`.
126    U32(u32),
127    /// A default `f64`.
128    F64(f64),
129    /// A default `bool`.
130    Bool(bool),
131}
132
133/// Where a parameter's value is read from.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum ParamSource {
136    /// From a `{name}` segment of the URL path.
137    Path,
138    /// From the query string.
139    Query,
140    /// From the request body.
141    Body,
142}
143
144/// The compile-time description of one route parameter, recorded by the
145/// `routes!` macro and used to extract and parse it at request time.
146#[derive(Debug, Clone)]
147pub struct ParamDef {
148    /// The parameter name.
149    pub name: &'static str,
150    /// The declared type the raw value is parsed into.
151    pub ty: ParamType,
152    /// Where the value is read from (path, query, or body).
153    pub source: ParamSource,
154    /// The default applied when the request omits the parameter, if any.
155    pub default: Option<ParamDefault>,
156}
157
158// =========================
159// Route definition
160// =========================
161
162/// The compile-time description of one route in a controller, recorded by the
163/// `routes!` macro. The framework matches and dispatches against these, and
164/// tools can introspect them (e.g. the OpenAPI generator).
165#[derive(Debug, Clone)]
166pub struct RouteDef {
167    /// The route pattern relative to the controller's mount (e.g.
168    /// `"posts/{id}"`).
169    pub pattern: &'static str,
170    /// Internal dispatch token (`"handler_0"`, `"handler_1"`, …) produced by
171    /// the `#[controller]` macro. Opaque to user code; for the
172    /// human-readable handler method name (useful for OpenAPI operationIds
173    /// and the like), see [`RouteDef::handler`].
174    pub handler_id: &'static str,
175    /// Handler method name as written in the controller's `impl` block
176    /// (`"list"`, `"get"`, `"create"`, …). Captured at macro-expansion time
177    /// so introspection tools (OpenAPI doc generators, route audit scripts)
178    /// can identify the handler without grepping.
179    pub handler: &'static str,
180    /// Verbs this route accepts. Always non-empty: a single-element slice for
181    /// an explicitly declared verb, [`DEFAULT_VERBS`] for an unmarked route.
182    pub verb: &'static [Verb],
183    /// The route's declared parameters, in order.
184    pub params: &'static [ParamDef],
185    /// The handler method's `///` doc comment, if any — surfaced to tools like
186    /// the OpenAPI generator.
187    pub doc: Option<&'static str>,
188}
189
190// =========================
191// Runtime parameter handling
192// =========================
193
194/// Raw parameters from the HTTP request, plus headers, the parsed body, and
195/// a typed extensions slot for per-request data that prepare hooks (or
196/// middleware) want to attach for handlers to read.
197///
198/// Headers are a **multimap**, just like query: each lowercased name maps to
199/// every value seen for it, in request order. Scalar accessor
200/// [`Params::header`] reads the first value (the common case);
201/// [`Params::header_all`] returns every value (for `Forwarded`, `Via`, etc.
202/// which can legitimately appear multiple times — e.g. in a proxy chain).
203///
204/// Query parameters are a **multimap**: each name maps to *every* value seen
205/// for it, in request order — `?tags=a&tags=b` is `{"tags": ["a", "b"]}`.
206/// Scalar accessors (`require`, `get_u64`, …) read the first value;
207/// [`Params::get_all`] returns the whole list (this is what backs
208/// `Vec<String>` handler parameters, so a one-element array works the same
209/// as a many-element one). Repeated *keys* are what create multiple values;
210/// a comma in a single value (`?tags=a,b`) is just one value `"a,b"`.
211///
212/// `body` and `raw_body` are populated by the server's `Request::to_params`
213/// using `Content-Type` discrimination:
214///
215/// * `application/json` → `body = Some(parsed)`, `raw_body = original_bytes`.
216/// * `application/x-www-form-urlencoded` → fields are appended into `query`
217///   (same multimap); `body = None`, `raw_body = original_bytes`.
218/// * any other `Content-Type` (including `application/octet-stream`,
219///   `application/zip`, …) → `body = None`, `raw_body = original_bytes`.
220/// * empty body → `body = None`, `raw_body = Bytes::new()`.
221///
222/// A non-empty request body without a `Content-Type` header is rejected
223/// at ingest with `WebError::BadRequest` — `body` and `raw_body` are
224/// therefore never both empty by accident.
225pub struct Params {
226    verb: Verb,
227    query: HashMap<String, Vec<String>>,
228    body: Option<JsonValue>,
229    raw_body: Bytes,
230    headers: HashMap<String, Vec<String>>,
231    extensions: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
232}
233
234impl Params {
235    /// Construct a `Params` from the extracted request pieces. Called by the
236    /// framework's dispatch path; handlers receive an already-built `Params`.
237    pub fn new(
238        verb: Verb,
239        query: HashMap<String, Vec<String>>,
240        body: Option<JsonValue>,
241        raw_body: Bytes,
242        headers: HashMap<String, Vec<String>>,
243    ) -> Self {
244        Self {
245            verb,
246            query,
247            body,
248            raw_body,
249            headers,
250            extensions: HashMap::new(),
251        }
252    }
253
254    /// First value of query parameter `name`, if present. The basis for all
255    /// the scalar accessors below.
256    fn first(&self, name: &str) -> Option<&str> {
257        self.query
258            .get(name)
259            .and_then(|values| values.first())
260            .map(String::as_str)
261    }
262
263    /// The entire query multimap — every parameter name and all its values,
264    /// in request order, plus any folded `application/x-www-form-urlencoded`
265    /// body fields.
266    ///
267    /// Use this for "catch the rest" handlers — a search endpoint with
268    /// open-ended filters, a request proxy, etc.: declare `params: &Params`,
269    /// mark the controller `#[controller(lax)]` so strict mode doesn't reject
270    /// the undeclared keys, and read `params.query()`. Handlers that know
271    /// their parameters up front should declare them as typed arguments
272    /// instead; this is the escape hatch, not the default.
273    pub fn query(&self) -> &HashMap<String, Vec<String>> {
274        &self.query
275    }
276
277    /// Raw bytes of the request body, before any content-type-specific
278    /// parsing. Always present (`Bytes::new()` for empty bodies).
279    ///
280    /// Use this when the handler consumes binary uploads (`.uwx`, image
281    /// blobs, etc.). For JSON bodies, prefer [`Params::json_body`] or
282    /// macro-extracted typed args — those operate on the parsed value
283    /// the framework already produced from the same bytes.
284    pub fn body_bytes(&self) -> &Bytes {
285        &self.raw_body
286    }
287
288    /// Stash a value for later retrieval. The value is keyed by its type;
289    /// inserting a second value of the same type replaces the first.
290    /// Typically called from a `prepare` hook to pass a resolved user (or
291    /// other request-scoped state) through to the handler.
292    pub fn insert<T: Any + Send + Sync>(&mut self, value: T) -> Option<T> {
293        self.extensions
294            .insert(TypeId::of::<T>(), Box::new(value))
295            .and_then(|prev| prev.downcast::<T>().ok().map(|b| *b))
296    }
297
298    /// Look up a value previously inserted with [`Params::insert`].
299    pub fn get<T: Any + Send + Sync>(&self) -> Option<&T> {
300        self.extensions
301            .get(&TypeId::of::<T>())
302            .and_then(|b| b.downcast_ref::<T>())
303    }
304
305    /// The HTTP verb this request was dispatched with.
306    pub fn verb(&self) -> Verb {
307        self.verb
308    }
309
310    /// Look up a request header (case-insensitive). Returns the *first*
311    /// value if the header appears more than once; see [`Params::header_all`]
312    /// for every value.
313    pub fn header(&self, name: &str) -> Option<&str> {
314        self.headers
315            .get(&name.to_ascii_lowercase())
316            .and_then(|values| values.first())
317            .map(String::as_str)
318    }
319
320    /// Every value for a request header (case-insensitive), in receipt
321    /// order. Empty slice if the header wasn't present. Use this for headers
322    /// that can legitimately appear multiple times — `Forwarded`, `Via`,
323    /// `X-Forwarded-For` (when proxies emit one entry per hop), etc.
324    pub fn header_all(&self, name: &str) -> &[String] {
325        self.headers
326            .get(&name.to_ascii_lowercase())
327            .map(Vec::as_slice)
328            .unwrap_or(&[])
329    }
330
331    /// Convenience: extract a Bearer token from the `Authorization` header.
332    /// Returns `None` if the header is missing or doesn't start with `Bearer `.
333    pub fn bearer_token(&self) -> Option<&str> {
334        let auth = self.header("authorization")?;
335        auth.strip_prefix("Bearer ")
336            .or_else(|| auth.strip_prefix("bearer "))
337    }
338
339    // Core methods for parameter extraction
340
341    /// The first value of query parameter `name`, or a `400 Bad Request` if
342    /// it's absent.
343    pub fn require(&self, name: &str) -> Result<&str, WebError> {
344        self.first(name)
345            .ok_or_else(|| WebError::BadRequest(format!("Missing required parameter: {}", name)))
346    }
347
348    /// Look up a query parameter's first value; returns `None` if absent.
349    pub fn get_optional(&self, name: &str) -> Option<&str> {
350        self.first(name)
351    }
352
353    /// Parse query parameter `name` as an `i64`; `400` if missing or unparsable.
354    pub fn get_int(&self, name: &str) -> Result<i64, WebError> {
355        self.require(name)?
356            .parse()
357            .map_err(|_| WebError::BadRequest(format!("Invalid integer: {}", name)))
358    }
359
360    /// Parse optional query parameter `name` as an `i64`; `Ok(None)` if absent,
361    /// `400` if present but unparsable.
362    pub fn get_int_optional(&self, name: &str) -> Result<Option<i64>, WebError> {
363        match self.get_optional(name) {
364            Some(s) => s
365                .parse()
366                .map(Some)
367                .map_err(|_| WebError::BadRequest(format!("Invalid integer: {}", name))),
368            None => Ok(None),
369        }
370    }
371
372    // Extended type methods
373
374    /// Parse query parameter `name` as a `u64`; `400` if missing or unparsable.
375    pub fn get_u64(&self, name: &str) -> Result<u64, WebError> {
376        self.require(name)?
377            .parse()
378            .map_err(|_| WebError::BadRequest(format!("Invalid u64: {}", name)))
379    }
380
381    /// Parse optional query parameter `name` as a `u64`; `Ok(None)` if absent,
382    /// `400` if present but unparsable.
383    pub fn get_u64_optional(&self, name: &str) -> Result<Option<u64>, WebError> {
384        match self.get_optional(name) {
385            Some(s) => s
386                .parse()
387                .map(Some)
388                .map_err(|_| WebError::BadRequest(format!("Invalid u64: {}", name))),
389            None => Ok(None),
390        }
391    }
392
393    /// Parse query parameter `name` as a `u32`; `400` if missing or unparsable.
394    pub fn get_u32(&self, name: &str) -> Result<u32, WebError> {
395        self.require(name)?
396            .parse()
397            .map_err(|_| WebError::BadRequest(format!("Invalid u32: {}", name)))
398    }
399
400    /// Parse optional query parameter `name` as a `u32`; `Ok(None)` if absent,
401    /// `400` if present but unparsable.
402    pub fn get_u32_optional(&self, name: &str) -> Result<Option<u32>, WebError> {
403        match self.get_optional(name) {
404            Some(s) => s
405                .parse()
406                .map(Some)
407                .map_err(|_| WebError::BadRequest(format!("Invalid u32: {}", name))),
408            None => Ok(None),
409        }
410    }
411
412    /// Parse query parameter `name` as an `f64`; `400` if missing or unparsable.
413    pub fn get_f64(&self, name: &str) -> Result<f64, WebError> {
414        self.require(name)?
415            .parse()
416            .map_err(|_| WebError::BadRequest(format!("Invalid float: {}", name)))
417    }
418
419    /// Parse optional query parameter `name` as an `f64`; `Ok(None)` if absent,
420    /// `400` if present but unparsable.
421    pub fn get_f64_optional(&self, name: &str) -> Result<Option<f64>, WebError> {
422        match self.get_optional(name) {
423            Some(s) => s
424                .parse()
425                .map(Some)
426                .map_err(|_| WebError::BadRequest(format!("Invalid float: {}", name))),
427            None => Ok(None),
428        }
429    }
430
431    /// Read query parameter `name` as a bool — `false` when absent, empty,
432    /// `"false"`, or `"0"`; `true` otherwise.
433    pub fn get_bool(&self, name: &str) -> bool {
434        self.first(name)
435            .map(|s| !s.is_empty() && s != "false" && s != "0")
436            .unwrap_or(false)
437    }
438
439    /// Like [`Params::get_bool`], but `None` when the parameter is absent
440    /// (rather than `false`).
441    pub fn get_bool_optional(&self, name: &str) -> Option<bool> {
442        self.first(name)
443            .map(|s| !s.is_empty() && s != "false" && s != "0")
444    }
445
446    /// All values of query parameter `name`, in request order (empty if the
447    /// name wasn't present). Backs `Vec<String>` handler parameters.
448    pub fn get_all(&self, name: &str) -> Result<Vec<String>, WebError> {
449        Ok(self.query.get(name).cloned().unwrap_or_default())
450    }
451
452    /// All values of query parameter `name`; `None` if the name wasn't present
453    /// at all (vs. `Some(vec![])`, which urlencoding can't actually produce).
454    pub fn get_all_optional(&self, name: &str) -> Option<Vec<String>> {
455        self.query.get(name).cloned()
456    }
457
458    /// The parsed JSON request body, or `400 Bad Request` if there wasn't one.
459    pub fn json_body(&self) -> Result<JsonValue, WebError> {
460        self.body
461            .clone()
462            .ok_or_else(|| WebError::BadRequest("Missing JSON body".to_string()))
463    }
464
465    /// In strict mode, return any query keys *not* in `expected` (so the caller
466    /// can reject the request); `None` if every key was expected.
467    pub fn check_unexpected(&self, expected: &[&str]) -> Option<Vec<String>> {
468        let unexpected: Vec<String> = self
469            .query
470            .keys()
471            .filter(|k| !expected.contains(&k.as_str()))
472            .cloned()
473            .collect();
474
475        if unexpected.is_empty() {
476            None
477        } else {
478            Some(unexpected)
479        }
480    }
481}
482
483// =========================
484// Extracted parameters (after route resolution)
485// =========================
486
487/// Parameters extracted after route resolution — path captures plus the
488/// declared query and body values. The `#[controller]` macro reads typed
489/// handler arguments out of this; application code rarely touches it directly.
490#[derive(Debug)]
491pub struct ExtractedParams {
492    /// Path captures (single-segment `{name}` or the joined `{...rest}`).
493    path: HashMap<String, String>,
494    /// Declared query parameters that were present, with *all* their values
495    /// (see [`Params`] — query is a multimap).
496    query: HashMap<String, Vec<String>>,
497    body: Option<JsonValue>,
498    raw_body: Bytes,
499}
500
501impl ExtractedParams {
502    /// The single scalar value for `name`: a path capture takes precedence,
503    /// otherwise the first query value. `None` if neither has it.
504    fn scalar(&self, name: &str) -> Option<&str> {
505        self.path.get(name).map(String::as_str).or_else(|| {
506            self.query
507                .get(name)
508                .and_then(|values| values.first())
509                .map(String::as_str)
510        })
511    }
512
513    fn require_scalar(&self, name: &str) -> Result<&str, WebError> {
514        self.scalar(name)
515            .ok_or_else(|| WebError::BadRequest(format!("Missing parameter: {}", name)))
516    }
517
518    /// The value of path/query parameter `name` as a `String`; `400` if absent.
519    pub fn get_string(&self, name: &str) -> Result<String, WebError> {
520        self.require_scalar(name).map(str::to_string)
521    }
522
523    /// Parse path/query parameter `name` as an `i64`; `400` if missing or
524    /// unparsable.
525    pub fn get_i64(&self, name: &str) -> Result<i64, WebError> {
526        self.require_scalar(name)?
527            .parse()
528            .map_err(|_| WebError::BadRequest(format!("Invalid integer: {}", name)))
529    }
530
531    /// Parse path/query parameter `name` as a `u64`; `400` if missing or
532    /// unparsable.
533    pub fn get_u64(&self, name: &str) -> Result<u64, WebError> {
534        self.require_scalar(name)?
535            .parse()
536            .map_err(|_| WebError::BadRequest(format!("Invalid u64: {}", name)))
537    }
538
539    /// Parse path/query parameter `name` as a `u32`; `400` if missing or
540    /// unparsable.
541    pub fn get_u32(&self, name: &str) -> Result<u32, WebError> {
542        self.require_scalar(name)?
543            .parse()
544            .map_err(|_| WebError::BadRequest(format!("Invalid u32: {}", name)))
545    }
546
547    /// Parse path/query parameter `name` as an `f64`; `400` if missing or
548    /// unparsable.
549    pub fn get_f64(&self, name: &str) -> Result<f64, WebError> {
550        self.require_scalar(name)?
551            .parse()
552            .map_err(|_| WebError::BadRequest(format!("Invalid float: {}", name)))
553    }
554
555    /// Read path/query parameter `name` as a bool — `false` when absent,
556    /// empty, `"false"`, or `"0"`; `true` otherwise.
557    pub fn get_bool(&self, name: &str) -> Result<bool, WebError> {
558        Ok(self
559            .scalar(name)
560            .map(|s| !s.is_empty() && s != "false" && s != "0")
561            .unwrap_or(false))
562    }
563
564    /// All values for `name` (in request order; empty if the name wasn't
565    /// present). Backs `Vec<String>` handler parameters — a one-element list
566    /// (`?tags=a`) and a many-element one (`?tags=a&tags=b`) flow through the
567    /// same path.
568    pub fn get_string_array(&self, name: &str) -> Result<Vec<String>, WebError> {
569        Ok(self.query.get(name).cloned().unwrap_or_default())
570    }
571
572    /// The parsed JSON request body, or `400 Bad Request` if there wasn't one.
573    pub fn get_json_body(&self) -> Result<JsonValue, WebError> {
574        self.body
575            .clone()
576            .ok_or_else(|| WebError::BadRequest("Missing JSON body".to_string()))
577    }
578
579    /// Raw bytes of the request body. See [`Params::body_bytes`] for the
580    /// content-type-aware semantics. Used by the macro to extract a
581    /// `Bytes` handler argument.
582    pub fn get_body_bytes(&self) -> Bytes {
583        self.raw_body.clone()
584    }
585}
586
587// =========================
588// Routing utilities module
589// =========================
590
591/// Route resolution helpers — matching a request path + verb against a
592/// controller's `&[RouteDef]` and extracting the path/query/body parameters.
593/// Used by the `#[controller]` macro's generated dispatch; exposed for tools
594/// and tests that need to resolve routes directly.
595pub mod routing {
596    use super::*;
597    use std::collections::HashMap;
598
599    /// The segments of a mount path or family prefix, normalised exactly as
600    /// `RouterBuilder::add_route` and the `families` block of `app_routes!`
601    /// normalise them: surrounding slashes trimmed, empty segments dropped, a
602    /// trailing `*` (the catch-all sugar) removed — so `"api/*"`, `"/api/"`
603    /// and `"api"` are the same prefix, and `""` / `"*"` is the root.
604    pub fn family_segments(path: &str) -> Vec<&str> {
605        let mut segs: Vec<&str> = path
606            .trim_matches('/')
607            .split('/')
608            .filter(|s| !s.is_empty())
609            .collect();
610        if segs.last() == Some(&"*") {
611            segs.pop();
612        }
613        segs
614    }
615
616    /// Which of `prefixes` covers `mount` — by the **same rule the `families`
617    /// block of `app_routes!` applies at compile time**, so a boot-time check
618    /// written against this cannot disagree with the compile-time one:
619    /// segment-aligned (`"api"` covers `"api/things"`, never `"apiary"`), the
620    /// **longest** covering prefix wins (so `"api/auth"` carves its subtree out
621    /// of `"api"`), a trailing `*` reads as the prefix before it, and the root
622    /// (`""` or `"*"`) covers every mount. Returns the winning prefix as it was
623    /// given, so the caller can look it up in its own table. On two prefixes
624    /// that normalise identically, the later one wins, as in the macro.
625    ///
626    /// Do not re-derive this with `mount.split('/').next()`: that matches by
627    /// first segment, agrees with the macro only while every family is a single
628    /// segment, and silently diverges the moment one nests.
629    pub fn covering_family<'a, I>(mount: &str, prefixes: I) -> Option<&'a str>
630    where
631        I: IntoIterator<Item = &'a str>,
632    {
633        let m = family_segments(mount);
634        let mut best: Option<(&'a str, usize)> = None;
635        for p in prefixes {
636            let ps = family_segments(p);
637            let covers = ps.len() <= m.len() && ps.iter().zip(m.iter()).all(|(a, b)| a == b);
638            if covers && best.is_none_or(|(_, n)| ps.len() >= n) {
639                best = Some((p, ps.len()));
640            }
641        }
642        best.map(|(p, _)| p)
643    }
644
645    /// Main route resolution function. Tries each route in declaration order
646    /// and returns the first whose path pattern *and* verb both match, along
647    /// with its extracted parameters.
648    ///
649    /// "Declaration order" matters: when two patterns can match the same
650    /// action (e.g. `"special"` and `"{id}"`), the one declared first wins —
651    /// list the more specific route earlier.
652    ///
653    /// When no route fully matches, the error distinguishes:
654    /// - `WebError::MethodNotAllowed(methods)`: at least one route's *path*
655    ///   pattern matched but its verb didn't; `methods` is the (sorted, deduped)
656    ///   set of verbs those routes accept, which the caller surfaces as the
657    ///   `Allow` header.
658    /// - `WebError::NotFound`: no route's path pattern matched at all.
659    #[inline]
660    pub fn resolve<'a>(
661        routes: &'a [RouteDef],
662        action: &str,
663        params: &Params,
664        mode: ControllerMode,
665    ) -> Result<(&'a RouteDef, ExtractedParams), WebError> {
666        // Verbs accepted by routes whose *path* matched but whose verb didn't.
667        let mut allowed_methods: Vec<&'static str> = Vec::new();
668
669        for route in routes {
670            // Pattern match first.
671            let path_params = match match_pattern(route.pattern, action) {
672                Some(p) => p,
673                None => continue,
674            };
675
676            // Then verb check. `route.verb` is the (non-empty) set of verbs
677            // this route accepts; an unmarked route carries `DEFAULT_VERBS`.
678            if !route.verb.contains(&params.verb()) {
679                for v in route.verb {
680                    let token = v.as_str();
681                    if !allowed_methods.contains(&token) {
682                        allowed_methods.push(token);
683                    }
684                }
685                continue;
686            }
687
688            // Full match: build extracted params and return.
689            {
690                // Build extracted params
691                let mut extracted = ExtractedParams {
692                    path: path_params,
693                    query: HashMap::new(),
694                    body: params.body.clone(),
695                    raw_body: params.raw_body.clone(),
696                };
697
698                // Extract and validate parameters
699                for param_def in route.params {
700                    match param_def.source {
701                        ParamSource::Path => {
702                            // Path-source params come from `{name}` / `{...name}`
703                            // tokens, which `match_pattern` always captures when
704                            // the pattern matched — so this is already in
705                            // `extracted.path`. (The `#[controller]` macro is what
706                            // guarantees the `ParamDef` ↔ pattern correspondence;
707                            // a hand-built `RouteDef` that violates it would trip
708                            // this in debug builds.)
709                            debug_assert!(
710                                extracted.path.contains_key(param_def.name),
711                                "path parameter `{}` not captured by pattern",
712                                param_def.name
713                            );
714                        }
715                        ParamSource::Query => {
716                            // Extract from query params. A `Vec<String>`
717                            // parameter is inherently optional — absent means
718                            // the empty list, never a 400. Other scalar types
719                            // are required unless they declared a default.
720                            if let Some(value) = params.query.get(param_def.name) {
721                                extracted
722                                    .query
723                                    .insert(param_def.name.to_string(), value.clone());
724                            } else if param_def.default.is_none()
725                                && !matches!(param_def.ty, ParamType::StringArray)
726                            {
727                                return Err(WebError::BadRequest(format!(
728                                    "Missing required parameter: {}",
729                                    param_def.name
730                                )));
731                            }
732                            // Defaults are applied in the handler extraction phase.
733                        }
734                        ParamSource::Body => {
735                            // JSON body is already handled
736                        }
737                    }
738                }
739
740                // Check for unexpected parameters in strict mode
741                if mode == ControllerMode::Strict {
742                    let expected: Vec<&str> = route
743                        .params
744                        .iter()
745                        .filter(|p| p.source == ParamSource::Query)
746                        .map(|p| p.name)
747                        .collect();
748
749                    if let Some(unexpected) = params.check_unexpected(&expected) {
750                        return Err(WebError::BadRequest(format!(
751                            "Unexpected parameters: {}",
752                            unexpected.join(", ")
753                        )));
754                    }
755                }
756
757                return Ok((route, extracted));
758            }
759        }
760
761        if allowed_methods.is_empty() {
762            Err(WebError::NotFound)
763        } else {
764            allowed_methods.sort_unstable();
765            allowed_methods.dedup();
766            Err(WebError::MethodNotAllowed(allowed_methods))
767        }
768    }
769
770    /// If `segment` is a `{...name}` rest token, returns `name` (non-empty).
771    fn rest_token_name(segment: &str) -> Option<&str> {
772        segment
773            .strip_prefix("{...")
774            .and_then(|s| s.strip_suffix('}'))
775            .filter(|name| !name.is_empty())
776    }
777
778    /// Match one fixed (non-rest) pattern segment against a path segment,
779    /// recording a capture for `{name}` tokens. Returns `false` if a literal
780    /// segment doesn't match.
781    fn match_fixed_segment(
782        pattern_part: &str,
783        path_part: &str,
784        params: &mut HashMap<String, String>,
785    ) -> bool {
786        if let Some(param_name) = pattern_part
787            .strip_prefix('{')
788            .and_then(|s| s.strip_suffix('}'))
789        {
790            params.insert(param_name.to_string(), path_part.to_string());
791            true
792        } else {
793            pattern_part == path_part
794        }
795    }
796
797    /// Split a pattern or action into its segments. A pattern/action is a
798    /// `/`-joined list of **non-empty** segments; empty segments — from
799    /// leading, trailing, or doubled slashes, and notably from the empty
800    /// action `""` (which `str::split` would otherwise yield as `[""]`) —
801    /// are not segments. This is the same normalization `Request` applies to
802    /// the full request path, applied here to the per-controller action and
803    /// the route patterns it's matched against.
804    fn segments(s: &str) -> Vec<&str> {
805        s.split('/').filter(|seg| !seg.is_empty()).collect()
806    }
807
808    /// Match a route pattern against an action path.
809    /// Returns extracted path parameters if matched.
810    ///
811    /// Both sides are viewed as lists of non-empty path segments.
812    /// A literal segment or `{name}` token is **required** — it has no match
813    /// when there's no segment to fill it, so `match_pattern("{id}", "")` is
814    /// `None`. (A controller that wants to serve its collection root declares
815    /// `"" => …`.)
816    ///
817    /// A trailing `{...name}` token is a *rest* parameter: it captures the
818    /// remainder of the path (slashes included) and matches **zero or more**
819    /// segments — `match_pattern("{...path}", "")` is `Some({path: ""})`, and
820    /// `"{folder_id}/{...path}"` matches `"abc"` (`path == ""`) and
821    /// `"abc/x/y"` (`path == "x/y"`) but **not** `""` (the required
822    /// `folder_id` has no segment). The `#[controller]` macro enforces that
823    /// `{...name}` appears at most once and only as the final token; this
824    /// function trusts that and only inspects the last token.
825    #[inline]
826    pub fn match_pattern(pattern: &str, path: &str) -> Option<HashMap<String, String>> {
827        let pattern_parts = segments(pattern);
828        let path_parts = segments(path);
829        let mut params = HashMap::new();
830
831        if let Some(rest_name) = pattern_parts.last().and_then(|s| rest_token_name(s)) {
832            // Everything before the rest token is a fixed prefix that must
833            // match segment-for-segment; the rest token soaks up whatever
834            // is left (possibly nothing).
835            let fixed = &pattern_parts[..pattern_parts.len() - 1];
836            if path_parts.len() < fixed.len() {
837                return None;
838            }
839            for (pattern_part, path_part) in fixed.iter().zip(path_parts.iter()) {
840                if !match_fixed_segment(pattern_part, path_part, &mut params) {
841                    return None;
842                }
843            }
844            params.insert(rest_name.to_string(), path_parts[fixed.len()..].join("/"));
845            return Some(params);
846        }
847
848        if pattern_parts.len() != path_parts.len() {
849            return None;
850        }
851        for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
852            if !match_fixed_segment(pattern_part, path_part, &mut params) {
853                return None;
854            }
855        }
856        Some(params)
857    }
858}
859
860// =========================
861// Controller trait
862// =========================
863
864/// The runtime interface every controller implements. Hand-writing this is
865/// possible but unusual — the `#[controller]` macro generates the
866/// implementation (dispatch table, parameter extraction, the metadata methods)
867/// from a controller's `impl` block and its `routes!` declaration.
868#[async_trait]
869pub trait Controller: Send + Sync {
870    /// Route `action` (the path below this controller's mount) to the matching
871    /// handler and run it, returning its [`Reply`]. Generated by the macro.
872    async fn actus_dispatch(&self, action: &str, params: Params) -> Reply;
873
874    /// The controller's type name, for diagnostics and route auditing.
875    fn __name(&self) -> &'static str;
876
877    /// The controller's declared routes, for introspection (OpenAPI
878    /// generation, route audits). Defaults to empty; the macro overrides it.
879    fn actus_describe_routes(&self) -> Vec<RouteDef> {
880        vec![]
881    }
882
883    /// Per-controller maximum buffered body size, in bytes. Returned by the
884    /// `#[controller(max_body_bytes = …)]` attribute when set; `None` means the
885    /// controller defers to the server-level cap (`Server::with_max_body_bytes`).
886    ///
887    /// Resolution at request time (see `Server::handle_request_inner`):
888    /// controller value if `Some`, otherwise the server-wide cap, otherwise
889    /// `DEFAULT_MAX_BODY_BYTES` (2 MiB).
890    ///
891    /// The framework calls this *before* buffering the body — so a 1 KB
892    /// controller cap rejects a 50 KB request before the bytes are
893    /// allocated. (A request body big enough to be a memory concern
894    /// shouldn't get past the framework regardless of where the handler
895    /// would have rejected it.)
896    fn actus_max_body_bytes(&self) -> Option<usize> {
897        None
898    }
899
900    /// Per-controller rate-limit *class* label, as declared by
901    /// `#[controller(rate_limit = "name")]`. `None` (the default) means the
902    /// controller declared no class.
903    ///
904    /// This is a **label, not a policy**. Actus is policy-agnostic: it ships
905    /// no limiter algorithm, key function, or store, because the framework
906    /// can't pick those correctly for someone else (which key — IP / user /
907    /// API key? which algorithm — token bucket / sliding window? which store
908    /// — in-memory / Redis?). Those are application decisions, so the limiter
909    /// itself stays an application `Middleware`.
910    ///
911    /// What the framework *does* own is auditability and the response shape.
912    /// The server stamps this label onto the matched request (surfaced as
913    /// `Request::rate_limit_class` in `actus-server`), so a reviewer can read
914    /// each endpoint's rate-limit class straight off the `#[controller(...)]`
915    /// line, and an application's rate-limit `Middleware` can map class →
916    /// policy and reject over-limit requests with
917    /// [`WebError::TooManyRequests`] (429 + `Retry-After`, also framework-owned).
918    /// Two controllers sharing a class share a limit namespace; what each
919    /// class *means* is the application's call.
920    ///
921    /// Resolution is per-controller, mirroring [`Controller::actus_max_body_bytes`].
922    /// A per-route override would be an additive future change (the same shape
923    /// as the per-route body-cap proposal).
924    fn actus_rate_limit(&self) -> Option<&'static str> {
925        None
926    }
927
928    /// The controller's declared caller expectation, as set by
929    /// `#[controller(expects = "…")]`. `None` (the default) means the
930    /// controller declared nothing.
931    ///
932    /// This is a **label, not a policy** — and specifically a **floor**: it
933    /// names the *least-privileged caller the controller is written to
934    /// accept* (`"credential"`, `"anonymous"`, `"signature"`, …). It is not a
935    /// ceiling — individual routes may demand more in their handlers — and it
936    /// is not authorization: Actus never interprets the value, compares it
937    /// only for presence/equality in application code, and hands it back
938    /// untouched. What each label *means*, and what happens to a caller below
939    /// the floor, is entirely the application's (typically: a startup
940    /// coverage check over [`Router::mounts`], a declaration-keyed gate in a
941    /// `prepare` hook or middleware, and a probe test — see the README's
942    /// "Route families" section).
943    ///
944    /// Resolution is per-controller, mirroring
945    /// [`Controller::actus_rate_limit`]. A controller with routes above its
946    /// floor declares the floor and enforces the stricter routes in their
947    /// handlers.
948    ///
949    /// [`Router::mounts`]: https://docs.rs/actus-server/latest/actus_server/struct.Router.html#method.mounts
950    fn actus_expects(&self) -> Option<&'static str> {
951        None
952    }
953
954    /// The path of the controller's `prepare` hook, as written in
955    /// `#[controller(prepare = …)]` (e.g. `"Self::auth"`), or `None` when the
956    /// controller declared no hook.
957    ///
958    /// **Presence is the payload.** A route-family coverage check reads this
959    /// to enforce rules like *"a controller whose floor is `"credential"`
960    /// must have a hook to refuse anonymous callers with"* — the string
961    /// itself is a courtesy for route dumps and diagnostics, not an
962    /// invocation handle.
963    fn actus_prepare(&self) -> Option<&'static str> {
964        None
965    }
966}
967
968// =========================
969// Route families — the compile-time half (Phase 2)
970// =========================
971
972/// Implemented by the `#[controller]` macro for every controller that declares
973/// `expects = "…"`; **absent on a controller that declares nothing**, which is
974/// the whole point. The `families { … }` block in `app_routes!` requires this
975/// trait of every controller mounted under a listed prefix, so a silently
976/// undeclared controller under a covered prefix is a **compile error** — the
977/// `on_unimplemented` message below is the one the developer reads.
978///
979/// `EXPECTS` is the same label [`Controller::actus_expects`] returns at runtime,
980/// lifted to a constant so a family's *accepted set* can be checked at compile
981/// time too (see [`declares_expectation_in`]).
982#[diagnostic::on_unimplemented(
983    message = "`{Self}` is mounted under a route family that requires a declared caller expectation",
984    label = "this controller declares no `expects` label",
985    note = "add `expects = \"…\"` to its `#[controller(...)]` attribute, or drop the prefix \
986            from the `families` block in `app_routes!`"
987)]
988pub trait DeclaresExpectation {
989    /// The declared floor — the value of `#[controller(expects = …)]`.
990    const EXPECTS: &'static str;
991}
992
993/// A route family's accepted floors, as a type — generated by `app_routes!` for
994/// each `"prefix" => ["floor", …]` entry in its `families` block, and handed to
995/// [`declares_expectation_in`] so the acceptance check can run in a `const`.
996pub trait Family {
997    /// The floors this family accepts.
998    const ACCEPTS: &'static [&'static str];
999}
1000
1001/// `const`-evaluable string equality (`==` on `str` is not `const`).
1002pub const fn str_eq(a: &str, b: &str) -> bool {
1003    let (a, b) = (a.as_bytes(), b.as_bytes());
1004    if a.len() != b.len() {
1005        return false;
1006    }
1007    let mut i = 0;
1008    while i < a.len() {
1009        if a[i] != b[i] {
1010            return false;
1011        }
1012        i += 1;
1013    }
1014    true
1015}
1016
1017/// `const`-evaluable "is `floor` one of `accepts`".
1018pub const fn floor_accepted(floor: &str, accepts: &[&str]) -> bool {
1019    let mut i = 0;
1020    while i < accepts.len() {
1021        if str_eq(floor, accepts[i]) {
1022            return true;
1023        }
1024        i += 1;
1025    }
1026    false
1027}
1028
1029/// Identity pass-through that only compiles for a controller implementing
1030/// [`DeclaresExpectation`]. `app_routes!` wraps a mount's construction
1031/// expression in this when the mount falls under a `families` prefix declared
1032/// without an accepted set — so *presence* of a declaration is checked at
1033/// compile time, and any construction form (`Foo { db }`, `Foo::new(db)`,
1034/// `make_foo()`) works without the macro naming the type.
1035#[inline(always)]
1036pub fn declares_expectation<T: Controller + DeclaresExpectation>(c: T) -> T {
1037    c
1038}
1039
1040/// [`declares_expectation`], plus the *value* check: the controller's
1041/// [`DeclaresExpectation::EXPECTS`] must be one of `F::ACCEPTS`. The check is a
1042/// `const` assertion evaluated when this instantiation is compiled — i.e. when
1043/// the generated `init()` is reachable from something that runs, which in an
1044/// application it always is. A failure reads as an `E0080` naming the
1045/// controller type in its "while instantiating" note. ⚠️ Evaluation needs
1046/// codegen: `cargo check` (and IDE diagnostics built on it) does not run it;
1047/// `cargo build`, `cargo test` and CI do — only the presence bound is a type
1048/// error visible under `check`. (The boot-time coverage check over
1049/// `Router::mounts()` remains the backstop that needs no reachability.)
1050#[inline(always)]
1051pub fn declares_expectation_in<F: Family, T: Controller + DeclaresExpectation>(c: T) -> T {
1052    const {
1053        assert!(
1054            floor_accepted(T::EXPECTS, F::ACCEPTS),
1055            "this controller declares a caller expectation (`expects = …`) that its route \
1056             family does not accept — see the `families` block in `app_routes!`"
1057        )
1058    };
1059    c
1060}
1061
1062/// A list of `(mount, controller-factory)` pairs — the route-registration
1063/// shape the `app_routes!` macro builds when wiring controllers into a router.
1064pub type Routes = Vec<(
1065    &'static str,
1066    Box<dyn Fn() -> Box<dyn Controller> + Send + Sync>,
1067)>;
1068
1069/// A marker macro to define routes within a `#[controller]` impl block.
1070/// The `#[controller]` procedural macro is responsible for parsing this.
1071#[macro_export]
1072macro_rules! routes {
1073    ($($tokens:tt)*) => {};
1074}
1075
1076#[cfg(test)]
1077mod match_pattern_tests {
1078    use super::routing::match_pattern;
1079
1080    fn cap(pattern: &str, path: &str) -> Option<Vec<(String, String)>> {
1081        match_pattern(pattern, path).map(|m| {
1082            let mut v: Vec<_> = m.into_iter().collect();
1083            v.sort();
1084            v
1085        })
1086    }
1087
1088    fn pair(k: &str, v: &str) -> (String, String) {
1089        (k.to_string(), v.to_string())
1090    }
1091
1092    #[test]
1093    fn fixed_patterns_still_work() {
1094        assert_eq!(cap("", ""), Some(vec![]));
1095        assert_eq!(cap("{id}", "42"), Some(vec![pair("id", "42")]));
1096        assert_eq!(
1097            cap("posts/{id}/comments", "posts/3/comments"),
1098            Some(vec![pair("id", "3")])
1099        );
1100        assert_eq!(cap("a/b", "a/b/c"), None);
1101        assert_eq!(cap("a/b/c", "a/b"), None);
1102        assert_eq!(cap("posts/{id}", "users/3"), None);
1103    }
1104
1105    #[test]
1106    fn required_segments_dont_match_the_empty_action() {
1107        // A `{id}` (or any literal) is required: it has no match when there's
1108        // no segment for it. The empty action is the empty segment list, not
1109        // a one-element list containing `""`.
1110        assert_eq!(cap("{id}", ""), None);
1111        assert_eq!(cap("posts", ""), None);
1112        assert_eq!(cap("{a}/{b}", "x"), None);
1113        // ...but the empty pattern is *defined* as the empty segment list,
1114        // so it matches the empty action (this is how `"" => index` works).
1115        assert_eq!(cap("", ""), Some(vec![]));
1116        assert_eq!(cap("", "x"), None);
1117    }
1118
1119    #[test]
1120    fn rest_param_captures_remainder() {
1121        assert_eq!(
1122            cap("{folder_id}/{...path}", "abc/x/y/z"),
1123            Some(vec![pair("folder_id", "abc"), pair("path", "x/y/z")])
1124        );
1125        // zero trailing segments → rest is empty (folder_id is still present)
1126        assert_eq!(
1127            cap("{folder_id}/{...path}", "abc"),
1128            Some(vec![pair("folder_id", "abc"), pair("path", "")])
1129        );
1130        // ...but the required folder_id has no segment in the empty action
1131        assert_eq!(cap("{folder_id}/{...path}", ""), None);
1132    }
1133
1134    #[test]
1135    fn rest_param_as_sole_token() {
1136        assert_eq!(cap("{...path}", "a/b/c"), Some(vec![pair("path", "a/b/c")]));
1137        assert_eq!(cap("{...path}", "a"), Some(vec![pair("path", "a")]));
1138        // a sole rest token explicitly matches zero segments
1139        assert_eq!(cap("{...path}", ""), Some(vec![pair("path", "")]));
1140    }
1141
1142    #[test]
1143    fn rest_param_after_literal_prefix() {
1144        assert_eq!(
1145            cap("files/{...path}", "files/x/y"),
1146            Some(vec![pair("path", "x/y")])
1147        );
1148        assert_eq!(
1149            cap("files/{...path}", "files"),
1150            Some(vec![pair("path", "")])
1151        );
1152        assert_eq!(cap("files/{...path}", "other/x"), None);
1153        // a literal prefix longer than the path can't match
1154        assert_eq!(cap("a/b/{...path}", "a"), None);
1155    }
1156}
1157
1158#[cfg(test)]
1159mod resolve_tests {
1160    use super::routing::resolve;
1161    use super::*;
1162    use bytes::Bytes;
1163    use std::collections::HashMap;
1164
1165    fn params_with(verb: Verb, query: HashMap<String, Vec<String>>) -> Params {
1166        Params::new(verb, query, None, Bytes::new(), HashMap::new())
1167    }
1168
1169    #[test]
1170    fn headers_are_a_multimap_first_value_wins_for_scalar_access() {
1171        // Two values for one header name (e.g. a proxy chain stamping
1172        // `Forwarded` twice). `header()` returns the first; `header_all()`
1173        // returns both, in receipt order. Absent headers come back as `None`
1174        // / empty slice respectively.
1175        let mut headers = HashMap::new();
1176        headers.insert(
1177            "forwarded".to_string(),
1178            vec!["for=1.2.3.4".to_string(), "for=10.0.0.1".to_string()],
1179        );
1180        headers.insert("x-trace-id".to_string(), vec!["abc-123".to_string()]);
1181        let p = Params::new(Verb::GET, HashMap::new(), None, Bytes::new(), headers);
1182
1183        // Case-insensitive lookup; first value for scalar access.
1184        assert_eq!(p.header("Forwarded"), Some("for=1.2.3.4"));
1185        assert_eq!(p.header("FORWARDED"), Some("for=1.2.3.4"));
1186        assert_eq!(p.header_all("Forwarded"), ["for=1.2.3.4", "for=10.0.0.1"]);
1187
1188        // Single-value headers still work — header_all yields a one-element
1189        // slice, header yields the same value.
1190        assert_eq!(p.header("X-Trace-Id"), Some("abc-123"));
1191        assert_eq!(p.header_all("X-Trace-Id"), ["abc-123"]);
1192
1193        // Absent: None / empty slice.
1194        assert_eq!(p.header("Authorization"), None);
1195        assert!(p.header_all("Authorization").is_empty());
1196    }
1197
1198    #[test]
1199    fn params_query_exposes_the_whole_multimap() {
1200        let mut q = HashMap::new();
1201        q.insert("a".to_string(), vec!["1".to_string(), "2".to_string()]);
1202        q.insert("b".to_string(), vec!["3".to_string()]);
1203        let p = params_with(Verb::GET, q);
1204        assert_eq!(p.query().len(), 2);
1205        assert_eq!(
1206            p.query().get("a").unwrap(),
1207            &["1".to_string(), "2".to_string()]
1208        );
1209        // scalar view still takes the first
1210        assert_eq!(p.get_optional("a"), Some("1"));
1211    }
1212
1213    #[test]
1214    fn verb_mismatch_yields_405_with_sorted_deduped_allow_list() {
1215        // `""` matches the action `""` for both routes; the request verb
1216        // (PUT) matches neither, so we get 405 carrying the union of their
1217        // verbs — sorted and deduped, so the `Allow` header is deterministic.
1218        static ROUTES: &[RouteDef] = &[
1219            RouteDef {
1220                pattern: "",
1221                handler_id: "create",
1222                handler: "create",
1223                verb: &[Verb::POST],
1224                params: &[],
1225                doc: None,
1226            },
1227            RouteDef {
1228                pattern: "",
1229                handler_id: "list",
1230                handler: "list",
1231                verb: &[Verb::GET],
1232                params: &[],
1233                doc: None,
1234            },
1235        ];
1236        match resolve(
1237            ROUTES,
1238            "",
1239            &params_with(Verb::PUT, HashMap::new()),
1240            ControllerMode::Strict,
1241        ) {
1242            Err(WebError::MethodNotAllowed(methods)) => assert_eq!(methods, ["GET", "POST"]),
1243            other => panic!("expected 405, got {other:?}"),
1244        }
1245        // GET matches the second route → Ok.
1246        assert!(
1247            resolve(
1248                ROUTES,
1249                "",
1250                &params_with(Verb::GET, HashMap::new()),
1251                ControllerMode::Strict
1252            )
1253            .is_ok()
1254        );
1255    }
1256
1257    #[test]
1258    fn no_pattern_match_is_404_not_405() {
1259        static ROUTES: &[RouteDef] = &[RouteDef {
1260            pattern: "items",
1261            handler_id: "h",
1262            handler: "h",
1263            verb: &[Verb::GET],
1264            params: &[],
1265            doc: None,
1266        }];
1267        match resolve(
1268            ROUTES,
1269            "other",
1270            &params_with(Verb::DELETE, HashMap::new()),
1271            ControllerMode::Strict,
1272        ) {
1273            Err(WebError::NotFound) => {}
1274            other => panic!("expected 404, got {other:?}"),
1275        }
1276    }
1277
1278    #[test]
1279    fn vec_string_query_param_collects_all_values() {
1280        static ROUTES: &[RouteDef] = &[RouteDef {
1281            pattern: "",
1282            handler_id: "h",
1283            handler: "h",
1284            verb: &[Verb::GET],
1285            params: &[ParamDef {
1286                name: "tags",
1287                ty: ParamType::StringArray,
1288                source: ParamSource::Query,
1289                default: None,
1290            }],
1291            doc: None,
1292        }];
1293
1294        let mut q = HashMap::new();
1295        q.insert(
1296            "tags".to_string(),
1297            vec!["a".to_string(), "b".to_string(), "c".to_string()],
1298        );
1299        let (_, extracted) = resolve(
1300            ROUTES,
1301            "",
1302            &params_with(Verb::GET, q),
1303            ControllerMode::Strict,
1304        )
1305        .expect("route matches");
1306        assert_eq!(extracted.get_string_array("tags").unwrap(), ["a", "b", "c"]);
1307
1308        // A one-element array flows through the same path; a scalar accessor
1309        // takes the first value.
1310        let mut q1 = HashMap::new();
1311        q1.insert("tags".to_string(), vec!["solo".to_string()]);
1312        let (_, e1) = resolve(
1313            ROUTES,
1314            "",
1315            &params_with(Verb::GET, q1),
1316            ControllerMode::Strict,
1317        )
1318        .expect("route matches");
1319        assert_eq!(e1.get_string_array("tags").unwrap(), ["solo"]);
1320        assert_eq!(e1.get_string("tags").unwrap(), "solo");
1321
1322        // Absent is *not* a 400 for a `Vec<String>` param — it's the empty
1323        // list (unlike a missing required scalar).
1324        let (_, e2) = resolve(
1325            ROUTES,
1326            "",
1327            &params_with(Verb::GET, HashMap::new()),
1328            ControllerMode::Strict,
1329        )
1330        .expect("route matches with no query");
1331        assert!(e2.get_string_array("tags").unwrap().is_empty());
1332    }
1333}
1334
1335#[cfg(test)]
1336mod covering_family_tests {
1337    use super::routing::covering_family;
1338
1339    #[test]
1340    fn segment_aligned_longest_prefix_wins() {
1341        let fams = ["api", "api/auth", "public"];
1342        assert_eq!(covering_family("api/things", fams), Some("api"));
1343        assert_eq!(covering_family("api/auth", fams), Some("api/auth"));
1344        assert_eq!(covering_family("api/auth/oauth", fams), Some("api/auth"));
1345        assert_eq!(
1346            covering_family("api/authx", fams),
1347            Some("api"),
1348            "segment-aligned, not a string prefix"
1349        );
1350        assert_eq!(covering_family("apiary", fams), None);
1351        assert_eq!(covering_family("health", fams), None);
1352    }
1353
1354    #[test]
1355    fn star_sugar_and_root() {
1356        assert_eq!(covering_family("api/things", ["api/*"]), Some("api/*"));
1357        assert_eq!(covering_family("anything/at/all", ["*"]), Some("*"));
1358        assert_eq!(covering_family("", [""]), Some(""));
1359        assert_eq!(covering_family("/api/", ["api"]), Some("api"));
1360    }
1361
1362    #[test]
1363    fn a_later_identical_prefix_wins_like_the_macro() {
1364        assert_eq!(covering_family("api/x", ["api", "api/*"]), Some("api/*"));
1365    }
1366}