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    /// When a host alias routed this request: the path the alias put in front
306    /// of the request's own path — the aliased mount and the host's captured
307    /// labels, e.g. `"tenants/acme"`. `None` when the request was routed by
308    /// its own path, including a shared mount reached on an aliased host.
309    ///
310    /// This is the router's own answer to "did this request arrive on an
311    /// aliased host?". A handler that must behave differently there — say,
312    /// root-relative links on the tenant's own host, prefixed ones under the
313    /// mount — reads it here instead of matching `Host` a second time, so the
314    /// handler and the router cannot disagree. (See `RouterBuilder::host_alias`
315    /// in `actus-server`, and the README's "Host aliases" section.)
316    pub fn alias_prefix(&self) -> Option<&str> {
317        self.get::<AliasPrefix>().map(|p| p.0.as_str())
318    }
319
320    /// Record that a host alias routed this request under `prefix` — what
321    /// [`Params::alias_prefix`] then reports. The server calls this; it is
322    /// public so a test can build the `Params` an aliased request carries.
323    pub fn set_alias_prefix(&mut self, prefix: impl Into<String>) {
324        self.insert(AliasPrefix(prefix.into()));
325    }
326
327    /// The HTTP verb this request was dispatched with.
328    pub fn verb(&self) -> Verb {
329        self.verb
330    }
331
332    /// Look up a request header (case-insensitive). Returns the *first*
333    /// value if the header appears more than once; see [`Params::header_all`]
334    /// for every value.
335    pub fn header(&self, name: &str) -> Option<&str> {
336        self.headers
337            .get(&name.to_ascii_lowercase())
338            .and_then(|values| values.first())
339            .map(String::as_str)
340    }
341
342    /// Every value for a request header (case-insensitive), in receipt
343    /// order. Empty slice if the header wasn't present. Use this for headers
344    /// that can legitimately appear multiple times — `Forwarded`, `Via`,
345    /// `X-Forwarded-For` (when proxies emit one entry per hop), etc.
346    pub fn header_all(&self, name: &str) -> &[String] {
347        self.headers
348            .get(&name.to_ascii_lowercase())
349            .map(Vec::as_slice)
350            .unwrap_or(&[])
351    }
352
353    /// Convenience: extract a Bearer token from the `Authorization` header.
354    /// Returns `None` if the header is missing or doesn't start with `Bearer `.
355    pub fn bearer_token(&self) -> Option<&str> {
356        let auth = self.header("authorization")?;
357        auth.strip_prefix("Bearer ")
358            .or_else(|| auth.strip_prefix("bearer "))
359    }
360
361    // Core methods for parameter extraction
362
363    /// The first value of query parameter `name`, or a `400 Bad Request` if
364    /// it's absent.
365    pub fn require(&self, name: &str) -> Result<&str, WebError> {
366        self.first(name)
367            .ok_or_else(|| WebError::BadRequest(format!("Missing required parameter: {}", name)))
368    }
369
370    /// Look up a query parameter's first value; returns `None` if absent.
371    pub fn get_optional(&self, name: &str) -> Option<&str> {
372        self.first(name)
373    }
374
375    /// Parse query parameter `name` as an `i64`; `400` if missing or unparsable.
376    pub fn get_int(&self, name: &str) -> Result<i64, WebError> {
377        self.require(name)?
378            .parse()
379            .map_err(|_| WebError::BadRequest(format!("Invalid integer: {}", name)))
380    }
381
382    /// Parse optional query parameter `name` as an `i64`; `Ok(None)` if absent,
383    /// `400` if present but unparsable.
384    pub fn get_int_optional(&self, name: &str) -> Result<Option<i64>, WebError> {
385        match self.get_optional(name) {
386            Some(s) => s
387                .parse()
388                .map(Some)
389                .map_err(|_| WebError::BadRequest(format!("Invalid integer: {}", name))),
390            None => Ok(None),
391        }
392    }
393
394    // Extended type methods
395
396    /// Parse query parameter `name` as a `u64`; `400` if missing or unparsable.
397    pub fn get_u64(&self, name: &str) -> Result<u64, WebError> {
398        self.require(name)?
399            .parse()
400            .map_err(|_| WebError::BadRequest(format!("Invalid u64: {}", name)))
401    }
402
403    /// Parse optional query parameter `name` as a `u64`; `Ok(None)` if absent,
404    /// `400` if present but unparsable.
405    pub fn get_u64_optional(&self, name: &str) -> Result<Option<u64>, WebError> {
406        match self.get_optional(name) {
407            Some(s) => s
408                .parse()
409                .map(Some)
410                .map_err(|_| WebError::BadRequest(format!("Invalid u64: {}", name))),
411            None => Ok(None),
412        }
413    }
414
415    /// Parse query parameter `name` as a `u32`; `400` if missing or unparsable.
416    pub fn get_u32(&self, name: &str) -> Result<u32, WebError> {
417        self.require(name)?
418            .parse()
419            .map_err(|_| WebError::BadRequest(format!("Invalid u32: {}", name)))
420    }
421
422    /// Parse optional query parameter `name` as a `u32`; `Ok(None)` if absent,
423    /// `400` if present but unparsable.
424    pub fn get_u32_optional(&self, name: &str) -> Result<Option<u32>, WebError> {
425        match self.get_optional(name) {
426            Some(s) => s
427                .parse()
428                .map(Some)
429                .map_err(|_| WebError::BadRequest(format!("Invalid u32: {}", name))),
430            None => Ok(None),
431        }
432    }
433
434    /// Parse query parameter `name` as an `f64`; `400` if missing or unparsable.
435    pub fn get_f64(&self, name: &str) -> Result<f64, WebError> {
436        self.require(name)?
437            .parse()
438            .map_err(|_| WebError::BadRequest(format!("Invalid float: {}", name)))
439    }
440
441    /// Parse optional query parameter `name` as an `f64`; `Ok(None)` if absent,
442    /// `400` if present but unparsable.
443    pub fn get_f64_optional(&self, name: &str) -> Result<Option<f64>, WebError> {
444        match self.get_optional(name) {
445            Some(s) => s
446                .parse()
447                .map(Some)
448                .map_err(|_| WebError::BadRequest(format!("Invalid float: {}", name))),
449            None => Ok(None),
450        }
451    }
452
453    /// Read query parameter `name` as a bool — `false` when absent, empty,
454    /// `"false"`, or `"0"`; `true` otherwise.
455    pub fn get_bool(&self, name: &str) -> bool {
456        self.first(name)
457            .map(|s| !s.is_empty() && s != "false" && s != "0")
458            .unwrap_or(false)
459    }
460
461    /// Like [`Params::get_bool`], but `None` when the parameter is absent
462    /// (rather than `false`).
463    pub fn get_bool_optional(&self, name: &str) -> Option<bool> {
464        self.first(name)
465            .map(|s| !s.is_empty() && s != "false" && s != "0")
466    }
467
468    /// All values of query parameter `name`, in request order (empty if the
469    /// name wasn't present). Backs `Vec<String>` handler parameters.
470    pub fn get_all(&self, name: &str) -> Result<Vec<String>, WebError> {
471        Ok(self.query.get(name).cloned().unwrap_or_default())
472    }
473
474    /// All values of query parameter `name`; `None` if the name wasn't present
475    /// at all (vs. `Some(vec![])`, which urlencoding can't actually produce).
476    pub fn get_all_optional(&self, name: &str) -> Option<Vec<String>> {
477        self.query.get(name).cloned()
478    }
479
480    /// The parsed JSON request body, or `400 Bad Request` if there wasn't one.
481    pub fn json_body(&self) -> Result<JsonValue, WebError> {
482        self.body
483            .clone()
484            .ok_or_else(|| WebError::BadRequest("Missing JSON body".to_string()))
485    }
486
487    /// In strict mode, return any query keys *not* in `expected` (so the caller
488    /// can reject the request); `None` if every key was expected.
489    pub fn check_unexpected(&self, expected: &[&str]) -> Option<Vec<String>> {
490        let unexpected: Vec<String> = self
491            .query
492            .keys()
493            .filter(|k| !expected.contains(&k.as_str()))
494            .cloned()
495            .collect();
496
497        if unexpected.is_empty() {
498            None
499        } else {
500            Some(unexpected)
501        }
502    }
503}
504
505/// The prefix a host alias put in front of a request's path — held in the
506/// `Params` extensions under a type nothing outside this crate can name, so a
507/// request no alias touched carries nothing at all. Read with
508/// [`Params::alias_prefix`].
509struct AliasPrefix(String);
510
511// =========================
512// Extracted parameters (after route resolution)
513// =========================
514
515/// Parameters extracted after route resolution — path captures plus the
516/// declared query and body values. The `#[controller]` macro reads typed
517/// handler arguments out of this; application code rarely touches it directly.
518#[derive(Debug)]
519pub struct ExtractedParams {
520    /// Path captures (single-segment `{name}` or the joined `{...rest}`).
521    path: HashMap<String, String>,
522    /// Declared query parameters that were present, with *all* their values
523    /// (see [`Params`] — query is a multimap).
524    query: HashMap<String, Vec<String>>,
525    body: Option<JsonValue>,
526    raw_body: Bytes,
527}
528
529impl ExtractedParams {
530    /// The single scalar value for `name`: a path capture takes precedence,
531    /// otherwise the first query value. `None` if neither has it.
532    fn scalar(&self, name: &str) -> Option<&str> {
533        self.path.get(name).map(String::as_str).or_else(|| {
534            self.query
535                .get(name)
536                .and_then(|values| values.first())
537                .map(String::as_str)
538        })
539    }
540
541    fn require_scalar(&self, name: &str) -> Result<&str, WebError> {
542        self.scalar(name)
543            .ok_or_else(|| WebError::BadRequest(format!("Missing parameter: {}", name)))
544    }
545
546    /// The value of path/query parameter `name` as a `String`; `400` if absent.
547    pub fn get_string(&self, name: &str) -> Result<String, WebError> {
548        self.require_scalar(name).map(str::to_string)
549    }
550
551    /// Parse path/query parameter `name` as an `i64`; `400` if missing or
552    /// unparsable.
553    pub fn get_i64(&self, name: &str) -> Result<i64, WebError> {
554        self.require_scalar(name)?
555            .parse()
556            .map_err(|_| WebError::BadRequest(format!("Invalid integer: {}", name)))
557    }
558
559    /// Parse path/query parameter `name` as a `u64`; `400` if missing or
560    /// unparsable.
561    pub fn get_u64(&self, name: &str) -> Result<u64, WebError> {
562        self.require_scalar(name)?
563            .parse()
564            .map_err(|_| WebError::BadRequest(format!("Invalid u64: {}", name)))
565    }
566
567    /// Parse path/query parameter `name` as a `u32`; `400` if missing or
568    /// unparsable.
569    pub fn get_u32(&self, name: &str) -> Result<u32, WebError> {
570        self.require_scalar(name)?
571            .parse()
572            .map_err(|_| WebError::BadRequest(format!("Invalid u32: {}", name)))
573    }
574
575    /// Parse path/query parameter `name` as an `f64`; `400` if missing or
576    /// unparsable.
577    pub fn get_f64(&self, name: &str) -> Result<f64, WebError> {
578        self.require_scalar(name)?
579            .parse()
580            .map_err(|_| WebError::BadRequest(format!("Invalid float: {}", name)))
581    }
582
583    /// Read path/query parameter `name` as a bool — `false` for an empty value,
584    /// `"false"` or `"0"`; `true` otherwise. **`400` when absent**, exactly as
585    /// `get_string` / `get_i64` / every other scalar getter, all of which go
586    /// through `require_scalar`.
587    ///
588    /// ⛔ **It did NOT always do that**, and the history is the point. It used to
589    /// answer `Ok(false)` for a missing parameter — the one getter that invented
590    /// a value instead of erroring. The generated default is
591    /// `get_x(name).unwrap_or(default)`, which relies on that `Err`, so this
592    /// method swallowed the absence and **every `param: bool = true` silently
593    /// behaved as `false`**.
594    ///
595    /// ⚠️ Found in a consumer, 2026-09-04: a cancellation route declared
596    /// `at_period_end: bool = true` and documented the reversible, deferred form as
597    /// its default. A client that omitted the parameter got the **immediate**
598    /// cancellation instead — the destructive direction, and the opposite of what
599    /// the route promised. It stayed invisible because the route behaved correctly
600    /// whenever the parameter **was** supplied.
601    ///
602    /// ⇒ Use [`Self::get_bool_optional`] when you need to tell an absent
603    /// parameter from an explicit `false`; that is what a declared default reads.
604    ///
605    /// ⚠️ **Correction, 2026-09-04.** This doc used to justify the old
606    /// `unwrap_or(false)` by saying a bare `param: bool` means *"false unless
607    /// asked for"* (`confirm`, `dry_run`) and that erroring on absence would turn
608    /// those into `400`s. **That was false and was never checked**: a bare `bool`
609    /// *already* 400s, because [`routing::param_is_required`] reports it required
610    /// and `resolve` rejects the request before extraction runs. An optional flag
611    /// is written `confirm: bool = false`. The fallback was the last code in the
612    /// crate asserting the opposite of what the router does, so it is gone —
613    /// nothing outside this crate could reach it, since [`ExtractedParams`] has
614    /// no public constructor and `resolve` is the only way to obtain one.
615    pub fn get_bool(&self, name: &str) -> Result<bool, WebError> {
616        Ok(!matches!(self.require_scalar(name)?, "" | "false" | "0"))
617    }
618
619    /// Like [`Self::get_bool`], but distinguishes **absent** (`None`) from the
620    /// value `false` — the reader a declared default needs.
621    ///
622    /// ⭐ This exists because `bool` is the only parameter type where "not
623    /// supplied" and "supplied as the falsy value" are both meaningful. For every
624    /// other type the absence is already an `Err` and the default falls out of
625    /// `unwrap_or`; here it has to be asked for explicitly.
626    ///
627    /// The `_optional` name is the crate's convention for exactly this pair —
628    /// see [`Params::get_bool_optional`], which draws the same distinction on
629    /// the pre-resolution type. (This one returns `Result` because every
630    /// `ExtractedParams` getter does; it has no failure case of its own.)
631    pub fn get_bool_optional(&self, name: &str) -> Result<Option<bool>, WebError> {
632        Ok(self
633            .scalar(name)
634            .map(|s| !s.is_empty() && s != "false" && s != "0"))
635    }
636
637    /// All values for `name` (in request order; empty if the name wasn't
638    /// present). Backs `Vec<String>` handler parameters — a one-element list
639    /// (`?tags=a`) and a many-element one (`?tags=a&tags=b`) flow through the
640    /// same path.
641    pub fn get_string_array(&self, name: &str) -> Result<Vec<String>, WebError> {
642        Ok(self.query.get(name).cloned().unwrap_or_default())
643    }
644
645    /// The parsed JSON request body, or `400 Bad Request` if there wasn't one.
646    pub fn get_json_body(&self) -> Result<JsonValue, WebError> {
647        self.body
648            .clone()
649            .ok_or_else(|| WebError::BadRequest("Missing JSON body".to_string()))
650    }
651
652    /// Raw bytes of the request body. See [`Params::body_bytes`] for the
653    /// content-type-aware semantics. Used by the macro to extract a
654    /// `Bytes` handler argument.
655    pub fn get_body_bytes(&self) -> Bytes {
656        self.raw_body.clone()
657    }
658}
659
660// =========================
661// Routing utilities module
662// =========================
663
664/// Route resolution helpers — matching a request path + verb against a
665/// controller's `&[RouteDef]` and extracting the path/query/body parameters.
666/// Used by the `#[controller]` macro's generated dispatch; exposed for tools
667/// and tests that need to resolve routes directly.
668pub mod routing {
669    use super::*;
670    use std::collections::HashMap;
671
672    /// The segments of a mount path or family prefix, normalised exactly as
673    /// `RouterBuilder::add_route` and the `families` block of `app_routes!`
674    /// normalise them: surrounding slashes trimmed, empty segments dropped, a
675    /// trailing `*` (the catch-all sugar) removed — so `"api/*"`, `"/api/"`
676    /// and `"api"` are the same prefix, and `""` / `"*"` is the root.
677    pub fn family_segments(path: &str) -> Vec<&str> {
678        let mut segs: Vec<&str> = path
679            .trim_matches('/')
680            .split('/')
681            .filter(|s| !s.is_empty())
682            .collect();
683        if segs.last() == Some(&"*") {
684            segs.pop();
685        }
686        segs
687    }
688
689    /// Which of `prefixes` covers `mount` — by the **same rule the `families`
690    /// block of `app_routes!` applies at compile time**, so a boot-time check
691    /// written against this cannot disagree with the compile-time one:
692    /// segment-aligned (`"api"` covers `"api/things"`, never `"apiary"`), the
693    /// **longest** covering prefix wins (so `"api/auth"` carves its subtree out
694    /// of `"api"`), a trailing `*` reads as the prefix before it, and the root
695    /// (`""` or `"*"`) covers every mount. Returns the winning prefix as it was
696    /// given, so the caller can look it up in its own table. On two prefixes
697    /// that normalise identically, the later one wins, as in the macro.
698    ///
699    /// Do not re-derive this with `mount.split('/').next()`: that matches by
700    /// first segment, agrees with the macro only while every family is a single
701    /// segment, and silently diverges the moment one nests.
702    pub fn covering_family<'a, I>(mount: &str, prefixes: I) -> Option<&'a str>
703    where
704        I: IntoIterator<Item = &'a str>,
705    {
706        let m = family_segments(mount);
707        let mut best: Option<(&'a str, usize)> = None;
708        for p in prefixes {
709            let ps = family_segments(p);
710            let covers = ps.len() <= m.len() && ps.iter().zip(m.iter()).all(|(a, b)| a == b);
711            if covers && best.is_none_or(|(_, n)| ps.len() >= n) {
712                best = Some((p, ps.len()));
713            }
714        }
715        best.map(|(p, _)| p)
716    }
717
718    /// Whether an absent value for `param` makes the request a `400`.
719    ///
720    /// ⭐ **This is the rule [`resolve`] enforces, exported so that a tool
721    /// reporting requiredness cannot disagree with the router that enforces it.**
722    /// The OpenAPI generator's `required` flag is computed from this; before it
723    /// was extracted the same expression was written out in two crates with
724    /// nothing relating them, and they agreed only by diligence.
725    ///
726    /// The rule is **syntactic, and uniform across every scalar type**: a query
727    /// parameter is required unless it declared a default. Requiredness is
728    /// therefore readable straight off a `routes!` block — `name: T` is required,
729    /// `name: T = x` is optional — without knowing what `T` is.
730    ///
731    /// ⛔ **`bool` is deliberately NOT exempt**, though the pull to exempt it is
732    /// strong: `confirm: bool` is required, and an optional flag must be written
733    /// `confirm: bool = false`. Two reasons. It keeps *both* meanings sayable —
734    /// exempting `bool` would leave no way to declare a required one. And an
735    /// absent `bool` and an explicit `?flag=false` are genuinely distinguishable
736    /// on the wire (see [`ExtractedParams::get_bool_optional`]), so reading
737    /// absence as `false` would discard a distinction the request carries.
738    ///
739    /// [`ParamType::StringArray`] is exempt for the opposite reason — its
740    /// exemption is **forced, not chosen**: urlencoding cannot express "present
741    /// but empty" (see [`Params::get_all_optional`]), so there is no
742    /// required/optional distinction available to lose. ⇒ A new type earns an
743    /// exemption only when the wire format denies it the distinction. That it
744    /// merely *has* a natural zero value is not enough.
745    pub fn param_is_required(param: &ParamDef) -> bool {
746        match param.source {
747            // A `{name}` capture is always present when the pattern matched, so
748            // absence never arises. (OpenAPI additionally *mandates*
749            // `required: true` for path parameters.)
750            ParamSource::Path => true,
751            // The body is required by whichever getter reads it.
752            ParamSource::Body => true,
753            // The only case that actually varies.
754            ParamSource::Query => {
755                param.default.is_none() && !matches!(param.ty, ParamType::StringArray)
756            }
757        }
758    }
759
760    /// Main route resolution function. Tries each route in declaration order
761    /// and returns the first whose path pattern *and* verb both match, along
762    /// with its extracted parameters.
763    ///
764    /// "Declaration order" matters: when two patterns can match the same
765    /// action (e.g. `"special"` and `"{id}"`), the one declared first wins —
766    /// list the more specific route earlier.
767    ///
768    /// When no route fully matches, the error distinguishes:
769    /// - `WebError::MethodNotAllowed(methods)`: at least one route's *path*
770    ///   pattern matched but its verb didn't; `methods` is the (sorted, deduped)
771    ///   set of verbs those routes accept, which the caller surfaces as the
772    ///   `Allow` header.
773    /// - `WebError::NotFound`: no route's path pattern matched at all.
774    #[inline]
775    pub fn resolve<'a>(
776        routes: &'a [RouteDef],
777        action: &str,
778        params: &Params,
779        mode: ControllerMode,
780    ) -> Result<(&'a RouteDef, ExtractedParams), WebError> {
781        // Verbs accepted by routes whose *path* matched but whose verb didn't.
782        let mut allowed_methods: Vec<&'static str> = Vec::new();
783
784        for route in routes {
785            // Pattern match first.
786            let path_params = match match_pattern(route.pattern, action) {
787                Some(p) => p,
788                None => continue,
789            };
790
791            // Then verb check. `route.verb` is the (non-empty) set of verbs
792            // this route accepts; an unmarked route carries `DEFAULT_VERBS`.
793            if !route.verb.contains(&params.verb()) {
794                for v in route.verb {
795                    let token = v.as_str();
796                    if !allowed_methods.contains(&token) {
797                        allowed_methods.push(token);
798                    }
799                }
800                continue;
801            }
802
803            // Full match: build extracted params and return.
804            {
805                // Build extracted params
806                let mut extracted = ExtractedParams {
807                    path: path_params,
808                    query: HashMap::new(),
809                    body: params.body.clone(),
810                    raw_body: params.raw_body.clone(),
811                };
812
813                // Extract and validate parameters
814                for param_def in route.params {
815                    match param_def.source {
816                        ParamSource::Path => {
817                            // Path-source params come from `{name}` / `{...name}`
818                            // tokens, which `match_pattern` always captures when
819                            // the pattern matched — so this is already in
820                            // `extracted.path`. (The `#[controller]` macro is what
821                            // guarantees the `ParamDef` ↔ pattern correspondence;
822                            // a hand-built `RouteDef` that violates it would trip
823                            // this in debug builds.)
824                            debug_assert!(
825                                extracted.path.contains_key(param_def.name),
826                                "path parameter `{}` not captured by pattern",
827                                param_def.name
828                            );
829                        }
830                        ParamSource::Query => {
831                            // Requiredness is `param_is_required` and nothing
832                            // else — the OpenAPI generator reports the same
833                            // predicate, so a spec cannot claim a parameter is
834                            // optional that this branch would 400.
835                            if let Some(value) = params.query.get(param_def.name) {
836                                extracted
837                                    .query
838                                    .insert(param_def.name.to_string(), value.clone());
839                            } else if param_is_required(param_def) {
840                                return Err(WebError::BadRequest(format!(
841                                    "Missing required parameter: {}",
842                                    param_def.name
843                                )));
844                            }
845                            // Defaults are applied in the handler extraction phase.
846                        }
847                        ParamSource::Body => {
848                            // JSON body is already handled
849                        }
850                    }
851                }
852
853                // Check for unexpected parameters in strict mode
854                if mode == ControllerMode::Strict {
855                    let expected: Vec<&str> = route
856                        .params
857                        .iter()
858                        .filter(|p| p.source == ParamSource::Query)
859                        .map(|p| p.name)
860                        .collect();
861
862                    if let Some(unexpected) = params.check_unexpected(&expected) {
863                        return Err(WebError::BadRequest(format!(
864                            "Unexpected parameters: {}",
865                            unexpected.join(", ")
866                        )));
867                    }
868                }
869
870                return Ok((route, extracted));
871            }
872        }
873
874        if allowed_methods.is_empty() {
875            Err(WebError::NotFound)
876        } else {
877            allowed_methods.sort_unstable();
878            allowed_methods.dedup();
879            Err(WebError::MethodNotAllowed(allowed_methods))
880        }
881    }
882
883    /// If `segment` is a `{...name}` rest token, returns `name` (non-empty).
884    fn rest_token_name(segment: &str) -> Option<&str> {
885        segment
886            .strip_prefix("{...")
887            .and_then(|s| s.strip_suffix('}'))
888            .filter(|name| !name.is_empty())
889    }
890
891    /// Match one fixed (non-rest) pattern segment against a path segment,
892    /// recording a capture for `{name}` tokens. Returns `false` if a literal
893    /// segment doesn't match.
894    fn match_fixed_segment(
895        pattern_part: &str,
896        path_part: &str,
897        params: &mut HashMap<String, String>,
898    ) -> bool {
899        if let Some(param_name) = pattern_part
900            .strip_prefix('{')
901            .and_then(|s| s.strip_suffix('}'))
902        {
903            params.insert(param_name.to_string(), path_part.to_string());
904            true
905        } else {
906            pattern_part == path_part
907        }
908    }
909
910    /// Split a pattern or action into its segments. A pattern/action is a
911    /// `/`-joined list of **non-empty** segments; empty segments — from
912    /// leading, trailing, or doubled slashes, and notably from the empty
913    /// action `""` (which `str::split` would otherwise yield as `[""]`) —
914    /// are not segments. This is the same normalization `Request` applies to
915    /// the full request path, applied here to the per-controller action and
916    /// the route patterns it's matched against.
917    fn segments(s: &str) -> Vec<&str> {
918        s.split('/').filter(|seg| !seg.is_empty()).collect()
919    }
920
921    /// Match a route pattern against an action path.
922    /// Returns extracted path parameters if matched.
923    ///
924    /// Both sides are viewed as lists of non-empty path segments.
925    /// A literal segment or `{name}` token is **required** — it has no match
926    /// when there's no segment to fill it, so `match_pattern("{id}", "")` is
927    /// `None`. (A controller that wants to serve its collection root declares
928    /// `"" => …`.)
929    ///
930    /// A trailing `{...name}` token is a *rest* parameter: it captures the
931    /// remainder of the path (slashes included) and matches **zero or more**
932    /// segments — `match_pattern("{...path}", "")` is `Some({path: ""})`, and
933    /// `"{folder_id}/{...path}"` matches `"abc"` (`path == ""`) and
934    /// `"abc/x/y"` (`path == "x/y"`) but **not** `""` (the required
935    /// `folder_id` has no segment). The `#[controller]` macro enforces that
936    /// `{...name}` appears at most once and only as the final token; this
937    /// function trusts that and only inspects the last token.
938    #[inline]
939    pub fn match_pattern(pattern: &str, path: &str) -> Option<HashMap<String, String>> {
940        let pattern_parts = segments(pattern);
941        let path_parts = segments(path);
942        let mut params = HashMap::new();
943
944        if let Some(rest_name) = pattern_parts.last().and_then(|s| rest_token_name(s)) {
945            // Everything before the rest token is a fixed prefix that must
946            // match segment-for-segment; the rest token soaks up whatever
947            // is left (possibly nothing).
948            let fixed = &pattern_parts[..pattern_parts.len() - 1];
949            if path_parts.len() < fixed.len() {
950                return None;
951            }
952            for (pattern_part, path_part) in fixed.iter().zip(path_parts.iter()) {
953                if !match_fixed_segment(pattern_part, path_part, &mut params) {
954                    return None;
955                }
956            }
957            params.insert(rest_name.to_string(), path_parts[fixed.len()..].join("/"));
958            return Some(params);
959        }
960
961        if pattern_parts.len() != path_parts.len() {
962            return None;
963        }
964        for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
965            if !match_fixed_segment(pattern_part, path_part, &mut params) {
966                return None;
967            }
968        }
969        Some(params)
970    }
971}
972
973// =========================
974// Controller trait
975// =========================
976
977/// The runtime interface every controller implements. Hand-writing this is
978/// possible but unusual — the `#[controller]` macro generates the
979/// implementation (dispatch table, parameter extraction, the metadata methods)
980/// from a controller's `impl` block and its `routes!` declaration.
981#[async_trait]
982pub trait Controller: Send + Sync {
983    /// Route `action` (the path below this controller's mount) to the matching
984    /// handler and run it, returning its [`Reply`]. Generated by the macro.
985    async fn actus_dispatch(&self, action: &str, params: Params) -> Reply;
986
987    /// The controller's type name, for diagnostics and route auditing.
988    fn __name(&self) -> &'static str;
989
990    /// The controller's declared routes, for introspection (OpenAPI
991    /// generation, route audits). Defaults to empty; the macro overrides it.
992    fn actus_describe_routes(&self) -> Vec<RouteDef> {
993        vec![]
994    }
995
996    /// Per-controller maximum buffered body size, in bytes. Returned by the
997    /// `#[controller(max_body_bytes = …)]` attribute when set; `None` means the
998    /// controller defers to the server-level cap (`Server::with_max_body_bytes`).
999    ///
1000    /// Resolution at request time (see `Server::handle_request_inner`):
1001    /// controller value if `Some`, otherwise the server-wide cap, otherwise
1002    /// `DEFAULT_MAX_BODY_BYTES` (2 MiB).
1003    ///
1004    /// The framework calls this *before* buffering the body — so a 1 KB
1005    /// controller cap rejects a 50 KB request before the bytes are
1006    /// allocated. (A request body big enough to be a memory concern
1007    /// shouldn't get past the framework regardless of where the handler
1008    /// would have rejected it.)
1009    fn actus_max_body_bytes(&self) -> Option<usize> {
1010        None
1011    }
1012
1013    /// Per-controller rate-limit *class* label, as declared by
1014    /// `#[controller(rate_limit = "name")]`. `None` (the default) means the
1015    /// controller declared no class.
1016    ///
1017    /// This is a **label, not a policy**. Actus is policy-agnostic: it ships
1018    /// no limiter algorithm, key function, or store, because the framework
1019    /// can't pick those correctly for someone else (which key — IP / user /
1020    /// API key? which algorithm — token bucket / sliding window? which store
1021    /// — in-memory / Redis?). Those are application decisions, so the limiter
1022    /// itself stays an application `Middleware`.
1023    ///
1024    /// What the framework *does* own is auditability and the response shape.
1025    /// The server stamps this label onto the matched request (surfaced as
1026    /// `Request::rate_limit_class` in `actus-server`), so a reviewer can read
1027    /// each endpoint's rate-limit class straight off the `#[controller(...)]`
1028    /// line, and an application's rate-limit `Middleware` can map class →
1029    /// policy and reject over-limit requests with
1030    /// [`WebError::TooManyRequests`] (429 + `Retry-After`, also framework-owned).
1031    /// Two controllers sharing a class share a limit namespace; what each
1032    /// class *means* is the application's call.
1033    ///
1034    /// Resolution is per-controller, mirroring [`Controller::actus_max_body_bytes`].
1035    /// A per-route override would be an additive future change (the same shape
1036    /// as the per-route body-cap proposal).
1037    fn actus_rate_limit(&self) -> Option<&'static str> {
1038        None
1039    }
1040
1041    /// The controller's declared caller expectation, as set by
1042    /// `#[controller(expects = "…")]`. `None` (the default) means the
1043    /// controller declared nothing.
1044    ///
1045    /// This is a **label, not a policy** — and specifically a **floor**: it
1046    /// names the *least-privileged caller the controller is written to
1047    /// accept* (`"credential"`, `"anonymous"`, `"signature"`, …). It is not a
1048    /// ceiling — individual routes may demand more in their handlers — and it
1049    /// is not authorization: Actus never interprets the value, compares it
1050    /// only for presence/equality in application code, and hands it back
1051    /// untouched. What each label *means*, and what happens to a caller below
1052    /// the floor, is entirely the application's (typically: a startup
1053    /// coverage check over [`Router::mounts`], a declaration-keyed gate in a
1054    /// `prepare` hook or middleware, and a probe test — see the README's
1055    /// "Route families" section).
1056    ///
1057    /// Resolution is per-controller, mirroring
1058    /// [`Controller::actus_rate_limit`]. A controller with routes above its
1059    /// floor declares the floor and enforces the stricter routes in their
1060    /// handlers.
1061    ///
1062    /// [`Router::mounts`]: https://docs.rs/actus-server/latest/actus_server/struct.Router.html#method.mounts
1063    fn actus_expects(&self) -> Option<&'static str> {
1064        None
1065    }
1066
1067    /// The path of the controller's `prepare` hook, as written in
1068    /// `#[controller(prepare = …)]` (e.g. `"Self::auth"`), or `None` when the
1069    /// controller declared no hook.
1070    ///
1071    /// **Presence is the payload.** A route-family coverage check reads this
1072    /// to enforce rules like *"a controller whose floor is `"credential"`
1073    /// must have a hook to refuse anonymous callers with"* — the string
1074    /// itself is a courtesy for route dumps and diagnostics, not an
1075    /// invocation handle.
1076    fn actus_prepare(&self) -> Option<&'static str> {
1077        None
1078    }
1079}
1080
1081// =========================
1082// Route families — the compile-time half (Phase 2)
1083// =========================
1084
1085/// Implemented by the `#[controller]` macro for every controller that declares
1086/// `expects = "…"`; **absent on a controller that declares nothing**, which is
1087/// the whole point. The `families { … }` block in `app_routes!` requires this
1088/// trait of every controller mounted under a listed prefix, so a silently
1089/// undeclared controller under a covered prefix is a **compile error** — the
1090/// `on_unimplemented` message below is the one the developer reads.
1091///
1092/// `EXPECTS` is the same label [`Controller::actus_expects`] returns at runtime,
1093/// lifted to a constant so a family's *accepted set* can be checked at compile
1094/// time too (see [`declares_expectation_in`]).
1095#[diagnostic::on_unimplemented(
1096    message = "`{Self}` is mounted under a route family that requires a declared caller expectation",
1097    label = "this controller declares no `expects` label",
1098    note = "add `expects = \"…\"` to its `#[controller(...)]` attribute, or drop the prefix \
1099            from the `families` block in `app_routes!`"
1100)]
1101pub trait DeclaresExpectation {
1102    /// The declared floor — the value of `#[controller(expects = …)]`.
1103    const EXPECTS: &'static str;
1104}
1105
1106/// A route family's accepted floors, as a type — generated by `app_routes!` for
1107/// each `"prefix" => ["floor", …]` entry in its `families` block, and handed to
1108/// [`declares_expectation_in`] so the acceptance check can run in a `const`.
1109pub trait Family {
1110    /// The floors this family accepts.
1111    const ACCEPTS: &'static [&'static str];
1112}
1113
1114/// `const`-evaluable string equality (`==` on `str` is not `const`).
1115pub const fn str_eq(a: &str, b: &str) -> bool {
1116    let (a, b) = (a.as_bytes(), b.as_bytes());
1117    if a.len() != b.len() {
1118        return false;
1119    }
1120    let mut i = 0;
1121    while i < a.len() {
1122        if a[i] != b[i] {
1123            return false;
1124        }
1125        i += 1;
1126    }
1127    true
1128}
1129
1130/// `const`-evaluable "is `floor` one of `accepts`".
1131pub const fn floor_accepted(floor: &str, accepts: &[&str]) -> bool {
1132    let mut i = 0;
1133    while i < accepts.len() {
1134        if str_eq(floor, accepts[i]) {
1135            return true;
1136        }
1137        i += 1;
1138    }
1139    false
1140}
1141
1142/// Identity pass-through that only compiles for a controller implementing
1143/// [`DeclaresExpectation`]. `app_routes!` wraps a mount's construction
1144/// expression in this when the mount falls under a `families` prefix declared
1145/// without an accepted set — so *presence* of a declaration is checked at
1146/// compile time, and any construction form (`Foo { db }`, `Foo::new(db)`,
1147/// `make_foo()`) works without the macro naming the type.
1148#[inline(always)]
1149pub fn declares_expectation<T: Controller + DeclaresExpectation>(c: T) -> T {
1150    c
1151}
1152
1153/// [`declares_expectation`], plus the *value* check: the controller's
1154/// [`DeclaresExpectation::EXPECTS`] must be one of `F::ACCEPTS`. The check is a
1155/// `const` assertion evaluated when this instantiation is compiled — i.e. when
1156/// the generated `init()` is reachable from something that runs, which in an
1157/// application it always is. A failure reads as an `E0080` naming the
1158/// controller type in its "while instantiating" note. ⚠️ Evaluation needs
1159/// codegen: `cargo check` (and IDE diagnostics built on it) does not run it;
1160/// `cargo build`, `cargo test` and CI do — only the presence bound is a type
1161/// error visible under `check`. (The boot-time coverage check over
1162/// `Router::mounts()` remains the backstop that needs no reachability.)
1163#[inline(always)]
1164pub fn declares_expectation_in<F: Family, T: Controller + DeclaresExpectation>(c: T) -> T {
1165    const {
1166        assert!(
1167            floor_accepted(T::EXPECTS, F::ACCEPTS),
1168            "this controller declares a caller expectation (`expects = …`) that its route \
1169             family does not accept — see the `families` block in `app_routes!`"
1170        )
1171    };
1172    c
1173}
1174
1175// =========================
1176// Host aliases — macro plumbing
1177// =========================
1178
1179/// The host side of a `hosts` entry in `app_routes!` — a pattern (`&str`,
1180/// `String`), or an `Option` of one where `None` means the alias does not
1181/// exist in this deployment — as the `Option<&str>` that
1182/// `RouterBuilder::host_alias` takes. Used by the macro; not meant to be named.
1183#[doc(hidden)]
1184pub trait AliasHost {
1185    /// The pattern, or `None` for an alias that is not configured.
1186    fn alias_host(&self) -> Option<&str>;
1187}
1188
1189impl AliasHost for str {
1190    fn alias_host(&self) -> Option<&str> {
1191        Some(self)
1192    }
1193}
1194
1195impl AliasHost for String {
1196    fn alias_host(&self) -> Option<&str> {
1197        Some(self)
1198    }
1199}
1200
1201impl<T: AliasHost + ?Sized> AliasHost for &T {
1202    fn alias_host(&self) -> Option<&str> {
1203        (**self).alias_host()
1204    }
1205}
1206
1207impl<T: AliasHost> AliasHost for Option<T> {
1208    fn alias_host(&self) -> Option<&str> {
1209        self.as_ref().and_then(|h| h.alias_host())
1210    }
1211}
1212
1213/// A list of `(mount, controller-factory)` pairs — the route-registration
1214/// shape the `app_routes!` macro builds when wiring controllers into a router.
1215pub type Routes = Vec<(
1216    &'static str,
1217    Box<dyn Fn() -> Box<dyn Controller> + Send + Sync>,
1218)>;
1219
1220/// A marker macro to define routes within a `#[controller]` impl block.
1221/// The `#[controller]` procedural macro is responsible for parsing this.
1222#[macro_export]
1223macro_rules! routes {
1224    ($($tokens:tt)*) => {};
1225}
1226
1227#[cfg(test)]
1228mod match_pattern_tests {
1229    use super::routing::match_pattern;
1230
1231    fn cap(pattern: &str, path: &str) -> Option<Vec<(String, String)>> {
1232        match_pattern(pattern, path).map(|m| {
1233            let mut v: Vec<_> = m.into_iter().collect();
1234            v.sort();
1235            v
1236        })
1237    }
1238
1239    fn pair(k: &str, v: &str) -> (String, String) {
1240        (k.to_string(), v.to_string())
1241    }
1242
1243    #[test]
1244    fn fixed_patterns_still_work() {
1245        assert_eq!(cap("", ""), Some(vec![]));
1246        assert_eq!(cap("{id}", "42"), Some(vec![pair("id", "42")]));
1247        assert_eq!(
1248            cap("posts/{id}/comments", "posts/3/comments"),
1249            Some(vec![pair("id", "3")])
1250        );
1251        assert_eq!(cap("a/b", "a/b/c"), None);
1252        assert_eq!(cap("a/b/c", "a/b"), None);
1253        assert_eq!(cap("posts/{id}", "users/3"), None);
1254    }
1255
1256    #[test]
1257    fn required_segments_dont_match_the_empty_action() {
1258        // A `{id}` (or any literal) is required: it has no match when there's
1259        // no segment for it. The empty action is the empty segment list, not
1260        // a one-element list containing `""`.
1261        assert_eq!(cap("{id}", ""), None);
1262        assert_eq!(cap("posts", ""), None);
1263        assert_eq!(cap("{a}/{b}", "x"), None);
1264        // ...but the empty pattern is *defined* as the empty segment list,
1265        // so it matches the empty action (this is how `"" => index` works).
1266        assert_eq!(cap("", ""), Some(vec![]));
1267        assert_eq!(cap("", "x"), None);
1268    }
1269
1270    #[test]
1271    fn rest_param_captures_remainder() {
1272        assert_eq!(
1273            cap("{folder_id}/{...path}", "abc/x/y/z"),
1274            Some(vec![pair("folder_id", "abc"), pair("path", "x/y/z")])
1275        );
1276        // zero trailing segments → rest is empty (folder_id is still present)
1277        assert_eq!(
1278            cap("{folder_id}/{...path}", "abc"),
1279            Some(vec![pair("folder_id", "abc"), pair("path", "")])
1280        );
1281        // ...but the required folder_id has no segment in the empty action
1282        assert_eq!(cap("{folder_id}/{...path}", ""), None);
1283    }
1284
1285    #[test]
1286    fn rest_param_as_sole_token() {
1287        assert_eq!(cap("{...path}", "a/b/c"), Some(vec![pair("path", "a/b/c")]));
1288        assert_eq!(cap("{...path}", "a"), Some(vec![pair("path", "a")]));
1289        // a sole rest token explicitly matches zero segments
1290        assert_eq!(cap("{...path}", ""), Some(vec![pair("path", "")]));
1291    }
1292
1293    #[test]
1294    fn rest_param_after_literal_prefix() {
1295        assert_eq!(
1296            cap("files/{...path}", "files/x/y"),
1297            Some(vec![pair("path", "x/y")])
1298        );
1299        assert_eq!(
1300            cap("files/{...path}", "files"),
1301            Some(vec![pair("path", "")])
1302        );
1303        assert_eq!(cap("files/{...path}", "other/x"), None);
1304        // a literal prefix longer than the path can't match
1305        assert_eq!(cap("a/b/{...path}", "a"), None);
1306    }
1307}
1308
1309#[cfg(test)]
1310mod resolve_tests {
1311    use super::routing::resolve;
1312    use super::*;
1313    use bytes::Bytes;
1314    use std::collections::HashMap;
1315
1316    fn params_with(verb: Verb, query: HashMap<String, Vec<String>>) -> Params {
1317        Params::new(verb, query, None, Bytes::new(), HashMap::new())
1318    }
1319
1320    #[test]
1321    fn headers_are_a_multimap_first_value_wins_for_scalar_access() {
1322        // Two values for one header name (e.g. a proxy chain stamping
1323        // `Forwarded` twice). `header()` returns the first; `header_all()`
1324        // returns both, in receipt order. Absent headers come back as `None`
1325        // / empty slice respectively.
1326        let mut headers = HashMap::new();
1327        headers.insert(
1328            "forwarded".to_string(),
1329            vec!["for=1.2.3.4".to_string(), "for=10.0.0.1".to_string()],
1330        );
1331        headers.insert("x-trace-id".to_string(), vec!["abc-123".to_string()]);
1332        let p = Params::new(Verb::GET, HashMap::new(), None, Bytes::new(), headers);
1333
1334        // Case-insensitive lookup; first value for scalar access.
1335        assert_eq!(p.header("Forwarded"), Some("for=1.2.3.4"));
1336        assert_eq!(p.header("FORWARDED"), Some("for=1.2.3.4"));
1337        assert_eq!(p.header_all("Forwarded"), ["for=1.2.3.4", "for=10.0.0.1"]);
1338
1339        // Single-value headers still work — header_all yields a one-element
1340        // slice, header yields the same value.
1341        assert_eq!(p.header("X-Trace-Id"), Some("abc-123"));
1342        assert_eq!(p.header_all("X-Trace-Id"), ["abc-123"]);
1343
1344        // Absent: None / empty slice.
1345        assert_eq!(p.header("Authorization"), None);
1346        assert!(p.header_all("Authorization").is_empty());
1347    }
1348
1349    #[test]
1350    fn params_query_exposes_the_whole_multimap() {
1351        let mut q = HashMap::new();
1352        q.insert("a".to_string(), vec!["1".to_string(), "2".to_string()]);
1353        q.insert("b".to_string(), vec!["3".to_string()]);
1354        let p = params_with(Verb::GET, q);
1355        assert_eq!(p.query().len(), 2);
1356        assert_eq!(
1357            p.query().get("a").unwrap(),
1358            &["1".to_string(), "2".to_string()]
1359        );
1360        // scalar view still takes the first
1361        assert_eq!(p.get_optional("a"), Some("1"));
1362    }
1363
1364    #[test]
1365    fn verb_mismatch_yields_405_with_sorted_deduped_allow_list() {
1366        // `""` matches the action `""` for both routes; the request verb
1367        // (PUT) matches neither, so we get 405 carrying the union of their
1368        // verbs — sorted and deduped, so the `Allow` header is deterministic.
1369        static ROUTES: &[RouteDef] = &[
1370            RouteDef {
1371                pattern: "",
1372                handler_id: "create",
1373                handler: "create",
1374                verb: &[Verb::POST],
1375                params: &[],
1376                doc: None,
1377            },
1378            RouteDef {
1379                pattern: "",
1380                handler_id: "list",
1381                handler: "list",
1382                verb: &[Verb::GET],
1383                params: &[],
1384                doc: None,
1385            },
1386        ];
1387        match resolve(
1388            ROUTES,
1389            "",
1390            &params_with(Verb::PUT, HashMap::new()),
1391            ControllerMode::Strict,
1392        ) {
1393            Err(WebError::MethodNotAllowed(methods)) => assert_eq!(methods, ["GET", "POST"]),
1394            other => panic!("expected 405, got {other:?}"),
1395        }
1396        // GET matches the second route → Ok.
1397        assert!(
1398            resolve(
1399                ROUTES,
1400                "",
1401                &params_with(Verb::GET, HashMap::new()),
1402                ControllerMode::Strict
1403            )
1404            .is_ok()
1405        );
1406    }
1407
1408    #[test]
1409    fn no_pattern_match_is_404_not_405() {
1410        static ROUTES: &[RouteDef] = &[RouteDef {
1411            pattern: "items",
1412            handler_id: "h",
1413            handler: "h",
1414            verb: &[Verb::GET],
1415            params: &[],
1416            doc: None,
1417        }];
1418        match resolve(
1419            ROUTES,
1420            "other",
1421            &params_with(Verb::DELETE, HashMap::new()),
1422            ControllerMode::Strict,
1423        ) {
1424            Err(WebError::NotFound) => {}
1425            other => panic!("expected 404, got {other:?}"),
1426        }
1427    }
1428
1429    #[test]
1430    fn vec_string_query_param_collects_all_values() {
1431        static ROUTES: &[RouteDef] = &[RouteDef {
1432            pattern: "",
1433            handler_id: "h",
1434            handler: "h",
1435            verb: &[Verb::GET],
1436            params: &[ParamDef {
1437                name: "tags",
1438                ty: ParamType::StringArray,
1439                source: ParamSource::Query,
1440                default: None,
1441            }],
1442            doc: None,
1443        }];
1444
1445        let mut q = HashMap::new();
1446        q.insert(
1447            "tags".to_string(),
1448            vec!["a".to_string(), "b".to_string(), "c".to_string()],
1449        );
1450        let (_, extracted) = resolve(
1451            ROUTES,
1452            "",
1453            &params_with(Verb::GET, q),
1454            ControllerMode::Strict,
1455        )
1456        .expect("route matches");
1457        assert_eq!(extracted.get_string_array("tags").unwrap(), ["a", "b", "c"]);
1458
1459        // A one-element array flows through the same path; a scalar accessor
1460        // takes the first value.
1461        let mut q1 = HashMap::new();
1462        q1.insert("tags".to_string(), vec!["solo".to_string()]);
1463        let (_, e1) = resolve(
1464            ROUTES,
1465            "",
1466            &params_with(Verb::GET, q1),
1467            ControllerMode::Strict,
1468        )
1469        .expect("route matches");
1470        assert_eq!(e1.get_string_array("tags").unwrap(), ["solo"]);
1471        assert_eq!(e1.get_string("tags").unwrap(), "solo");
1472
1473        // Absent is *not* a 400 for a `Vec<String>` param — it's the empty
1474        // list (unlike a missing required scalar).
1475        let (_, e2) = resolve(
1476            ROUTES,
1477            "",
1478            &params_with(Verb::GET, HashMap::new()),
1479            ControllerMode::Strict,
1480        )
1481        .expect("route matches with no query");
1482        assert!(e2.get_string_array("tags").unwrap().is_empty());
1483    }
1484}
1485
1486#[cfg(test)]
1487mod covering_family_tests {
1488    use super::routing::covering_family;
1489
1490    #[test]
1491    fn segment_aligned_longest_prefix_wins() {
1492        let fams = ["api", "api/auth", "public"];
1493        assert_eq!(covering_family("api/things", fams), Some("api"));
1494        assert_eq!(covering_family("api/auth", fams), Some("api/auth"));
1495        assert_eq!(covering_family("api/auth/oauth", fams), Some("api/auth"));
1496        assert_eq!(
1497            covering_family("api/authx", fams),
1498            Some("api"),
1499            "segment-aligned, not a string prefix"
1500        );
1501        assert_eq!(covering_family("apiary", fams), None);
1502        assert_eq!(covering_family("health", fams), None);
1503    }
1504
1505    #[test]
1506    fn star_sugar_and_root() {
1507        assert_eq!(covering_family("api/things", ["api/*"]), Some("api/*"));
1508        assert_eq!(covering_family("anything/at/all", ["*"]), Some("*"));
1509        assert_eq!(covering_family("", [""]), Some(""));
1510        assert_eq!(covering_family("/api/", ["api"]), Some("api"));
1511    }
1512
1513    #[test]
1514    fn a_later_identical_prefix_wins_like_the_macro() {
1515        assert_eq!(covering_family("api/x", ["api", "api/*"]), Some("api/*"));
1516    }
1517}
1518
1519#[cfg(test)]
1520mod bool_default_tests {
1521    //! ⛔ **A declared `bool` default must actually reach the handler.**
1522    //!
1523    //! `bool` is the only parameter type whose *absence* is a usable value, so
1524    //! `get_bool` answers `Ok(false)` for a missing parameter rather than `Err`.
1525    //! Every other type reaches its declared default because the generated code is
1526    //! `get_x(name).unwrap_or(default)` and `get_x` **errors** when the parameter is
1527    //! missing — so for `bool` that `unwrap_or` unwrapped an `Ok(false)` and the
1528    //! default was **dead code**.
1529    //!
1530    //! ⚠️ Measured in a consumer on 2026-09-04: a cancel route declaring
1531    //! `at_period_end: bool = true`, and documenting the reversible at-period-end
1532    //! form as its default, cancelled **immediately** whenever the client omitted the
1533    //! parameter. Destructive direction, opposite of the documented promise, and
1534    //! invisible because the route worked perfectly when the parameter *was* sent.
1535    use super::*;
1536
1537    fn params(query: &[(&str, &str)]) -> ExtractedParams {
1538        let mut q: HashMap<String, Vec<String>> = HashMap::new();
1539        for (k, v) in query {
1540            q.entry((*k).to_string())
1541                .or_default()
1542                .push((*v).to_string());
1543        }
1544        ExtractedParams {
1545            path: HashMap::new(),
1546            query: q,
1547            body: None,
1548            raw_body: Bytes::new(),
1549        }
1550    }
1551
1552    #[test]
1553    fn an_absent_bool_is_distinguishable_from_an_explicit_false() {
1554        let absent = params(&[]);
1555        assert_eq!(
1556            absent.get_bool_optional("flag").unwrap(),
1557            None,
1558            "absent must be None, or a declared default has nothing to fall back from"
1559        );
1560        assert_eq!(
1561            params(&[("flag", "false")])
1562                .get_bool_optional("flag")
1563                .unwrap(),
1564            Some(false),
1565            "an explicit `false` is a VALUE, and must not be confused with absence — \
1566             that conflation is the whole defect"
1567        );
1568        assert_eq!(
1569            params(&[("flag", "true")])
1570                .get_bool_optional("flag")
1571                .unwrap(),
1572            Some(true)
1573        );
1574
1575        // ⭐ The generated shape, verbatim: this is what a `param: bool = true`
1576        // declaration compiles to, and what silently yielded `false` before.
1577        let d = true;
1578        assert!(
1579            absent.get_bool_optional("flag").unwrap().unwrap_or(d),
1580            "⛔ a declared default of `true` must survive an omitted parameter"
1581        );
1582        assert!(
1583            !params(&[("flag", "false")])
1584                .get_bool_optional("flag")
1585                .unwrap()
1586                .unwrap_or(d),
1587            "…and an explicit `false` must still override that default"
1588        );
1589    }
1590
1591    #[test]
1592    fn get_bool_errors_on_absence_like_every_other_scalar_getter() {
1593        // ⛔ This test used to assert the opposite, on the stated grounds that a
1594        // bare `param: bool` means "false unless asked for" and erroring would
1595        // turn `confirm` / `dry_run` into `400`s. That was never true — a bare
1596        // `bool` is required and `resolve` 400s it before extraction (see
1597        // `routing::param_is_required`) — and a red test carrying a confident
1598        // wrong reason is what would stop the next person from fixing it.
1599        assert!(
1600            params(&[]).get_bool("confirm").is_err(),
1601            "absence is an `Err` here, as it is for get_string / get_i64 / the rest"
1602        );
1603        // Present values parse exactly as before.
1604        assert!(params(&[("confirm", "1")]).get_bool("confirm").unwrap());
1605        assert!(!params(&[("confirm", "0")]).get_bool("confirm").unwrap());
1606        assert!(!params(&[("confirm", "false")]).get_bool("confirm").unwrap());
1607        assert!(!params(&[("confirm", "")]).get_bool("confirm").unwrap());
1608    }
1609}
1610
1611#[cfg(test)]
1612mod param_requiredness_tests {
1613    //! ⭐ **The requiredness rule is one predicate, and this is where the
1614    //! decision it encodes is written down.**
1615    //!
1616    //! `routing::resolve` (which 400s) and the OpenAPI generator (which reports
1617    //! `required`) both call [`routing::param_is_required`]. They used to spell
1618    //! the rule out separately and agreed only by diligence; a third site —
1619    //! `ExtractedParams::get_bool`'s `unwrap_or(false)` — quietly encoded the
1620    //! *opposite* rule for `bool`, and that disagreement is what made a declared
1621    //! `bool` default unreachable (fixed 2026-09-04).
1622    use super::routing::param_is_required;
1623    use super::*;
1624
1625    fn q(ty: ParamType, default: Option<ParamDefault>) -> ParamDef {
1626        ParamDef {
1627            name: "p",
1628            ty,
1629            source: ParamSource::Query,
1630            default,
1631        }
1632    }
1633
1634    #[test]
1635    fn requiredness_is_syntactic_and_uniform_across_scalar_types() {
1636        // ⭐ The property worth having: whether a parameter is required is
1637        // readable off `routes!` without knowing the type. `name: T` is
1638        // required; `name: T = x` is not. No per-type table.
1639        for ty in [
1640            ParamType::String,
1641            ParamType::Int,
1642            ParamType::U64,
1643            ParamType::U32,
1644            ParamType::F64,
1645            ParamType::Bool,
1646        ] {
1647            assert!(
1648                param_is_required(&q(ty, None)),
1649                "a bare `{ty:?}` query param is required"
1650            );
1651        }
1652        assert!(!param_is_required(&q(
1653            ParamType::String,
1654            Some(ParamDefault::String("x"))
1655        )));
1656        assert!(!param_is_required(&q(
1657            ParamType::U32,
1658            Some(ParamDefault::U32(1))
1659        )));
1660        assert!(!param_is_required(&q(
1661            ParamType::Bool,
1662            Some(ParamDefault::Bool(true))
1663        )));
1664    }
1665
1666    #[test]
1667    fn a_bare_bool_is_required_and_that_is_the_deliberate_choice() {
1668        // ⛔ Do not "fix" this into `false unless asked for`. Exempting `bool`
1669        // would leave NO way to declare a required one, and it would discard a
1670        // distinction the wire actually carries — absent and `?p=false` are
1671        // different requests (see `ExtractedParams::get_bool_optional`).
1672        // An optional flag is spelled `confirm: bool = false`.
1673        assert!(param_is_required(&q(ParamType::Bool, None)));
1674        assert!(!param_is_required(&q(
1675            ParamType::Bool,
1676            Some(ParamDefault::Bool(false))
1677        )));
1678    }
1679
1680    #[test]
1681    fn string_array_is_the_one_exemption_and_it_is_forced_not_chosen() {
1682        // ⚖️ urlencoding cannot express "present but empty", so there is no
1683        // required/optional distinction available to lose. That is the bar a
1684        // future exemption has to clear — a natural zero value is not enough.
1685        assert!(!param_is_required(&q(ParamType::StringArray, None)));
1686    }
1687
1688    #[test]
1689    fn a_path_capture_is_always_required() {
1690        // The pattern matched, so the capture is present; absence never arises.
1691        assert!(param_is_required(&ParamDef {
1692            name: "id",
1693            ty: ParamType::U64,
1694            source: ParamSource::Path,
1695            default: None,
1696        }));
1697    }
1698}