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