Skip to main content

blazingly_http/
lib.rs

1#![forbid(unsafe_code)]
2
3use blazingly_core::{
4    AppDefinition, BackgroundTask, BackgroundTaskError, BodyStreamError, HttpMethod, HttpUpgrade,
5    InputSource, OperationDescriptor, ResponseHeader, SecuritySchemeDescriptor, StreamingBody,
6};
7use blazingly_executor::{
8    DependencyError, ExecutableApp, ExecutionOutcome, FromInvocation,
9    HttpRequestParts as InvocationRequestParts, InputRejection, InvocationControl, InvocationInput,
10};
11use blazingly_json::{Value, json};
12use blazingly_openapi::{OpenApiAssetResponse, OpenApiConfig, OpenApiService};
13use serde::Serialize;
14use serde::de::DeserializeOwned;
15use std::any::{Any, TypeId};
16use std::borrow::Cow;
17use std::cell::{OnceCell, RefCell};
18use std::collections::{BTreeMap, HashMap};
19use std::fmt;
20use std::future::Future;
21use std::hash::{BuildHasherDefault, Hasher};
22use std::net::{IpAddr, SocketAddr};
23use std::rc::Rc;
24use std::str::Utf8Error;
25
26pub const DEFAULT_MAX_BODY_BYTES: usize = 1024 * 1024;
27
28/// Environment variable that turns server construction into a print-and-exit
29/// introspection run. See [`HttpApp::new`] for the contract.
30pub const EMIT_VARIABLE: &str = "BLAZINGLY_EMIT";
31
32/// A runtime-neutral HTTP request.
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct Request {
35    method: HttpMethod,
36    target: String,
37    headers: BTreeMap<String, String>,
38    body: Vec<u8>,
39    peer_addr: Option<SocketAddr>,
40    scheme: String,
41}
42
43/// Borrowed request access used by in-memory and native HTTP adapters.
44///
45/// Adapters can implement this trait directly over their receive buffer so
46/// dispatch does not require copying the target, headers, or body.
47pub trait HttpRequestView {
48    fn method(&self) -> HttpMethod;
49    fn target(&self) -> &str;
50    fn header_value(&self, name: &str, index: usize) -> Option<&str>;
51    fn body(&self) -> &[u8];
52
53    /// Transfers a pull-based request body to the operation, when supported.
54    fn take_body_stream(&self) -> Option<StreamingBody> {
55        None
56    }
57
58    /// Address of the direct network peer, when known by the adapter.
59    fn peer_addr(&self) -> Option<SocketAddr> {
60        None
61    }
62
63    /// Original transport scheme before trusted proxy normalization.
64    #[allow(clippy::unnecessary_literal_bound)]
65    fn scheme(&self) -> &str {
66        "http"
67    }
68}
69
70/// Mutable, request-local context shared by runtime-neutral HTTP middleware.
71///
72/// The base request remains borrowed. Proxy middleware can replace the
73/// effective client IP, scheme, and host without rewriting the adapter's
74/// receive buffer. Typed extensions are allocated only when inserted.
75pub struct HttpRequestContext<'request> {
76    request: &'request dyn HttpRequestView,
77    client_ip: Option<IpAddr>,
78    scheme: Cow<'request, str>,
79    host: Option<Cow<'request, str>>,
80    extensions: Vec<(TypeId, Box<dyn Any>)>,
81}
82
83impl<'request> HttpRequestContext<'request> {
84    fn new(request: &'request dyn HttpRequestView) -> Self {
85        Self {
86            request,
87            client_ip: request.peer_addr().map(|address| address.ip()),
88            scheme: Cow::Borrowed(request.scheme()),
89            host: None,
90            extensions: Vec::new(),
91        }
92    }
93
94    #[must_use]
95    pub fn request(&self) -> &dyn HttpRequestView {
96        self.request
97    }
98
99    #[must_use]
100    pub fn client_ip(&self) -> Option<IpAddr> {
101        self.client_ip
102    }
103
104    pub fn set_client_ip(&mut self, client_ip: IpAddr) {
105        self.client_ip = Some(client_ip);
106    }
107
108    #[must_use]
109    pub fn scheme(&self) -> &str {
110        &self.scheme
111    }
112
113    pub fn set_scheme(&mut self, scheme: impl Into<String>) {
114        self.scheme = Cow::Owned(scheme.into());
115    }
116
117    #[must_use]
118    pub fn host(&self) -> Option<&str> {
119        self.host
120            .as_deref()
121            .or_else(|| self.request.header_value("host", 0))
122    }
123
124    pub fn set_host(&mut self, host: impl Into<String>) {
125        self.host = Some(Cow::Owned(host.into()));
126    }
127
128    pub fn insert_extension<T: 'static>(&mut self, value: T) {
129        let type_id = TypeId::of::<T>();
130        if let Some((_, existing)) = self
131            .extensions
132            .iter_mut()
133            .find(|(existing, _)| *existing == type_id)
134        {
135            *existing = Box::new(value);
136        } else {
137            self.extensions.push((type_id, Box::new(value)));
138        }
139    }
140
141    #[must_use]
142    pub fn extension<T: 'static>(&self) -> Option<&T> {
143        self.extension_by_id(TypeId::of::<T>())?.downcast_ref()
144    }
145
146    /// Snapshots the normalized client IP, scheme, and host so the dispatch
147    /// path can carry them into the operation request context.
148    #[must_use]
149    pub fn connection_info(&self) -> ConnectionInfo {
150        ConnectionInfo {
151            client_ip: self.client_ip,
152            scheme: self.scheme.as_ref().to_owned(),
153            host: self.host().map(str::to_owned),
154        }
155    }
156
157    fn extension_by_id(&self, type_id: TypeId) -> Option<&dyn Any> {
158        self.extensions
159            .iter()
160            .find(|(existing, _)| *existing == type_id)
161            .map(|(_, value)| value.as_ref())
162    }
163}
164
165/// Normalized transport values readable by a handler extractor.
166///
167/// Dispatch exposes this as a request extension, so `Extension<ConnectionInfo>`
168/// observes the values left by proxy middleware, or the raw adapter values when
169/// no middleware runs.
170#[derive(Clone, Debug, Eq, PartialEq)]
171pub struct ConnectionInfo {
172    client_ip: Option<IpAddr>,
173    scheme: String,
174    host: Option<String>,
175}
176
177impl ConnectionInfo {
178    /// Effective client IP after trusted proxy normalization.
179    #[must_use]
180    pub const fn client_ip(&self) -> Option<IpAddr> {
181        self.client_ip
182    }
183
184    /// Effective request scheme after trusted proxy normalization.
185    #[must_use]
186    pub fn scheme(&self) -> &str {
187        &self.scheme
188    }
189
190    /// Effective host after trusted proxy normalization.
191    #[must_use]
192    pub fn host(&self) -> Option<&str> {
193        self.host.as_deref()
194    }
195
196    fn from_request(request: &(impl HttpRequestView + ?Sized)) -> Self {
197        Self {
198            client_ip: request.peer_addr().map(|address| address.ip()),
199            scheme: request.scheme().to_owned(),
200            host: request.header_value("host", 0).map(str::to_owned),
201        }
202    }
203}
204
205/// Synchronous middleware interception points shared by every HTTP adapter.
206///
207/// Middleware is deliberately runtime-neutral: cryptographic verification,
208/// header policy, compression, and rate limiting do not require Tokio or any
209/// other async runtime.
210pub trait HttpMiddleware {
211    /// Runs before routing. Returning a response short-circuits dispatch.
212    fn on_request(&self, _context: &mut HttpRequestContext<'_>) -> Option<Response> {
213        None
214    }
215
216    /// Runs after routing and before body parsing/handler invocation.
217    fn on_operation(
218        &self,
219        _context: &mut HttpRequestContext<'_>,
220        _operation: &OperationDescriptor,
221        _security_schemes: &[SecuritySchemeDescriptor],
222    ) -> Option<Response> {
223        None
224    }
225
226    /// Runs in reverse registration order for normal and short-circuit
227    /// responses.
228    fn on_response(
229        &self,
230        _context: &HttpRequestContext<'_>,
231        _operation: Option<&OperationDescriptor>,
232        _response: &mut Response,
233    ) {
234    }
235
236    /// Returns whether this layer can verify contract security requirements.
237    ///
238    /// Dispatch fails closed when an operation declares a security scheme and
239    /// no registered layer can verify it. The default is `true` so an unknown
240    /// layer is assumed capable and never turns the guard into a false 500;
241    /// layers that never authenticate should return `false` so a dispatch path
242    /// without a verifier is detected.
243    fn verifies_security(&self) -> bool {
244        true
245    }
246}
247
248type OperationFilter = Rc<dyn Fn(&str) -> bool>;
249
250#[derive(Clone)]
251enum OperationPredicate {
252    Exact(Box<str>),
253    Prefix(Box<str>),
254    Filter(OperationFilter),
255}
256
257impl OperationPredicate {
258    fn matches(&self, operation_id: &str) -> bool {
259        match self {
260            Self::Exact(expected) => operation_id == expected.as_ref(),
261            Self::Prefix(prefix) => operation_id.starts_with(prefix.as_ref()),
262            Self::Filter(filter) => filter(operation_id),
263        }
264    }
265}
266
267/// Selects the requests one registered middleware layer observes.
268///
269/// An empty scope matches every request, which is what
270/// [`HttpApp::with_middleware`] registers. Path prefixes and operation
271/// predicates combine as `AND` between the two categories and `OR` inside one
272/// category.
273///
274/// The selected operation is unknown before routing, so a scope that declares
275/// an operation predicate never matches [`HttpMiddleware::on_request`]; that
276/// layer sees [`HttpMiddleware::on_operation`] and
277/// [`HttpMiddleware::on_response`] instead. A layer whose scope does not match
278/// is also not counted by the security guard, so a scoped verifier cannot
279/// silently authorize an operation outside its subtree.
280#[derive(Clone, Default)]
281pub struct MiddlewareScope {
282    prefixes: Vec<Box<str>>,
283    operations: Vec<OperationPredicate>,
284}
285
286impl MiddlewareScope {
287    /// A scope that constrains nothing.
288    #[must_use]
289    pub const fn all() -> Self {
290        Self {
291            prefixes: Vec::new(),
292            operations: Vec::new(),
293        }
294    }
295
296    /// A scope limited to one path prefix.
297    #[must_use]
298    pub fn prefix(prefix: &str) -> Self {
299        Self::all().with_prefix(prefix)
300    }
301
302    /// A scope limited to one operation id.
303    #[must_use]
304    pub fn operation(operation_id: &str) -> Self {
305        Self::all().with_operation(operation_id)
306    }
307
308    /// Adds an accepted path prefix, matched on segment boundaries.
309    ///
310    /// `/ingest` matches `/ingest` and `/ingest/events`, never `/ingested`.
311    #[must_use]
312    pub fn with_prefix(mut self, prefix: &str) -> Self {
313        self.prefixes.push(normalize_prefix(prefix));
314        self
315    }
316
317    /// Adds one accepted operation id.
318    #[must_use]
319    pub fn with_operation(mut self, operation_id: &str) -> Self {
320        self.operations
321            .push(OperationPredicate::Exact(Box::from(operation_id)));
322        self
323    }
324
325    /// Adds an accepted operation id prefix, for id namespaces such as
326    /// `ingest.`.
327    #[must_use]
328    pub fn with_operation_prefix(mut self, prefix: &str) -> Self {
329        self.operations
330            .push(OperationPredicate::Prefix(Box::from(prefix)));
331        self
332    }
333
334    /// Adds an operation id predicate for a selection the other constraints
335    /// cannot express.
336    #[must_use]
337    pub fn with_operation_filter<Filter>(mut self, filter: Filter) -> Self
338    where
339        Filter: Fn(&str) -> bool + 'static,
340    {
341        self.operations
342            .push(OperationPredicate::Filter(Rc::new(filter)));
343        self
344    }
345
346    /// Returns whether this scope constrains nothing.
347    #[must_use]
348    pub fn is_global(&self) -> bool {
349        self.prefixes.is_empty() && self.operations.is_empty()
350    }
351
352    /// Matches before routing, when only the request path is known.
353    #[must_use]
354    pub fn matches_request(&self, path: &str) -> bool {
355        self.operations.is_empty() && self.matches_path(path)
356    }
357
358    /// Matches after routing, when the selected operation is known.
359    #[must_use]
360    pub fn matches_operation(&self, path: &str, operation_id: &str) -> bool {
361        self.matches_path(path)
362            && (self.operations.is_empty()
363                || self
364                    .operations
365                    .iter()
366                    .any(|predicate| predicate.matches(operation_id)))
367    }
368
369    fn matches_path(&self, path: &str) -> bool {
370        self.prefixes.is_empty()
371            || self
372                .prefixes
373                .iter()
374                .any(|prefix| path_has_prefix(path, prefix))
375    }
376}
377
378impl fmt::Debug for MiddlewareScope {
379    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
380        formatter
381            .debug_struct("MiddlewareScope")
382            .field("prefixes", &self.prefixes)
383            .field("operations", &self.operations.len())
384            .finish()
385    }
386}
387
388fn normalize_prefix(prefix: &str) -> Box<str> {
389    let trimmed = prefix.trim_end_matches('/');
390    if trimmed.is_empty() {
391        Box::from("")
392    } else if trimmed.starts_with('/') {
393        Box::from(trimmed)
394    } else {
395        Box::from(format!("/{trimmed}"))
396    }
397}
398
399fn path_has_prefix(path: &str, prefix: &str) -> bool {
400    let Some(rest) = path.strip_prefix(prefix) else {
401        return false;
402    };
403    rest.is_empty() || rest.starts_with('/')
404}
405
406struct ScopedMiddleware {
407    scope: MiddlewareScope,
408    layer: Rc<dyn HttpMiddleware>,
409}
410
411impl ScopedMiddleware {
412    fn matches(&self, path: &str, operation: Option<&OperationDescriptor>) -> bool {
413        operation.map_or_else(
414            || self.scope.matches_request(path),
415            |operation| {
416                self.scope
417                    .matches_operation(path, operation.contract.id.as_str())
418            },
419        )
420    }
421}
422
423/// Where a failure response produced by dispatch came from.
424#[derive(Clone, Copy, Debug, Eq, PartialEq)]
425pub enum HttpErrorSource {
426    /// No route matched the request method and path.
427    Routing,
428    /// Dispatch rejected the request before the operation ran.
429    Request,
430    /// Argument extraction or model validation rejected the request.
431    Rejection,
432    /// A typed `#[api_error]` variant declared by the operation contract.
433    Domain,
434    /// A server-side failure, including a security scheme no registered layer
435    /// can verify.
436    Internal,
437}
438
439/// The failure an application error handler is asked to rewrite.
440pub struct HttpError<'dispatch> {
441    source: HttpErrorSource,
442    status: u16,
443    code: &'dispatch str,
444    message: &'dispatch str,
445    method: HttpMethod,
446    path: &'dispatch str,
447    operation: Option<&'dispatch OperationDescriptor>,
448}
449
450impl HttpError<'_> {
451    /// Origin of this failure.
452    #[must_use]
453    pub const fn source(&self) -> HttpErrorSource {
454        self.source
455    }
456
457    /// Status of the response dispatch built before any handler ran.
458    #[must_use]
459    pub const fn status(&self) -> u16 {
460        self.status
461    }
462
463    /// Stable error code of the response dispatch built.
464    #[must_use]
465    pub const fn code(&self) -> &str {
466        self.code
467    }
468
469    /// Message of the response dispatch built.
470    #[must_use]
471    pub const fn message(&self) -> &str {
472        self.message
473    }
474
475    /// Request method.
476    #[must_use]
477    pub const fn method(&self) -> HttpMethod {
478        self.method
479    }
480
481    /// Request path without its query string.
482    #[must_use]
483    pub const fn path(&self) -> &str {
484        self.path
485    }
486
487    /// Operation the router selected, when the failure happened after routing.
488    #[must_use]
489    pub const fn operation(&self) -> Option<&OperationDescriptor> {
490        self.operation
491    }
492}
493
494/// Application-level rewriting of failure responses.
495///
496/// Handlers run in registration order after dispatch has produced a failure
497/// response and before middleware `on_response`, so an application can give
498/// every failure one house style without touching the typed errors each
499/// operation declares.
500///
501/// The status of a [`HttpErrorSource::Domain`] failure is restored after the
502/// handlers run: a `#[api_error]` variant publishes its status in the contract,
503/// and this seam must not make that published status a lie. Body, headers, and
504/// the status of every other source are the handler's to change.
505///
506/// A response a middleware layer returns to short-circuit dispatch is that
507/// layer's own and does not reach these handlers; the layer shapes it in
508/// [`HttpMiddleware::on_response`].
509pub trait HttpErrorHandler {
510    /// Rewrites one failure response.
511    fn on_error(&self, error: &HttpError<'_>, response: &mut Response);
512}
513
514/// A request-scoped handle for scheduling work that runs after the response.
515///
516/// A handler injects it with `Extension<BackgroundTasks>` and schedules work
517/// anywhere in its body, instead of restructuring its return type around
518/// [`Background<T>`](blazingly_core::Background).
519///
520/// A scheduled task is attached to the response dispatch produces whatever the
521/// outcome is, including a rejection, a typed domain error, and an aborted
522/// invocation, so an adapter runs it once the body has been written. This is
523/// the one behavioral difference from `Background<T>`, whose tasks ride on a
524/// success value and are therefore discarded when the operation fails.
525#[derive(Clone, Debug, Default)]
526pub struct BackgroundTasks {
527    tasks: Rc<RefCell<Vec<BackgroundTask>>>,
528}
529
530impl BackgroundTasks {
531    #[must_use]
532    pub fn new() -> Self {
533        Self::default()
534    }
535
536    /// Schedules a prepared task.
537    pub fn add_task(&self, task: BackgroundTask) {
538        self.tasks.borrow_mut().push(task);
539    }
540
541    /// Schedules a fallible after-response task.
542    pub fn add<Task, TaskFuture>(&self, task: Task)
543    where
544        Task: FnOnce() -> TaskFuture + 'static,
545        TaskFuture: Future<Output = Result<(), BackgroundTaskError>> + 'static,
546    {
547        self.add_task(BackgroundTask::new(task));
548    }
549
550    /// Schedules an after-response task that cannot fail.
551    pub fn add_infallible<Task, TaskFuture>(&self, task: Task)
552    where
553        Task: FnOnce() -> TaskFuture + 'static,
554        TaskFuture: Future<Output = ()> + 'static,
555    {
556        self.add_task(BackgroundTask::infallible(task));
557    }
558
559    /// Number of tasks scheduled so far.
560    #[must_use]
561    pub fn len(&self) -> usize {
562        self.tasks.borrow().len()
563    }
564
565    /// Returns whether nothing has been scheduled yet.
566    #[must_use]
567    pub fn is_empty(&self) -> bool {
568        self.tasks.borrow().is_empty()
569    }
570
571    /// Takes the scheduled tasks, leaving the handle empty.
572    #[must_use]
573    pub fn take(&self) -> Vec<BackgroundTask> {
574        std::mem::take(&mut self.tasks.borrow_mut())
575    }
576}
577
578impl FromInvocation for BackgroundTasks {
579    fn from_invocation(
580        input: &InvocationInput<'_>,
581        name: &str,
582        _required: bool,
583    ) -> Result<Self, InputRejection> {
584        let InvocationInput::Http(request) = input else {
585            return Err(InputRejection::new(
586                500,
587                "background_tasks_transport_mismatch",
588                "after-response tasks are available only through HTTP",
589            ));
590        };
591        request
592            .extension(TypeId::of::<Self>())
593            .and_then(<dyn Any>::downcast_ref::<Self>)
594            .cloned()
595            .ok_or_else(|| {
596                InputRejection::new(
597                    500,
598                    "background_tasks_unavailable",
599                    format!("this transport installed no after-response tasks for `{name}`"),
600                )
601            })
602    }
603}
604
605impl Request {
606    #[must_use]
607    pub fn new(method: HttpMethod, target: impl Into<String>) -> Self {
608        Self {
609            method,
610            target: target.into(),
611            headers: BTreeMap::new(),
612            body: Vec::new(),
613            peer_addr: None,
614            scheme: "http".to_owned(),
615        }
616    }
617
618    #[must_use]
619    pub fn get(target: impl Into<String>) -> Self {
620        Self::new(HttpMethod::Get, target)
621    }
622
623    #[must_use]
624    pub fn head(target: impl Into<String>) -> Self {
625        Self::new(HttpMethod::Head, target)
626    }
627
628    #[must_use]
629    pub fn post(target: impl Into<String>) -> Self {
630        Self::new(HttpMethod::Post, target)
631    }
632
633    #[must_use]
634    pub fn put(target: impl Into<String>) -> Self {
635        Self::new(HttpMethod::Put, target)
636    }
637
638    #[must_use]
639    pub fn patch(target: impl Into<String>) -> Self {
640        Self::new(HttpMethod::Patch, target)
641    }
642
643    #[must_use]
644    pub fn delete(target: impl Into<String>) -> Self {
645        Self::new(HttpMethod::Delete, target)
646    }
647
648    #[must_use]
649    pub fn options(target: impl Into<String>) -> Self {
650        Self::new(HttpMethod::Options, target)
651    }
652
653    #[must_use]
654    pub fn trace(target: impl Into<String>) -> Self {
655        Self::new(HttpMethod::Trace, target)
656    }
657
658    #[must_use]
659    pub fn connect(target: impl Into<String>) -> Self {
660        Self::new(HttpMethod::Connect, target)
661    }
662
663    #[must_use]
664    pub const fn method(&self) -> HttpMethod {
665        self.method
666    }
667
668    #[must_use]
669    pub fn target(&self) -> &str {
670        &self.target
671    }
672
673    #[must_use]
674    pub fn path(&self) -> &str {
675        self.target
676            .split_once('?')
677            .map_or(self.target.as_str(), |(path, _)| path)
678    }
679
680    #[must_use]
681    pub fn header(mut self, name: impl AsRef<str>, value: impl Into<String>) -> Self {
682        self.headers
683            .insert(normalize_header_name(name.as_ref()), value.into());
684        self
685    }
686
687    #[must_use]
688    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
689        self.body = body.into();
690        self
691    }
692
693    /// Sets the direct peer address for in-memory adapter tests.
694    #[must_use]
695    pub const fn peer_addr(mut self, peer_addr: SocketAddr) -> Self {
696        self.peer_addr = Some(peer_addr);
697        self
698    }
699
700    /// Sets the original transport scheme for in-memory adapter tests.
701    #[must_use]
702    pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
703        self.scheme = scheme.into();
704        self
705    }
706
707    /// Serializes a JSON body and sets its media type.
708    ///
709    /// # Errors
710    ///
711    /// Returns the serialization error if `value` cannot be encoded as JSON.
712    pub fn json(mut self, value: &impl Serialize) -> Result<Self, blazingly_json::Error> {
713        self.body = blazingly_json::to_vec(value)?;
714        self.headers
715            .insert("content-type".to_owned(), "application/json".to_owned());
716        Ok(self)
717    }
718
719    #[must_use]
720    pub fn get_header(&self, name: &str) -> Option<&str> {
721        self.headers
722            .get(name)
723            .or_else(|| {
724                self.headers
725                    .iter()
726                    .find(|(header, _)| header.eq_ignore_ascii_case(name))
727                    .map(|(_, value)| value)
728            })
729            .map(String::as_str)
730    }
731
732    #[must_use]
733    pub fn headers(&self) -> &BTreeMap<String, String> {
734        &self.headers
735    }
736
737    #[must_use]
738    pub fn body_bytes(&self) -> &[u8] {
739        &self.body
740    }
741}
742
743impl HttpRequestView for Request {
744    fn method(&self) -> HttpMethod {
745        self.method()
746    }
747
748    fn target(&self) -> &str {
749        self.target()
750    }
751
752    fn header_value(&self, name: &str, index: usize) -> Option<&str> {
753        self.headers
754            .iter()
755            .filter(|(header, _)| header_name_matches(header, name))
756            .nth(index)
757            .map(|(_, value)| value.as_str())
758    }
759
760    fn body(&self) -> &[u8] {
761        self.body_bytes()
762    }
763
764    fn peer_addr(&self) -> Option<SocketAddr> {
765        self.peer_addr
766    }
767
768    fn scheme(&self) -> &str {
769        &self.scheme
770    }
771}
772
773/// A runtime-neutral HTTP response.
774#[derive(Debug)]
775pub struct Response {
776    status: u16,
777    headers: ResponseHeaders,
778    body: Vec<u8>,
779    stream: Option<StreamingBody>,
780    upgrade: Option<HttpUpgrade>,
781    background: Vec<BackgroundTask>,
782}
783
784impl Response {
785    /// Creates a buffered response with no headers.
786    #[must_use]
787    pub fn from_bytes(status: u16, body: impl Into<Vec<u8>>) -> Self {
788        Self {
789            status,
790            headers: ResponseHeaders::empty(),
791            body: body.into(),
792            stream: None,
793            upgrade: None,
794            background: Vec::new(),
795        }
796    }
797
798    /// Creates an empty buffered response.
799    #[must_use]
800    pub fn empty(status: u16) -> Self {
801        Self::from_bytes(status, Vec::new())
802    }
803
804    /// Creates the canonical response used when an adapter rejects an
805    /// oversized body before dispatch.
806    #[must_use]
807    pub fn payload_too_large(max_body_bytes: usize) -> Self {
808        BodyRejection::PayloadTooLarge { max_body_bytes }.into_response()
809    }
810
811    #[must_use]
812    pub const fn status(&self) -> u16 {
813        self.status
814    }
815
816    /// Replaces the response status.
817    pub const fn set_status(&mut self, status: u16) {
818        self.status = status;
819    }
820
821    #[must_use]
822    pub fn get_header(&self, name: &str) -> Option<&str> {
823        self.headers.get(name)
824    }
825
826    pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
827        self.headers.iter()
828    }
829
830    /// Inserts or replaces a response header. `Set-Cookie` is appended so
831    /// independent cookie mutations are preserved.
832    pub fn set_header(&mut self, name: impl AsRef<str>, value: impl Into<String>) {
833        self.headers.insert(
834            Cow::Owned(normalize_header_name(name.as_ref())),
835            Cow::Owned(value.into()),
836        );
837    }
838
839    /// Removes every response header with the supplied name.
840    pub fn remove_header(&mut self, name: &str) {
841        self.headers.remove(name);
842    }
843
844    #[must_use]
845    pub fn body(&self) -> &[u8] {
846        &self.body
847    }
848
849    /// Replaces a buffered response body. Streaming responses are left
850    /// untouched and return `false`.
851    pub fn replace_body(&mut self, body: impl Into<Vec<u8>>) -> bool {
852        if self.stream.is_some() {
853            return false;
854        }
855        self.body = body.into();
856        true
857    }
858
859    /// Returns whether this response owns a pull-based streaming body.
860    #[must_use]
861    pub const fn is_streaming(&self) -> bool {
862        self.stream.is_some()
863    }
864
865    /// Takes the pull-based streaming body, leaving the response buffered.
866    ///
867    /// A middleware layer that transforms a streamed body, such as a
868    /// chunk-wise content encoder, takes the source stream here and installs
869    /// its wrapper with [`Response::set_body_stream`].
870    #[must_use]
871    pub fn take_body_stream(&mut self) -> Option<StreamingBody> {
872        self.stream.take()
873    }
874
875    /// Installs a pull-based streaming body, discarding any buffered bytes.
876    ///
877    /// The caller owns the framing headers: a streamed body has no known
878    /// length, so `content-length` must be removed rather than left stale.
879    pub fn set_body_stream(&mut self, stream: StreamingBody) {
880        self.body.clear();
881        self.stream = Some(stream);
882    }
883
884    #[must_use]
885    pub const fn is_upgrade(&self) -> bool {
886        self.upgrade.is_some()
887    }
888
889    /// Takes ownership of a validated one-shot protocol upgrade.
890    pub fn take_upgrade(&mut self) -> Option<HttpUpgrade> {
891        self.upgrade.take()
892    }
893
894    /// Takes the after-response tasks so a network adapter can schedule them
895    /// after the body has been written.
896    pub fn take_background_tasks(&mut self) -> Vec<BackgroundTask> {
897        std::mem::take(&mut self.background)
898    }
899
900    /// Runs after-response tasks sequentially and gives every task a chance to
901    /// complete.
902    ///
903    /// # Errors
904    ///
905    /// Returns the first task failure after all tasks have run.
906    pub async fn run_background(&mut self) -> Result<(), BackgroundTaskError> {
907        let mut first_error = None;
908        for task in self.take_background_tasks() {
909            if let Err(error) = task.run().await
910                && first_error.is_none()
911            {
912                first_error = Some(error);
913            }
914        }
915        first_error.map_or(Ok(()), Err)
916    }
917
918    /// Returns the wire body length when it is known before streaming starts.
919    #[must_use]
920    pub fn exact_body_length(&self) -> Option<u64> {
921        self.stream.as_ref().map_or_else(
922            || u64::try_from(self.body.len()).ok(),
923            StreamingBody::exact_length,
924        )
925    }
926
927    /// Pulls one streaming body chunk.
928    ///
929    /// Calling this method is the backpressure demand boundary. Buffered
930    /// responses return `None`; use [`Self::body`] for their bytes.
931    pub async fn next_body_chunk(&mut self) -> Option<Result<Vec<u8>, BodyStreamError>> {
932        match self.stream.as_mut() {
933            Some(stream) => stream.next_chunk().await,
934            None => None,
935        }
936    }
937
938    /// Collects buffered or streaming bytes with an explicit memory bound.
939    ///
940    /// This is intended for tests and clients that deliberately want to turn a
941    /// stream back into one allocation. Network adapters should pull and write
942    /// chunks directly.
943    ///
944    /// # Errors
945    ///
946    /// Returns a producer error or [`CollectBodyError::LimitExceeded`].
947    pub async fn collect_body(mut self, limit: usize) -> Result<Vec<u8>, CollectBodyError> {
948        if self.stream.is_none() {
949            if self.body.len() > limit {
950                return Err(CollectBodyError::LimitExceeded { limit });
951            }
952            return Ok(self.body);
953        }
954
955        let initial_capacity = self
956            .exact_body_length()
957            .and_then(|length| usize::try_from(length).ok())
958            .unwrap_or(0)
959            .min(limit);
960        let mut body = Vec::with_capacity(initial_capacity);
961        while let Some(chunk) = self.next_body_chunk().await {
962            let chunk = chunk.map_err(CollectBodyError::Stream)?;
963            if body.len().saturating_add(chunk.len()) > limit {
964                return Err(CollectBodyError::LimitExceeded { limit });
965            }
966            body.extend_from_slice(&chunk);
967        }
968        Ok(body)
969    }
970
971    /// Decodes the response body as UTF-8.
972    ///
973    /// # Errors
974    ///
975    /// Returns an error when the response body is not valid UTF-8.
976    pub fn text(&self) -> Result<&str, Utf8Error> {
977        std::str::from_utf8(&self.body)
978    }
979
980    /// Deserializes the response body as JSON.
981    ///
982    /// # Errors
983    ///
984    /// Returns an error when the response is not valid JSON for `T`.
985    pub fn json<T: DeserializeOwned>(&self) -> Result<T, blazingly_json::Error> {
986        blazingly_json::from_slice(&self.body)
987    }
988}
989
990/// Failure while deliberately buffering an HTTP response stream.
991#[derive(Clone, Debug, Eq, PartialEq)]
992pub enum CollectBodyError {
993    Stream(BodyStreamError),
994    LimitExceeded { limit: usize },
995}
996
997impl fmt::Display for CollectBodyError {
998    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
999        match self {
1000            Self::Stream(error) => error.fmt(formatter),
1001            Self::LimitExceeded { limit } => {
1002                write!(
1003                    formatter,
1004                    "response body exceeds the {limit}-byte collection limit"
1005                )
1006            }
1007        }
1008    }
1009}
1010
1011impl std::error::Error for CollectBodyError {}
1012
1013/// An owned, runtime-neutral HTTP application compiled from the operation graph.
1014///
1015/// Network adapters own this type so the router is compiled once and request
1016/// dispatch stays independent from the selected socket runtime.
1017pub struct HttpApp {
1018    app: ExecutableApp,
1019    router: Router,
1020    max_body_bytes: usize,
1021    openapi: Option<OpenApiService>,
1022    middleware: Vec<ScopedMiddleware>,
1023    error_handlers: Vec<Rc<dyn HttpErrorHandler>>,
1024    allow_unverified_security_schemes: bool,
1025}
1026
1027impl HttpApp {
1028    /// Compiles the router for an owned application.
1029    ///
1030    /// # Introspection contract
1031    ///
1032    /// When the [`EMIT_VARIABLE`] environment variable (`BLAZINGLY_EMIT`) is
1033    /// set to `openapi` or `routes`, construction prints the `OpenAPI`
1034    /// document or the operation table to stdout and terminates the process
1035    /// with exit code 0 instead of returning, before any socket is served.
1036    /// Every native serving path constructs an `HttpApp`, so
1037    /// `cargo blazingly openapi` and `cargo blazingly routes` run an
1038    /// unmodified application binary as a printer through this seam. The exit
1039    /// inside a constructor is deliberate and is part of the CLI contract.
1040    ///
1041    /// The document is rendered with [`OpenApiConfig::default`]; an
1042    /// application's own [`HttpApp::with_openapi`] configuration is not known
1043    /// at construction time. Any other non-empty value terminates with exit
1044    /// code 2, so a typo never falls through to serving. An unset or empty
1045    /// variable leaves construction unaffected, and [`TestApp`] never
1046    /// consults the variable.
1047    #[must_use]
1048    pub fn new(app: ExecutableApp) -> Self {
1049        emit_and_exit_if_requested(&app);
1050        let router = Router::new(&app);
1051        Self {
1052            app,
1053            router,
1054            max_body_bytes: DEFAULT_MAX_BODY_BYTES,
1055            openapi: None,
1056            middleware: Vec::new(),
1057            error_handlers: Vec::new(),
1058            allow_unverified_security_schemes: false,
1059        }
1060    }
1061
1062    #[must_use]
1063    pub const fn with_max_body_bytes(mut self, max_body_bytes: usize) -> Self {
1064        self.max_body_bytes = max_body_bytes;
1065        self
1066    }
1067
1068    /// Allows operations that declare a security scheme to run without a
1069    /// registered verifier. Defaults to `false`.
1070    ///
1071    /// Passing `true` disables the fail-closed guard: a declared scheme then
1072    /// only documents the contract and this adapter performs no
1073    /// authentication. Enable it for tests, or for an application that
1074    /// deliberately enforces the scheme outside this dispatch path.
1075    #[must_use]
1076    pub const fn with_unverified_security_schemes(mut self, allow: bool) -> Self {
1077        self.allow_unverified_security_schemes = allow;
1078        self
1079    }
1080
1081    /// Runs compiled application startup hooks.
1082    ///
1083    /// # Errors
1084    ///
1085    /// Returns the first startup failure.
1086    pub async fn startup(&self) -> Result<(), DependencyError> {
1087        self.app.startup().await
1088    }
1089
1090    /// Runs compiled application shutdown hooks.
1091    ///
1092    /// # Errors
1093    ///
1094    /// Returns the first cleanup failure after all shutdown hooks have run.
1095    pub async fn shutdown(&self) -> Result<(), DependencyError> {
1096        self.app.shutdown().await
1097    }
1098
1099    /// Mounts precompiled `OpenAPI` JSON and UI assets.
1100    #[must_use]
1101    pub fn with_openapi(mut self, config: OpenApiConfig) -> Self {
1102        self.openapi = Some(OpenApiService::new(self.app.definition(), config));
1103        self
1104    }
1105
1106    /// Registers runtime-neutral HTTP middleware for every request.
1107    #[must_use]
1108    pub fn with_middleware(mut self, middleware: impl HttpMiddleware + 'static) -> Self {
1109        self.middleware.push(ScopedMiddleware {
1110            scope: MiddlewareScope::all(),
1111            layer: Rc::new(middleware),
1112        });
1113        self
1114    }
1115
1116    /// Registers shared middleware state for every request.
1117    #[must_use]
1118    pub fn with_shared_middleware(mut self, middleware: Rc<dyn HttpMiddleware>) -> Self {
1119        self.middleware.push(ScopedMiddleware {
1120            scope: MiddlewareScope::all(),
1121            layer: middleware,
1122        });
1123        self
1124    }
1125
1126    /// Registers runtime-neutral HTTP middleware for one path prefix or
1127    /// operation selection.
1128    #[must_use]
1129    pub fn with_scoped_middleware(
1130        mut self,
1131        scope: MiddlewareScope,
1132        middleware: impl HttpMiddleware + 'static,
1133    ) -> Self {
1134        self.middleware.push(ScopedMiddleware {
1135            scope,
1136            layer: Rc::new(middleware),
1137        });
1138        self
1139    }
1140
1141    /// Registers shared middleware state for one path prefix or operation
1142    /// selection.
1143    #[must_use]
1144    pub fn with_shared_scoped_middleware(
1145        mut self,
1146        scope: MiddlewareScope,
1147        middleware: Rc<dyn HttpMiddleware>,
1148    ) -> Self {
1149        self.middleware.push(ScopedMiddleware {
1150            scope,
1151            layer: middleware,
1152        });
1153        self
1154    }
1155
1156    /// Registers an application-level handler for failure responses.
1157    #[must_use]
1158    pub fn with_error_handler(mut self, handler: impl HttpErrorHandler + 'static) -> Self {
1159        self.error_handlers.push(Rc::new(handler));
1160        self
1161    }
1162
1163    /// Registers shared application-level error handler state.
1164    #[must_use]
1165    pub fn with_shared_error_handler(mut self, handler: Rc<dyn HttpErrorHandler>) -> Self {
1166        self.error_handlers.push(handler);
1167        self
1168    }
1169
1170    pub async fn call(&self, request: Request) -> Response {
1171        self.call_view(&request).await
1172    }
1173
1174    pub async fn call_view(&self, request: &impl HttpRequestView) -> Response {
1175        let dispatcher = self.dispatcher();
1176        dispatcher.dispatch(request, None).await
1177    }
1178
1179    pub async fn call_view_controlled(
1180        &self,
1181        request: &impl HttpRequestView,
1182        control: InvocationControl,
1183    ) -> Response {
1184        let dispatcher = self.dispatcher();
1185        dispatcher.dispatch(request, Some(control)).await
1186    }
1187
1188    fn dispatcher(&self) -> Dispatcher<'_> {
1189        Dispatcher {
1190            app: &self.app,
1191            router: &self.router,
1192            max_body_bytes: self.max_body_bytes,
1193            openapi: self.openapi.as_ref(),
1194            middleware: &self.middleware,
1195            error_handlers: &self.error_handlers,
1196            allow_unverified_security_schemes: self.allow_unverified_security_schemes,
1197        }
1198    }
1199
1200    /// Returns the compiled body source for a recognized request.
1201    ///
1202    /// Native adapters use this before buffering a body so streaming
1203    /// operations can start as soon as the request head is validated.
1204    #[must_use]
1205    pub fn request_body_source(&self, method: HttpMethod, target: &str) -> Option<InputSource> {
1206        let path = target.split_once('?').map_or(target, |(path, _)| path);
1207        self.router
1208            .recognize(method, path)
1209            .ok()
1210            .and_then(|route| route.body_source())
1211    }
1212}
1213
1214/// Prints the requested introspection output and terminates the process when
1215/// [`EMIT_VARIABLE`] is set. Runs on every [`HttpApp::new`] call; a normal
1216/// serving process returns immediately from the unset-variable check.
1217///
1218/// A multicore server constructs one `HttpApp` per worker, so the emission is
1219/// guarded by a process-wide [`std::sync::Once`]: exactly one thread prints,
1220/// every caller exits.
1221fn emit_and_exit_if_requested(app: &ExecutableApp) {
1222    static EMITTED: std::sync::Once = std::sync::Once::new();
1223    let Some(mode) = std::env::var_os(EMIT_VARIABLE) else {
1224        return;
1225    };
1226    if mode.is_empty() {
1227        return;
1228    }
1229    let mut code = 0;
1230    EMITTED.call_once(|| code = emit(app, &mode.to_string_lossy()));
1231    std::process::exit(code);
1232}
1233
1234/// Writes one introspection document to stdout and returns the process exit
1235/// code: 0 on success, 1 on a serialization or write failure, 2 on an
1236/// unrecognized mode.
1237fn emit(app: &ExecutableApp, mode: &str) -> i32 {
1238    use std::io::Write as _;
1239    let output = match mode {
1240        "openapi" => match openapi_document_text(app.definition()) {
1241            Ok(document) => document,
1242            Err(error) => {
1243                eprintln!("error: the OpenAPI document could not be serialized: {error}");
1244                return 1;
1245            }
1246        },
1247        "routes" => routes_table_text(app.definition()),
1248        unknown => {
1249            eprintln!("error: {EMIT_VARIABLE} must be `openapi` or `routes`, not `{unknown}`");
1250            return 2;
1251        }
1252    };
1253    let mut stdout = std::io::stdout().lock();
1254    if stdout.write_all(output.as_bytes()).is_err() || stdout.flush().is_err() {
1255        return 1;
1256    }
1257    0
1258}
1259
1260/// The pretty-printed `OpenAPI` document emitted for `BLAZINGLY_EMIT=openapi`.
1261fn openapi_document_text(definition: &AppDefinition) -> Result<String, blazingly_json::Error> {
1262    let document = blazingly_openapi::to_value(definition);
1263    let mut text = blazingly_json::to_string_pretty(&document)?;
1264    text.push('\n');
1265    Ok(text)
1266}
1267
1268/// The tab-separated operation table emitted for `BLAZINGLY_EMIT=routes`.
1269fn routes_table_text(definition: &AppDefinition) -> String {
1270    use std::fmt::Write as _;
1271    let mut table = String::from("METHOD\tPATH\tOPERATION\tSUMMARY\n");
1272    for operation in definition.operations() {
1273        let _ = writeln!(
1274            table,
1275            "{}\t{}\t{}\t{}",
1276            operation.http.method.as_str(),
1277            operation.http.path,
1278            operation.contract.id.as_str(),
1279            operation.contract.summary
1280        );
1281    }
1282    table
1283}
1284
1285/// An in-memory borrowed HTTP adapter over the shared executable operation graph.
1286pub struct TestApp<'app> {
1287    app: &'app ExecutableApp,
1288    router: Router,
1289    max_body_bytes: usize,
1290    openapi: Option<OpenApiService>,
1291    middleware: Vec<ScopedMiddleware>,
1292    error_handlers: Vec<Rc<dyn HttpErrorHandler>>,
1293    allow_unverified_security_schemes: bool,
1294}
1295
1296impl<'app> TestApp<'app> {
1297    #[must_use]
1298    pub fn new(app: &'app ExecutableApp) -> Self {
1299        Self {
1300            app,
1301            router: Router::new(app),
1302            max_body_bytes: DEFAULT_MAX_BODY_BYTES,
1303            openapi: None,
1304            middleware: Vec::new(),
1305            error_handlers: Vec::new(),
1306            allow_unverified_security_schemes: false,
1307        }
1308    }
1309
1310    #[must_use]
1311    pub const fn with_max_body_bytes(mut self, max_body_bytes: usize) -> Self {
1312        self.max_body_bytes = max_body_bytes;
1313        self
1314    }
1315
1316    /// Allows operations that declare a security scheme to run without a
1317    /// registered verifier. Defaults to `false`.
1318    ///
1319    /// Passing `true` disables the fail-closed guard, so an in-memory test can
1320    /// exercise a secured operation without registering a verifier.
1321    #[must_use]
1322    pub const fn with_unverified_security_schemes(mut self, allow: bool) -> Self {
1323        self.allow_unverified_security_schemes = allow;
1324        self
1325    }
1326
1327    /// Runs application startup hooks for an in-memory test lifespan.
1328    ///
1329    /// # Errors
1330    ///
1331    /// Returns the first startup failure.
1332    pub async fn startup(&self) -> Result<(), DependencyError> {
1333        self.app.startup().await
1334    }
1335
1336    /// Runs application shutdown hooks for an in-memory test lifespan.
1337    ///
1338    /// # Errors
1339    ///
1340    /// Returns the first cleanup failure after all shutdown hooks have run.
1341    pub async fn shutdown(&self) -> Result<(), DependencyError> {
1342        self.app.shutdown().await
1343    }
1344
1345    /// Mounts precompiled `OpenAPI` JSON and UI assets in the in-memory adapter.
1346    #[must_use]
1347    pub fn with_openapi(mut self, config: OpenApiConfig) -> Self {
1348        self.openapi = Some(OpenApiService::new(self.app.definition(), config));
1349        self
1350    }
1351
1352    /// Registers runtime-neutral HTTP middleware for every request.
1353    #[must_use]
1354    pub fn with_middleware(mut self, middleware: impl HttpMiddleware + 'static) -> Self {
1355        self.middleware.push(ScopedMiddleware {
1356            scope: MiddlewareScope::all(),
1357            layer: Rc::new(middleware),
1358        });
1359        self
1360    }
1361
1362    /// Registers shared middleware state for every request.
1363    #[must_use]
1364    pub fn with_shared_middleware(mut self, middleware: Rc<dyn HttpMiddleware>) -> Self {
1365        self.middleware.push(ScopedMiddleware {
1366            scope: MiddlewareScope::all(),
1367            layer: middleware,
1368        });
1369        self
1370    }
1371
1372    /// Registers runtime-neutral HTTP middleware for one path prefix or
1373    /// operation selection.
1374    #[must_use]
1375    pub fn with_scoped_middleware(
1376        mut self,
1377        scope: MiddlewareScope,
1378        middleware: impl HttpMiddleware + 'static,
1379    ) -> Self {
1380        self.middleware.push(ScopedMiddleware {
1381            scope,
1382            layer: Rc::new(middleware),
1383        });
1384        self
1385    }
1386
1387    /// Registers shared middleware state for one path prefix or operation
1388    /// selection.
1389    #[must_use]
1390    pub fn with_shared_scoped_middleware(
1391        mut self,
1392        scope: MiddlewareScope,
1393        middleware: Rc<dyn HttpMiddleware>,
1394    ) -> Self {
1395        self.middleware.push(ScopedMiddleware {
1396            scope,
1397            layer: middleware,
1398        });
1399        self
1400    }
1401
1402    /// Registers an application-level handler for failure responses.
1403    #[must_use]
1404    pub fn with_error_handler(mut self, handler: impl HttpErrorHandler + 'static) -> Self {
1405        self.error_handlers.push(Rc::new(handler));
1406        self
1407    }
1408
1409    /// Registers shared application-level error handler state.
1410    #[must_use]
1411    pub fn with_shared_error_handler(mut self, handler: Rc<dyn HttpErrorHandler>) -> Self {
1412        self.error_handlers.push(handler);
1413        self
1414    }
1415
1416    pub async fn call(&self, request: Request) -> Response {
1417        let dispatcher = self.dispatcher();
1418        dispatcher.dispatch(&request, None).await
1419    }
1420
1421    pub async fn call_controlled(&self, request: Request, control: InvocationControl) -> Response {
1422        let dispatcher = self.dispatcher();
1423        dispatcher.dispatch(&request, Some(control)).await
1424    }
1425
1426    fn dispatcher(&self) -> Dispatcher<'_> {
1427        Dispatcher {
1428            app: self.app,
1429            router: &self.router,
1430            max_body_bytes: self.max_body_bytes,
1431            openapi: self.openapi.as_ref(),
1432            middleware: &self.middleware,
1433            error_handlers: &self.error_handlers,
1434            allow_unverified_security_schemes: self.allow_unverified_security_schemes,
1435        }
1436    }
1437}
1438
1439/// Compiled dispatch inputs shared by the owned and borrowed HTTP adapters.
1440struct Dispatcher<'app> {
1441    app: &'app ExecutableApp,
1442    router: &'app Router,
1443    max_body_bytes: usize,
1444    openapi: Option<&'app OpenApiService>,
1445    middleware: &'app [ScopedMiddleware],
1446    error_handlers: &'app [Rc<dyn HttpErrorHandler>],
1447    allow_unverified_security_schemes: bool,
1448}
1449
1450/// The request coordinates every scoped layer and error handler is selected by.
1451struct DispatchSite<'dispatch> {
1452    method: HttpMethod,
1453    path: &'dispatch str,
1454    operation: Option<&'dispatch OperationDescriptor>,
1455}
1456
1457impl Dispatcher<'_> {
1458    async fn dispatch<RequestView>(
1459        &self,
1460        request: &RequestView,
1461        control: Option<InvocationControl>,
1462    ) -> Response
1463    where
1464        RequestView: HttpRequestView,
1465    {
1466        if self.middleware.is_empty() {
1467            return self.dispatch_unlayered(request, control).await;
1468        }
1469        let middleware = self.middleware;
1470        let target = request.target();
1471        let mut site = DispatchSite {
1472            method: request.method(),
1473            path: target.split_once('?').map_or(target, |(path, _)| path),
1474            operation: None,
1475        };
1476        let mut context = HttpRequestContext::new(request);
1477        for layer in middleware {
1478            if layer.scope.matches_request(site.path)
1479                && let Some(response) = layer.layer.on_request(&mut context)
1480            {
1481                return complete_response(middleware, &context, &site, response);
1482            }
1483        }
1484
1485        if validate_url_encoding(target).is_err() {
1486            let response = self.fail(&site, invalid_url_encoding_failure());
1487            return complete_response(middleware, &context, &site, response);
1488        }
1489        if let Some(response) = self
1490            .openapi
1491            .and_then(|service| service.handle(site.method, site.path))
1492        {
1493            return complete_response(middleware, &context, &site, openapi_response(response));
1494        }
1495        let recognized = match self.router.recognize(site.method, site.path) {
1496            Ok(recognized) => recognized,
1497            Err(error) => {
1498                let response = self.fail(&site, route_miss_failure(&error));
1499                return complete_response(middleware, &context, &site, response);
1500            }
1501        };
1502        let Some(operation) = self.app.operation_at(recognized.operation_index()) else {
1503            let response = self.fail(&site, internal_failure());
1504            return complete_response(middleware, &context, &site, response);
1505        };
1506        let descriptor = operation.descriptor();
1507        site.operation = Some(descriptor);
1508        for layer in middleware {
1509            if layer.matches(site.path, site.operation)
1510                && let Some(response) = layer.layer.on_operation(
1511                    &mut context,
1512                    descriptor,
1513                    self.app.definition().security_schemes(),
1514                )
1515            {
1516                return complete_response(middleware, &context, &site, response);
1517            }
1518        }
1519        if let Some(failure) = self.security_guard(site.path, descriptor) {
1520            let response = self.fail(&site, failure);
1521            return complete_response(middleware, &context, &site, response);
1522        }
1523        if let Some(body_source) = recognized.body_source() {
1524            match validate_body(request, self.max_body_bytes, body_source) {
1525                Ok(()) => {}
1526                Err(rejection) => {
1527                    let response = self.fail(&site, rejection.into_failure());
1528                    return complete_response(middleware, &context, &site, response);
1529                }
1530            }
1531        }
1532        let request_parts = RoutedRequestParts {
1533            request,
1534            route: &recognized,
1535            context: Some(&context),
1536            connection: OnceCell::new(),
1537            background: OnceCell::new(),
1538        };
1539        let outcome = if let Some(control) = control {
1540            operation
1541                .invoke_http_controlled(&request_parts, control)
1542                .await
1543        } else {
1544            operation.invoke_http(&request_parts).await
1545        };
1546        let mut response = match outcome_result(outcome) {
1547            Ok(response) => response,
1548            Err(failure) => self.fail(&site, failure),
1549        };
1550        response.background.extend(request_parts.scheduled_tasks());
1551        complete_response(middleware, &context, &site, response)
1552    }
1553
1554    async fn dispatch_unlayered<RequestView>(
1555        &self,
1556        request: &RequestView,
1557        control: Option<InvocationControl>,
1558    ) -> Response
1559    where
1560        RequestView: HttpRequestView + ?Sized,
1561    {
1562        let target = request.target();
1563        let mut site = DispatchSite {
1564            method: request.method(),
1565            path: target.split_once('?').map_or(target, |(path, _)| path),
1566            operation: None,
1567        };
1568        if validate_url_encoding(target).is_err() {
1569            return self.fail(&site, invalid_url_encoding_failure());
1570        }
1571        if let Some(response) = self
1572            .openapi
1573            .and_then(|service| service.handle(site.method, site.path))
1574        {
1575            return openapi_response(response);
1576        }
1577        let recognized = match self.router.recognize(site.method, site.path) {
1578            Ok(recognized) => recognized,
1579            Err(error) => return self.fail(&site, route_miss_failure(&error)),
1580        };
1581        let Some(operation) = self.app.operation_at(recognized.operation_index()) else {
1582            return self.fail(&site, internal_failure());
1583        };
1584        site.operation = Some(operation.descriptor());
1585        if let Some(failure) = self.security_guard(site.path, operation.descriptor()) {
1586            return self.fail(&site, failure);
1587        }
1588        if let Some(body_source) = recognized.body_source() {
1589            match validate_body(request, self.max_body_bytes, body_source) {
1590                Ok(()) => {}
1591                Err(rejection) => return self.fail(&site, rejection.into_failure()),
1592            }
1593        }
1594        let request_parts = RoutedRequestParts {
1595            request,
1596            route: &recognized,
1597            context: None,
1598            connection: OnceCell::new(),
1599            background: OnceCell::new(),
1600        };
1601        let outcome = if let Some(control) = control {
1602            operation
1603                .invoke_http_controlled(&request_parts, control)
1604                .await
1605        } else {
1606            operation.invoke_http(&request_parts).await
1607        };
1608        let mut response = match outcome_result(outcome) {
1609            Ok(response) => response,
1610            Err(failure) => self.fail(&site, failure),
1611        };
1612        response.background.extend(request_parts.scheduled_tasks());
1613        response
1614    }
1615
1616    /// Builds a failure response and offers it to the application error
1617    /// handlers.
1618    fn fail(&self, site: &DispatchSite<'_>, mut failure: Failure) -> Response {
1619        let mut response = failure.response();
1620        if self.error_handlers.is_empty() {
1621            return response;
1622        }
1623        let error = HttpError {
1624            source: failure.source,
1625            status: failure.status,
1626            code: &failure.code,
1627            message: &failure.message,
1628            method: site.method,
1629            path: site.path,
1630            operation: site.operation,
1631        };
1632        for handler in self.error_handlers {
1633            handler.on_error(&error, &mut response);
1634        }
1635        // A typed `#[api_error]` variant publishes its status in the contract.
1636        if failure.source == HttpErrorSource::Domain {
1637            response.status = failure.status;
1638        }
1639        response
1640    }
1641
1642    /// Fails closed when the matched operation declares a security scheme that
1643    /// no layer on this dispatch path can verify.
1644    ///
1645    /// Both dispatch paths run this before invoking an operation, so an
1646    /// unlayered path never serves a declared scheme unauthenticated. A scoped
1647    /// layer counts only where its scope reaches.
1648    fn security_guard(&self, path: &str, descriptor: &OperationDescriptor) -> Option<Failure> {
1649        if self.allow_unverified_security_schemes || descriptor.contract.security.is_empty() {
1650            return None;
1651        }
1652        if self
1653            .middleware
1654            .iter()
1655            .any(|layer| layer.layer.verifies_security() && layer.matches(path, Some(descriptor)))
1656        {
1657            return None;
1658        }
1659        Some(Failure::new(
1660            HttpErrorSource::Internal,
1661            500,
1662            "security_verifier_missing",
1663            "the operation declares a security scheme with no registered verifier",
1664        ))
1665    }
1666}
1667
1668/// A failure response before it reaches the application error handlers.
1669struct Failure {
1670    source: HttpErrorSource,
1671    status: u16,
1672    code: Cow<'static, str>,
1673    message: Cow<'static, str>,
1674    details: Option<Value>,
1675    headers: Vec<ResponseHeader>,
1676}
1677
1678impl Failure {
1679    fn new(
1680        source: HttpErrorSource,
1681        status: u16,
1682        code: impl Into<Cow<'static, str>>,
1683        message: impl Into<Cow<'static, str>>,
1684    ) -> Self {
1685        Self {
1686            source,
1687            status,
1688            code: code.into(),
1689            message: message.into(),
1690            details: None,
1691            headers: Vec::new(),
1692        }
1693    }
1694
1695    fn with_details(mut self, details: Value) -> Self {
1696        self.details = Some(details);
1697        self
1698    }
1699
1700    fn with_headers(mut self, headers: Vec<ResponseHeader>) -> Self {
1701        self.headers = headers;
1702        self
1703    }
1704
1705    fn response(&mut self) -> Response {
1706        let response = error_response(self.status, &self.code, &self.message, self.details.take());
1707        with_outcome_headers(response, std::mem::take(&mut self.headers))
1708    }
1709
1710    fn into_response(mut self) -> Response {
1711        self.response()
1712    }
1713}
1714
1715fn invalid_url_encoding_failure() -> Failure {
1716    Failure::new(
1717        HttpErrorSource::Request,
1718        400,
1719        "invalid_url_encoding",
1720        "request target contains invalid percent encoding",
1721    )
1722}
1723
1724fn route_miss_failure(error: &RouteError) -> Failure {
1725    match error {
1726        RouteError::MethodNotAllowed { allowed } => {
1727            let allow = allowed
1728                .iter()
1729                .map(|method| method.as_str())
1730                .collect::<Vec<_>>()
1731                .join(", ");
1732            Failure::new(
1733                HttpErrorSource::Routing,
1734                405,
1735                "method_not_allowed",
1736                "HTTP method not allowed",
1737            )
1738            .with_headers(vec![ResponseHeader::new("allow", allow)])
1739        }
1740        RouteError::NotFound => Failure::new(
1741            HttpErrorSource::Routing,
1742            404,
1743            "not_found",
1744            "HTTP route not found",
1745        ),
1746    }
1747}
1748
1749fn complete_response(
1750    middleware: &[ScopedMiddleware],
1751    context: &HttpRequestContext<'_>,
1752    site: &DispatchSite<'_>,
1753    mut response: Response,
1754) -> Response {
1755    for layer in middleware.iter().rev() {
1756        if layer.matches(site.path, site.operation) {
1757            layer
1758                .layer
1759                .on_response(context, site.operation, &mut response);
1760        }
1761    }
1762    response
1763}
1764
1765fn openapi_response(asset: OpenApiAssetResponse) -> Response {
1766    let mut response = Response {
1767        status: asset.status,
1768        headers: ResponseHeaders::empty(),
1769        body: asset.body,
1770        stream: None,
1771        upgrade: None,
1772        background: Vec::new(),
1773    };
1774    for (name, value) in asset.headers {
1775        response = response.with_header(name, value);
1776    }
1777    response
1778}
1779
1780/// Every [`HttpMethod`] variant, in `Ord` order.
1781///
1782/// The `Allow` header of a 405 is rendered by walking this table low bit
1783/// first, so the order is what makes that list sorted; it must stay in sync
1784/// with the `HttpMethod` declaration order and with [`method_index`].
1785const METHODS: [HttpMethod; METHOD_COUNT] = [
1786    HttpMethod::Get,
1787    HttpMethod::Head,
1788    HttpMethod::Post,
1789    HttpMethod::Put,
1790    HttpMethod::Patch,
1791    HttpMethod::Delete,
1792    HttpMethod::Options,
1793    HttpMethod::Trace,
1794    HttpMethod::Connect,
1795];
1796
1797const METHOD_COUNT: usize = 9;
1798
1799const fn method_index(method: HttpMethod) -> usize {
1800    match method {
1801        HttpMethod::Get => 0,
1802        HttpMethod::Head => 1,
1803        HttpMethod::Post => 2,
1804        HttpMethod::Put => 3,
1805        HttpMethod::Patch => 4,
1806        HttpMethod::Delete => 5,
1807        HttpMethod::Options => 6,
1808        HttpMethod::Trace => 7,
1809        HttpMethod::Connect => 8,
1810    }
1811}
1812
1813const PATH_HASH_SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
1814
1815/// `FxHash`-style mixer for route paths and their segments.
1816///
1817/// The router hashes a path on every request; `SipHash` key setup and its
1818/// per-byte round dominated that probe. This consumes eight bytes per
1819/// multiply, and is hand-rolled because the workspace ships no hasher
1820/// dependency. It is not collision-resistant and must never be used on
1821/// attacker-chosen keys that are stored — the tables here are built once from
1822/// the operation graph and only probed at runtime.
1823#[derive(Default)]
1824struct PathHasher {
1825    hash: u64,
1826}
1827
1828impl PathHasher {
1829    fn mix(&mut self, word: u64) {
1830        self.hash = (self.hash.rotate_left(5) ^ word).wrapping_mul(PATH_HASH_SEED);
1831    }
1832}
1833
1834impl Hasher for PathHasher {
1835    fn write(&mut self, bytes: &[u8]) {
1836        let mut word = [0_u8; 8];
1837        let mut index = 0;
1838        while index + 8 <= bytes.len() {
1839            word.copy_from_slice(&bytes[index..index + 8]);
1840            self.mix(u64::from_le_bytes(word));
1841            index += 8;
1842        }
1843        let tail = &bytes[index..];
1844        if !tail.is_empty() {
1845            word = [0; 8];
1846            word[..tail.len()].copy_from_slice(tail);
1847            self.mix(u64::from_le_bytes(word));
1848        }
1849        // Zero padding makes "a" and "a\0" hash alike without this.
1850        self.mix(u64::try_from(bytes.len()).unwrap_or(u64::MAX));
1851    }
1852
1853    fn finish(&self) -> u64 {
1854        self.hash
1855    }
1856}
1857
1858type PathMap<Value> = HashMap<Box<str>, Value, BuildHasherDefault<PathHasher>>;
1859
1860/// One static path segment and the trie node it leads to.
1861struct StaticChild {
1862    head: u8,
1863    node: usize,
1864    segment: Box<str>,
1865}
1866
1867#[derive(Default)]
1868struct RouteNode {
1869    /// Only parameterized routes reach the trie, so these sets are tiny; a
1870    /// scan filtered by the first segment byte beats hashing every segment.
1871    static_children: Vec<StaticChild>,
1872    parameter_child: Option<usize>,
1873    /// Bit [`method_index`] set when `endpoints[method_index]` is bound.
1874    methods: u16,
1875    endpoints: [usize; METHOD_COUNT],
1876}
1877
1878impl RouteNode {
1879    fn static_child(&self, segment: &str) -> Option<usize> {
1880        let head = head_byte(segment);
1881        self.static_children
1882            .iter()
1883            .find(|child| child.head == head && &*child.segment == segment)
1884            .map(|child| child.node)
1885    }
1886}
1887
1888#[derive(Clone)]
1889struct CompiledEndpoint {
1890    operation_index: usize,
1891    parameter_names: Vec<String>,
1892    body_source: Option<InputSource>,
1893}
1894
1895/// What one static path resolves to, for every method at once.
1896///
1897/// Holding the whole method row behind a single key is what lets a miss
1898/// decide 404 versus 405 without a second hash: the probe that failed to find
1899/// the requested method already reported which methods the path does answer.
1900#[derive(Default)]
1901struct StaticSlot {
1902    /// Bit [`method_index`] set when `endpoints[method_index]` is bound.
1903    methods: u16,
1904    endpoints: [usize; METHOD_COUNT],
1905}
1906
1907/// A runtime-neutral router compiled once from the operation graph.
1908pub struct Router {
1909    nodes: Vec<RouteNode>,
1910    /// Endpoints reached through either table; both hold slots into this.
1911    endpoints: Vec<CompiledEndpoint>,
1912    static_routes: PathMap<StaticSlot>,
1913    /// Rejection filter over `(second path byte, path length)` for every
1914    /// static path. A path whose bit is clear cannot be static, so dynamic
1915    /// requests skip the static probe outright.
1916    static_filter: [u64; 2],
1917}
1918
1919impl Router {
1920    #[must_use]
1921    pub fn new(app: &ExecutableApp) -> Self {
1922        let mut router = Self {
1923            nodes: vec![RouteNode::default()],
1924            endpoints: Vec::new(),
1925            static_routes: PathMap::default(),
1926            static_filter: [0; 2],
1927        };
1928        for descriptor in app.definition().operations() {
1929            let Some(operation_index) = app.operation_index(&descriptor.contract.id) else {
1930                continue;
1931            };
1932            router.insert(descriptor, operation_index);
1933        }
1934        router
1935    }
1936
1937    fn insert(&mut self, descriptor: &OperationDescriptor, operation_index: usize) {
1938        let endpoint = CompiledEndpoint {
1939            operation_index,
1940            parameter_names: route_segments(&descriptor.http.path)
1941                .filter_map(path_parameter_name)
1942                .map(str::to_owned)
1943                .collect(),
1944            body_source: body_source(descriptor),
1945        };
1946        let method = method_index(descriptor.http.method);
1947        if endpoint.parameter_names.is_empty() {
1948            let path = descriptor.http.path.as_str();
1949            let (word, bit) = static_filter_slot(path);
1950            self.static_filter[word] |= bit;
1951            let slot = self.endpoints.len();
1952            self.endpoints.push(endpoint);
1953            let entry = self.static_routes.entry(path.into()).or_default();
1954            entry.endpoints[method] = slot;
1955            entry.methods |= 1_u16 << method;
1956            return;
1957        }
1958
1959        let mut node_index = 0;
1960        for segment in route_segments(&descriptor.http.path) {
1961            node_index = if path_parameter_name(segment).is_some() {
1962                if let Some(child) = self.nodes[node_index].parameter_child {
1963                    child
1964                } else {
1965                    let child = self.nodes.len();
1966                    self.nodes.push(RouteNode::default());
1967                    self.nodes[node_index].parameter_child = Some(child);
1968                    child
1969                }
1970            } else if let Some(child) = self.nodes[node_index].static_child(segment) {
1971                child
1972            } else {
1973                let child = self.nodes.len();
1974                self.nodes.push(RouteNode::default());
1975                self.nodes[node_index].static_children.push(StaticChild {
1976                    head: head_byte(segment),
1977                    node: child,
1978                    segment: segment.into(),
1979                });
1980                child
1981            };
1982        }
1983        let slot = self.endpoints.len();
1984        self.endpoints.push(endpoint);
1985        let node = &mut self.nodes[node_index];
1986        node.endpoints[method] = slot;
1987        node.methods |= 1_u16 << method;
1988    }
1989
1990    /// Resolves an HTTP method and path to a direct executable operation slot.
1991    ///
1992    /// # Errors
1993    ///
1994    /// Returns [`RouteError::NotFound`] when no path matches, or
1995    /// [`RouteError::MethodNotAllowed`] when the path exists for other methods.
1996    pub fn recognize<'router, 'path>(
1997        &'router self,
1998        method: HttpMethod,
1999        path: &'path str,
2000    ) -> Result<RouteMatch<'router, 'path>, RouteError> {
2001        let method = method_index(method);
2002        let (word, bit) = static_filter_slot(path);
2003        let slot = if self.static_filter[word] & bit == 0 {
2004            None
2005        } else {
2006            self.static_routes.get(path)
2007        };
2008        if let Some(slot) = slot
2009            && slot.methods & (1_u16 << method) != 0
2010            && let Some(endpoint) = self.endpoints.get(slot.endpoints[method])
2011        {
2012            return Ok(RouteMatch {
2013                endpoint,
2014                captures: CapturedSegments::new(),
2015            });
2016        }
2017
2018        let mut captures = CapturedSegments::new();
2019        let mut other_methods = false;
2020        if let Some(endpoint) = self.walk(
2021            0,
2022            Some(trim_leading_slash(path)),
2023            method,
2024            &mut captures,
2025            &mut other_methods,
2026        ) {
2027            return Ok(RouteMatch { endpoint, captures });
2028        }
2029
2030        let static_methods = slot.map_or(0, |slot| slot.methods);
2031        if static_methods == 0 && !other_methods {
2032            return Err(RouteError::NotFound);
2033        }
2034        Err(RouteError::MethodNotAllowed {
2035            allowed: self.allowed_methods(path, static_methods),
2036        })
2037    }
2038
2039    /// Walks the parameter trie for `method`, recording in `other_methods`
2040    /// whether the path exists under a method that was not asked for. That one
2041    /// bit is what lets a 404 answer without a second walk.
2042    ///
2043    /// `captures` is a stack: a parameter descent pushes and pops on failure,
2044    /// so backtracking never copies it.
2045    fn walk<'router, 'path>(
2046        &'router self,
2047        node_index: usize,
2048        rest: Option<&'path str>,
2049        method: usize,
2050        captures: &mut CapturedSegments<'path>,
2051        other_methods: &mut bool,
2052    ) -> Option<&'router CompiledEndpoint> {
2053        let node = &self.nodes[node_index];
2054        let Some(rest) = rest else {
2055            if node.methods & (1_u16 << method) == 0 {
2056                *other_methods |= node.methods != 0;
2057                return None;
2058            }
2059            return self.endpoints.get(node.endpoints[method]);
2060        };
2061        let (segment, tail) = split_segment(rest);
2062
2063        if let Some(child) = node.static_child(segment)
2064            && let Some(found) = self.walk(child, tail, method, captures, other_methods)
2065        {
2066            return Some(found);
2067        }
2068        let child = node.parameter_child?;
2069        captures.push(segment);
2070        if let Some(found) = self.walk(child, tail, method, captures, other_methods) {
2071            return Some(found);
2072        }
2073        captures.pop();
2074        None
2075    }
2076
2077    /// Renders the sorted `Allow` list of a 405. Only reached when a 405 is
2078    /// actually returned, never on the 404 path.
2079    fn allowed_methods(&self, path: &str, static_methods: u16) -> Vec<HttpMethod> {
2080        let mut methods = static_methods;
2081        self.collect_methods(0, Some(trim_leading_slash(path)), &mut methods);
2082        METHODS
2083            .iter()
2084            .enumerate()
2085            .filter(|(index, _)| methods & (1_u16 << index) != 0)
2086            .map(|(_, method)| *method)
2087            .collect()
2088    }
2089
2090    fn collect_methods(&self, node_index: usize, rest: Option<&str>, methods: &mut u16) {
2091        let node = &self.nodes[node_index];
2092        let Some(rest) = rest else {
2093            *methods |= node.methods;
2094            return;
2095        };
2096        let (segment, tail) = split_segment(rest);
2097        if let Some(child) = node.static_child(segment) {
2098            self.collect_methods(child, tail, methods);
2099        }
2100        if let Some(child) = node.parameter_child {
2101            self.collect_methods(child, tail, methods);
2102        }
2103    }
2104}
2105
2106fn route_segments(path: &str) -> std::str::Split<'_, char> {
2107    trim_leading_slash(path).split('/')
2108}
2109
2110fn trim_leading_slash(path: &str) -> &str {
2111    path.strip_prefix('/').unwrap_or(path)
2112}
2113
2114/// Splits the leading segment off a slash-separated remainder. `None` as the
2115/// tail means the segment just taken was the last one.
2116fn split_segment(rest: &str) -> (&str, Option<&str>) {
2117    match rest.as_bytes().iter().position(|byte| *byte == b'/') {
2118        Some(index) => (&rest[..index], Some(&rest[index + 1..])),
2119        None => (rest, None),
2120    }
2121}
2122
2123/// First byte of a segment, NUL standing in for the empty segment. Only a
2124/// prefilter for the child scan; the full comparison still decides.
2125const fn head_byte(segment: &str) -> u8 {
2126    match segment.as_bytes().first() {
2127        Some(byte) => *byte,
2128        None => 0,
2129    }
2130}
2131
2132/// Word and bit of the static-path rejection filter. Every path starts with
2133/// `/`, so the second byte is what discriminates; the length separates the
2134/// rest. Collisions only cost a wasted probe, but a set bit must never be
2135/// missed, so the same function fills the filter at build time.
2136fn static_filter_slot(path: &str) -> (usize, u64) {
2137    let byte = path.as_bytes().get(1).copied().unwrap_or(0);
2138    let slot = (usize::from(byte).wrapping_mul(3) ^ path.len()) & 127;
2139    (slot >> 6, 1_u64 << (slot & 63))
2140}
2141
2142/// A router miss that distinguishes an unknown path from a wrong method.
2143#[derive(Clone, Debug, Eq, PartialEq)]
2144pub enum RouteError {
2145    MethodNotAllowed { allowed: Vec<HttpMethod> },
2146    NotFound,
2147}
2148
2149/// A compiled route match with its direct operation slot and path captures.
2150pub struct RouteMatch<'router, 'path> {
2151    endpoint: &'router CompiledEndpoint,
2152    captures: CapturedSegments<'path>,
2153}
2154
2155impl RouteMatch<'_, '_> {
2156    #[must_use]
2157    pub const fn operation_index(&self) -> usize {
2158        self.endpoint.operation_index
2159    }
2160
2161    #[must_use]
2162    pub const fn requires_json(&self) -> bool {
2163        matches!(self.endpoint.body_source, Some(InputSource::Json))
2164    }
2165
2166    #[must_use]
2167    pub const fn body_source(&self) -> Option<InputSource> {
2168        self.endpoint.body_source
2169    }
2170
2171    #[must_use]
2172    pub fn path_parameter(&self, name: &str) -> Option<Cow<'_, str>> {
2173        self.endpoint
2174            .parameter_names
2175            .iter()
2176            .position(|parameter| parameter == name)
2177            .and_then(|position| self.captures.get(position))
2178            .and_then(|value| decode_url_component(value, false).ok())
2179    }
2180}
2181
2182const INLINE_PATH_PARAMETERS: usize = 8;
2183
2184/// A capture stack. `Empty` is not just `Inline` with zero length: a static
2185/// match constructs one per request and must not pay for zeroing the inline
2186/// array it will never write.
2187#[derive(Clone)]
2188enum CapturedSegments<'path> {
2189    Empty,
2190    Inline {
2191        values: [Option<&'path str>; INLINE_PATH_PARAMETERS],
2192        len: usize,
2193    },
2194    Heap(Vec<&'path str>),
2195}
2196
2197impl<'path> CapturedSegments<'path> {
2198    const fn new() -> Self {
2199        Self::Empty
2200    }
2201
2202    fn push(&mut self, value: &'path str) {
2203        match self {
2204            Self::Empty => {
2205                let mut values = [None; INLINE_PATH_PARAMETERS];
2206                values[0] = Some(value);
2207                *self = Self::Inline { values, len: 1 };
2208            }
2209            Self::Inline { values, len } if *len < INLINE_PATH_PARAMETERS => {
2210                values[*len] = Some(value);
2211                *len += 1;
2212            }
2213            Self::Inline { values, len } => {
2214                let mut heap = values[..*len]
2215                    .iter()
2216                    .filter_map(|value| *value)
2217                    .collect::<Vec<_>>();
2218                heap.push(value);
2219                *self = Self::Heap(heap);
2220            }
2221            Self::Heap(values) => values.push(value),
2222        }
2223    }
2224
2225    /// Undoes the most recent [`Self::push`], so a failed parameter descent
2226    /// can backtrack without the caller having cloned the stack.
2227    fn pop(&mut self) {
2228        match self {
2229            Self::Empty => {}
2230            Self::Inline { len, .. } => *len = len.saturating_sub(1),
2231            Self::Heap(values) => {
2232                values.pop();
2233            }
2234        }
2235    }
2236
2237    fn get(&self, index: usize) -> Option<&'path str> {
2238        match self {
2239            Self::Inline { values, len } if index < *len => values[index],
2240            Self::Empty | Self::Inline { .. } => None,
2241            Self::Heap(values) => values.get(index).copied(),
2242        }
2243    }
2244}
2245
2246struct RoutedRequestParts<'request, 'router, 'path, 'context, RequestView: ?Sized> {
2247    request: &'request RequestView,
2248    route: &'request RouteMatch<'router, 'path>,
2249    context: Option<&'context HttpRequestContext<'request>>,
2250    connection: OnceCell<ConnectionInfo>,
2251    background: OnceCell<BackgroundTasks>,
2252}
2253
2254impl<RequestView> RoutedRequestParts<'_, '_, '_, '_, RequestView>
2255where
2256    RequestView: HttpRequestView + ?Sized,
2257{
2258    /// Materializes the normalized transport values on first extractor use.
2259    fn connection_info(&self) -> &ConnectionInfo {
2260        self.connection.get_or_init(|| {
2261            self.context.map_or_else(
2262                || ConnectionInfo::from_request(self.request),
2263                HttpRequestContext::connection_info,
2264            )
2265        })
2266    }
2267
2268    /// Materializes the after-response task handle on first extractor use, so
2269    /// an operation that never injects one allocates nothing.
2270    fn background_tasks(&self) -> &BackgroundTasks {
2271        self.background.get_or_init(BackgroundTasks::new)
2272    }
2273
2274    /// Takes what the handler scheduled through the injected handle.
2275    fn scheduled_tasks(&self) -> Vec<BackgroundTask> {
2276        self.background
2277            .get()
2278            .map_or_else(Vec::new, BackgroundTasks::take)
2279    }
2280}
2281
2282impl<RequestView> InvocationRequestParts for RoutedRequestParts<'_, '_, '_, '_, RequestView>
2283where
2284    RequestView: HttpRequestView + ?Sized,
2285{
2286    fn value(&self, source: InputSource, name: &str, index: usize) -> Option<Cow<'_, str>> {
2287        match source {
2288            InputSource::Path if index == 0 => self.route.path_parameter(name),
2289            InputSource::Query => query_value(self.request.target(), name, index),
2290            InputSource::Header => self.request.header_value(name, index).map(Cow::Borrowed),
2291            InputSource::Cookie => cookie_value(self.request, name, index),
2292            InputSource::Form => form_value(self.request.body(), name, index),
2293            InputSource::Path
2294            | InputSource::Json
2295            | InputSource::Multipart
2296            | InputSource::File
2297            | InputSource::Stream => None,
2298        }
2299    }
2300
2301    fn body(&self) -> &[u8] {
2302        self.request.body()
2303    }
2304
2305    fn take_body_stream(&self) -> Option<StreamingBody> {
2306        self.request.take_body_stream()
2307    }
2308
2309    fn extension(&self, type_id: TypeId) -> Option<&dyn Any> {
2310        if let Some(value) = self
2311            .context
2312            .and_then(|context| context.extension_by_id(type_id))
2313        {
2314            return Some(value);
2315        }
2316        if type_id == TypeId::of::<ConnectionInfo>() {
2317            return Some(self.connection_info());
2318        }
2319        if type_id == TypeId::of::<BackgroundTasks>() {
2320            return Some(self.background_tasks());
2321        }
2322        None
2323    }
2324
2325    fn method(&self) -> Option<HttpMethod> {
2326        Some(self.request.method())
2327    }
2328
2329    fn path(&self) -> Option<&str> {
2330        let target = self.request.target();
2331        Some(target.split('?').next().unwrap_or(target))
2332    }
2333
2334    fn peer_addr(&self) -> Option<SocketAddr> {
2335        self.request.peer_addr()
2336    }
2337
2338    fn scheme(&self) -> Option<&str> {
2339        Some(self.connection_info().scheme())
2340    }
2341
2342    fn host(&self) -> Option<&str> {
2343        self.connection_info().host()
2344    }
2345}
2346
2347fn body_source(descriptor: &OperationDescriptor) -> Option<InputSource> {
2348    descriptor
2349        .contract
2350        .inputs
2351        .iter()
2352        .map(|input| input.source)
2353        .find(|source| {
2354            matches!(
2355                source,
2356                InputSource::Json
2357                    | InputSource::Form
2358                    | InputSource::Multipart
2359                    | InputSource::File
2360                    | InputSource::Stream
2361            )
2362        })
2363}
2364
2365fn cookie_value<'request>(
2366    request: &'request (impl HttpRequestView + ?Sized),
2367    name: &str,
2368    index: usize,
2369) -> Option<Cow<'request, str>> {
2370    let mut header_index = 0;
2371    let mut found = 0;
2372    while let Some(header) = request.header_value("cookie", header_index) {
2373        for cookie in header.split(';') {
2374            let (cookie_name, value) = cookie.trim().split_once('=').unwrap_or((cookie.trim(), ""));
2375            if cookie_name == name {
2376                if found == index {
2377                    return Some(Cow::Borrowed(value));
2378                }
2379                found += 1;
2380            }
2381        }
2382        header_index += 1;
2383    }
2384    None
2385}
2386
2387fn form_value<'body>(body: &'body [u8], name: &str, index: usize) -> Option<Cow<'body, str>> {
2388    let body = std::str::from_utf8(body).ok()?;
2389    let mut found = 0;
2390    for pair in body.split('&').filter(|pair| !pair.is_empty()) {
2391        let (raw_name, raw_value) = pair.split_once('=').unwrap_or((pair, ""));
2392        let decoded_name = decode_url_component(raw_name, true).ok()?;
2393        if decoded_name == name {
2394            if found == index {
2395                return decode_url_component(raw_value, true).ok();
2396            }
2397            found += 1;
2398        }
2399    }
2400    None
2401}
2402
2403fn path_parameter_name(segment: &str) -> Option<&str> {
2404    segment
2405        .strip_prefix('{')
2406        .and_then(|segment| segment.strip_suffix('}'))
2407        .filter(|name| !name.is_empty())
2408}
2409
2410fn query_value<'target>(
2411    target: &'target str,
2412    name: &str,
2413    index: usize,
2414) -> Option<Cow<'target, str>> {
2415    let (_, query) = target.split_once('?')?;
2416    let mut found = 0;
2417    for pair in query.split('&').filter(|pair| !pair.is_empty()) {
2418        let (raw_name, raw_value) = pair.split_once('=').unwrap_or((pair, ""));
2419        let decoded_name = decode_url_component(raw_name, true).ok()?;
2420        if decoded_name == name {
2421            if found == index {
2422                return decode_url_component(raw_value, true).ok();
2423            }
2424            found += 1;
2425        }
2426    }
2427    None
2428}
2429
2430fn header_name_matches(header: &str, argument: &str) -> bool {
2431    header.bytes().eq(argument.bytes().map(|byte| {
2432        if byte == b'_' {
2433            b'-'
2434        } else {
2435            byte.to_ascii_lowercase()
2436        }
2437    }))
2438}
2439
2440fn validate_url_encoding(target: &str) -> Result<(), ()> {
2441    let path = target.split_once('?').map_or(target, |(path, _)| path);
2442    decode_url_component(path, false)?;
2443    if let Some((_, query)) = target.split_once('?') {
2444        for pair in query.split('&') {
2445            let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
2446            decode_url_component(name, true)?;
2447            decode_url_component(value, true)?;
2448        }
2449    }
2450    Ok(())
2451}
2452
2453fn decode_url_component(value: &str, plus_as_space: bool) -> Result<Cow<'_, str>, ()> {
2454    if !value.as_bytes().contains(&b'%') && (!plus_as_space || !value.as_bytes().contains(&b'+')) {
2455        return Ok(Cow::Borrowed(value));
2456    }
2457    let bytes = value.as_bytes();
2458    let mut decoded = Vec::with_capacity(bytes.len());
2459    let mut index = 0;
2460    while index < bytes.len() {
2461        match bytes[index] {
2462            b'%' => {
2463                let high = bytes
2464                    .get(index + 1)
2465                    .copied()
2466                    .and_then(hex_value)
2467                    .ok_or(())?;
2468                let low = bytes
2469                    .get(index + 2)
2470                    .copied()
2471                    .and_then(hex_value)
2472                    .ok_or(())?;
2473                decoded.push((high << 4) | low);
2474                index += 3;
2475            }
2476            b'+' if plus_as_space => {
2477                decoded.push(b' ');
2478                index += 1;
2479            }
2480            byte => {
2481                decoded.push(byte);
2482                index += 1;
2483            }
2484        }
2485    }
2486    String::from_utf8(decoded).map(Cow::Owned).map_err(|_| ())
2487}
2488
2489const fn hex_value(byte: u8) -> Option<u8> {
2490    match byte {
2491        b'0'..=b'9' => Some(byte - b'0'),
2492        b'a'..=b'f' => Some(byte - b'a' + 10),
2493        b'A'..=b'F' => Some(byte - b'A' + 10),
2494        _ => None,
2495    }
2496}
2497
2498enum BodyRejection {
2499    PayloadTooLarge { max_body_bytes: usize },
2500    UnsupportedMediaType,
2501}
2502
2503impl BodyRejection {
2504    fn into_failure(self) -> Failure {
2505        match self {
2506            Self::PayloadTooLarge { max_body_bytes } => Failure::new(
2507                HttpErrorSource::Request,
2508                413,
2509                "payload_too_large",
2510                "request body exceeds the configured limit",
2511            )
2512            .with_details(json!({ "maxBytes": max_body_bytes })),
2513            Self::UnsupportedMediaType => Failure::new(
2514                HttpErrorSource::Request,
2515                415,
2516                "unsupported_media_type",
2517                "request body media type does not match the operation input",
2518            ),
2519        }
2520    }
2521
2522    fn into_response(self) -> Response {
2523        self.into_failure().into_response()
2524    }
2525}
2526
2527fn validate_body(
2528    request: &(impl HttpRequestView + ?Sized),
2529    max_body_bytes: usize,
2530    source: InputSource,
2531) -> Result<(), BodyRejection> {
2532    if request.body().len() > max_body_bytes {
2533        return Err(BodyRejection::PayloadTooLarge { max_body_bytes });
2534    }
2535    if source == InputSource::Stream {
2536        return Ok(());
2537    }
2538
2539    let valid_media_type = request
2540        .header_value("content-type", 0)
2541        .is_some_and(|content_type| match source {
2542            InputSource::Json => is_json_media_type(content_type),
2543            InputSource::Form => media_type_is(content_type, "application/x-www-form-urlencoded"),
2544            InputSource::Multipart | InputSource::File => {
2545                media_type_is(content_type, "multipart/form-data")
2546            }
2547            InputSource::Stream => unreachable!("streaming bodies do not require a media type"),
2548            InputSource::Path | InputSource::Query | InputSource::Header | InputSource::Cookie => {
2549                true
2550            }
2551        });
2552    if !valid_media_type {
2553        return Err(BodyRejection::UnsupportedMediaType);
2554    }
2555
2556    Ok(())
2557}
2558
2559fn media_type_is(value: &str, expected: &str) -> bool {
2560    value
2561        .split(';')
2562        .next()
2563        .is_some_and(|media_type| media_type.trim().eq_ignore_ascii_case(expected))
2564}
2565
2566fn outcome_result(outcome: ExecutionOutcome) -> Result<Response, Failure> {
2567    match outcome {
2568        ExecutionOutcome::Success {
2569            status,
2570            headers,
2571            body,
2572            background,
2573        } => {
2574            let response = match body {
2575                Some(body) => Response {
2576                    status,
2577                    headers: json_headers(),
2578                    body,
2579                    stream: None,
2580                    upgrade: None,
2581                    background: Vec::new(),
2582                },
2583                None => Response {
2584                    status,
2585                    headers: ResponseHeaders::empty(),
2586                    body: Vec::new(),
2587                    stream: None,
2588                    upgrade: None,
2589                    background: Vec::new(),
2590                },
2591            };
2592            let mut response = with_outcome_headers(response, headers);
2593            response.background = background;
2594            Ok(response)
2595        }
2596        ExecutionOutcome::StreamingSuccess {
2597            status,
2598            headers,
2599            body,
2600            background,
2601        } => Ok(with_outcome_headers(
2602            Response {
2603                status,
2604                headers: ResponseHeaders::empty(),
2605                body: Vec::new(),
2606                stream: Some(body),
2607                upgrade: None,
2608                background,
2609            },
2610            headers,
2611        )),
2612        ExecutionOutcome::Upgrade {
2613            upgrade,
2614            background,
2615        } => {
2616            let headers = upgrade.headers().to_vec();
2617            Ok(with_outcome_headers(
2618                Response {
2619                    status: 101,
2620                    headers: ResponseHeaders::empty(),
2621                    body: Vec::new(),
2622                    stream: None,
2623                    upgrade: Some(upgrade),
2624                    background,
2625                },
2626                headers,
2627            ))
2628        }
2629        ExecutionOutcome::Rejected {
2630            status,
2631            code,
2632            message,
2633            details,
2634        } => Err(Failure {
2635            source: HttpErrorSource::Rejection,
2636            status,
2637            code: Cow::Owned(code),
2638            message: Cow::Owned(message),
2639            details,
2640            headers: Vec::new(),
2641        }),
2642        ExecutionOutcome::DomainError(error) => {
2643            let details = match error.details {
2644                Some(details) => match blazingly_json::from_slice(&details) {
2645                    Ok(details) => Some(details),
2646                    Err(_) => return Err(internal_failure()),
2647                },
2648                None => None,
2649            };
2650            Err(Failure {
2651                source: HttpErrorSource::Domain,
2652                status: error.status,
2653                code: Cow::Owned(error.code),
2654                message: Cow::Owned(error.message),
2655                details,
2656                headers: error.headers,
2657            })
2658        }
2659        ExecutionOutcome::InternalError { .. } => Err(internal_failure()),
2660    }
2661}
2662
2663fn with_outcome_headers(mut response: Response, headers: Vec<ResponseHeader>) -> Response {
2664    for header in headers {
2665        response = response.with_header(header.name, header.value);
2666    }
2667    response
2668}
2669
2670fn error_response(status: u16, code: &str, message: &str, details: Option<Value>) -> Response {
2671    let mut error = json!({
2672        "error": {
2673            "code": code,
2674            "message": message,
2675        }
2676    });
2677    if let Some(details) = details {
2678        error["error"]["details"] = details;
2679    }
2680    json_response(status, &error)
2681}
2682
2683fn internal_failure() -> Failure {
2684    Failure::new(
2685        HttpErrorSource::Internal,
2686        500,
2687        "internal_error",
2688        "the operation could not be completed",
2689    )
2690}
2691
2692fn json_response(status: u16, value: &Value) -> Response {
2693    let Ok(body) = blazingly_json::to_vec(value) else {
2694        return Response {
2695            status: 500,
2696            headers: json_headers(),
2697            body: br#"{"error":{"code":"internal_error","message":"the operation could not be completed"}}"#
2698                .to_vec(),
2699            stream: None,
2700            upgrade: None,
2701            background: Vec::new(),
2702        };
2703    };
2704
2705    Response {
2706        status,
2707        headers: json_headers(),
2708        body,
2709        stream: None,
2710        upgrade: None,
2711        background: Vec::new(),
2712    }
2713}
2714
2715fn json_headers() -> ResponseHeaders {
2716    ResponseHeaders::one("content-type", "application/json")
2717}
2718
2719fn is_json_media_type(value: &str) -> bool {
2720    value
2721        .split(';')
2722        .next()
2723        .is_some_and(|media_type| media_type.trim().eq_ignore_ascii_case("application/json"))
2724}
2725
2726fn normalize_header_name(name: &str) -> String {
2727    name.to_ascii_lowercase()
2728}
2729
2730impl Response {
2731    #[must_use]
2732    pub fn with_header(mut self, name: impl AsRef<str>, value: impl Into<String>) -> Self {
2733        self.set_header(name, value);
2734        self
2735    }
2736}
2737
2738const INLINE_RESPONSE_HEADERS: usize = 4;
2739type OwnedHeader = (Cow<'static, str>, Cow<'static, str>);
2740
2741#[derive(Clone, Debug, Eq, PartialEq)]
2742struct ResponseHeaders {
2743    inline: [Option<OwnedHeader>; INLINE_RESPONSE_HEADERS],
2744    overflow: Vec<OwnedHeader>,
2745}
2746
2747impl ResponseHeaders {
2748    fn empty() -> Self {
2749        Self {
2750            inline: std::array::from_fn(|_| None),
2751            overflow: Vec::new(),
2752        }
2753    }
2754
2755    fn one(name: &'static str, value: &'static str) -> Self {
2756        let mut headers = Self::empty();
2757        headers.inline[0] = Some((Cow::Borrowed(name), Cow::Borrowed(value)));
2758        headers
2759    }
2760
2761    fn insert(&mut self, name: Cow<'static, str>, value: Cow<'static, str>) {
2762        // Set-Cookie is not a comma-joinable field: one response may carry
2763        // several independent cookie mutations.
2764        if !name.eq_ignore_ascii_case("set-cookie") {
2765            if let Some((_, existing)) = self
2766                .inline
2767                .iter_mut()
2768                .filter_map(Option::as_mut)
2769                .chain(self.overflow.iter_mut())
2770                .find(|(existing, _)| existing.eq_ignore_ascii_case(&name))
2771            {
2772                *existing = value;
2773                return;
2774            }
2775        }
2776        if let Some(slot) = self.inline.iter_mut().find(|slot| slot.is_none()) {
2777            *slot = Some((name, value));
2778        } else {
2779            self.overflow.push((name, value));
2780        }
2781    }
2782
2783    fn remove(&mut self, name: &str) {
2784        for slot in &mut self.inline {
2785            if slot
2786                .as_ref()
2787                .is_some_and(|(existing, _)| existing.eq_ignore_ascii_case(name))
2788            {
2789                *slot = None;
2790            }
2791        }
2792        self.overflow
2793            .retain(|(existing, _)| !existing.eq_ignore_ascii_case(name));
2794    }
2795
2796    fn get(&self, name: &str) -> Option<&str> {
2797        self.inline
2798            .iter()
2799            .filter_map(Option::as_ref)
2800            .chain(&self.overflow)
2801            .find(|(header, _)| header.eq_ignore_ascii_case(name))
2802            .map(|(_, value)| value.as_ref())
2803    }
2804
2805    fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
2806        self.inline
2807            .iter()
2808            .filter_map(Option::as_ref)
2809            .chain(&self.overflow)
2810            .map(|(name, value)| (name.as_ref(), value.as_ref()))
2811    }
2812}
2813
2814#[cfg(test)]
2815mod tests {
2816    use super::{
2817        BackgroundTasks, ConnectionInfo, HttpApp, HttpError, HttpErrorHandler, HttpErrorSource,
2818        HttpMiddleware, HttpRequestContext, MiddlewareScope, Request, Response, RouteError, Router,
2819        TestApp,
2820    };
2821    use blazingly_core::{
2822        HttpMethod, InputDescriptor, InputSource, OperationDescriptor, OperationFailure,
2823        PreparedJson, ResponseDescriptor, SecurityLocation, SecurityRequirement,
2824        SecuritySchemeDescriptor, SecuritySchemeKind, TypeDescriptor,
2825    };
2826    use blazingly_executor::{
2827        ExecutableApp, ExecutableOperation, ExecutionOutcome, Extension, FromInvocation,
2828        InputRejection, InvocationControl, InvocationInput, OperationFuture, OperationOutput,
2829    };
2830    use blazingly_json::{Value, json};
2831    use futures_lite::future;
2832    use std::cell::{Cell, RefCell};
2833    use std::future::Future;
2834    use std::net::{IpAddr, Ipv4Addr, SocketAddrV4};
2835    use std::pin::Pin;
2836    use std::rc::Rc;
2837    use std::task::{Context, Poll};
2838
2839    struct PassthroughLayer;
2840
2841    impl HttpMiddleware for PassthroughLayer {}
2842
2843    struct AuditLayer;
2844
2845    impl HttpMiddleware for AuditLayer {
2846        fn verifies_security(&self) -> bool {
2847            false
2848        }
2849    }
2850
2851    struct NormalizingProxy;
2852
2853    impl HttpMiddleware for NormalizingProxy {
2854        fn on_request(&self, context: &mut HttpRequestContext<'_>) -> Option<Response> {
2855            context.set_client_ip(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 7)));
2856            context.set_scheme("https");
2857            context.set_host("api.example");
2858            None
2859        }
2860    }
2861
2862    fn connection_operation(
2863        path: &str,
2864        id: &str,
2865        security: Vec<SecurityRequirement>,
2866    ) -> ExecutableOperation {
2867        let descriptor = OperationDescriptor::new(
2868            HttpMethod::Get,
2869            path,
2870            id,
2871            "Reports the normalized connection",
2872            None,
2873            vec![ResponseDescriptor::success(
2874                200,
2875                Some(TypeDescriptor::new("Connection")),
2876            )],
2877        )
2878        .expect("test operation id should be valid")
2879        .with_security(security);
2880        ExecutableOperation::typed(descriptor, |input| {
2881            let Extension(connection) =
2882                Extension::<ConnectionInfo>::from_invocation(&input, "connection", true)?;
2883            let body = json!({
2884                "scheme": connection.scheme(),
2885                "host": connection.host(),
2886                "clientIp": connection.client_ip().map(|address| address.to_string()),
2887            });
2888            Ok(Box::pin(async move {
2889                ExecutionOutcome::Success {
2890                    status: 200,
2891                    headers: Vec::new(),
2892                    body: Some(blazingly_json::to_vec(&body).expect("connection body")),
2893                    background: Vec::new(),
2894                }
2895            }) as OperationFuture)
2896        })
2897    }
2898
2899    fn executable() -> ExecutableApp {
2900        ExecutableApp::with_security_schemes(
2901            [
2902                connection_operation(
2903                    "/secure",
2904                    "http.secure",
2905                    vec![SecurityRequirement::new("api_key")],
2906                ),
2907                connection_operation("/plain", "http.plain", Vec::new()),
2908            ],
2909            [SecuritySchemeDescriptor::new(
2910                "api_key",
2911                SecuritySchemeKind::ApiKey {
2912                    location: SecurityLocation::Header,
2913                    name: "x-api-key".to_owned(),
2914                },
2915            )],
2916        )
2917        .expect("secured operation graph should compile")
2918    }
2919
2920    fn error_code(response: &Response) -> String {
2921        let body: Value = response.json().expect("json error body");
2922        body["error"]["code"]
2923            .as_str()
2924            .expect("stable error code")
2925            .to_owned()
2926    }
2927
2928    /// An operation that encodes a view borrowed from a value it owns, which
2929    /// `Json<T>` cannot express because the borrow ends when the operation
2930    /// returns.
2931    fn prepared_operation() -> ExecutableOperation {
2932        let descriptor = OperationDescriptor::new(
2933            HttpMethod::Get,
2934            "/prepared",
2935            "http.prepared",
2936            "Encodes a borrowed view inside the operation",
2937            None,
2938            vec![ResponseDescriptor::success(
2939                200,
2940                Some(TypeDescriptor::new("Titles")),
2941            )],
2942        )
2943        .expect("test operation id should be valid");
2944        ExecutableOperation::typed(descriptor, |_| {
2945            Ok(Box::pin(async move {
2946                let owned = [String::from("first"), String::from("second")];
2947                let borrowed: Vec<&str> = owned.iter().map(String::as_str).collect();
2948                let body = PreparedJson::<TitlesSchema>::encode(&borrowed)
2949                    .expect("the borrowed view encodes");
2950                OperationOutput::into_execution_outcome(body)
2951            }) as OperationFuture)
2952        })
2953    }
2954
2955    struct TitlesSchema;
2956
2957    impl blazingly_core::ApiSchema for TitlesSchema {
2958        fn type_descriptor() -> TypeDescriptor {
2959            TypeDescriptor::new("Titles")
2960        }
2961    }
2962
2963    #[test]
2964    fn a_prepared_body_reaches_the_wire_verbatim_as_json() {
2965        let executable = ExecutableApp::new([prepared_operation()])
2966            .expect("prepared operation graph should compile");
2967        let response = future::block_on(TestApp::new(&executable).call(Request::get("/prepared")));
2968
2969        assert_eq!(response.status(), 200);
2970        assert_eq!(
2971            response.get_header("content-type"),
2972            Some("application/json")
2973        );
2974        assert_eq!(response.body(), br#"["first","second"]"#);
2975    }
2976
2977    #[test]
2978    fn unlayered_dispatch_fails_closed_for_a_declared_scheme() {
2979        let executable = executable();
2980        let response = future::block_on(TestApp::new(&executable).call(Request::get("/secure")));
2981
2982        assert_eq!(response.status(), 500);
2983        assert_eq!(error_code(&response), "security_verifier_missing");
2984    }
2985
2986    #[test]
2987    fn owned_adapter_fails_closed_for_a_declared_scheme() {
2988        let app = HttpApp::new(executable());
2989        let response = future::block_on(app.call(Request::get("/secure")));
2990
2991        assert_eq!(response.status(), 500);
2992        assert_eq!(error_code(&response), "security_verifier_missing");
2993    }
2994
2995    #[test]
2996    fn unverified_scheme_opt_out_executes_the_operation() {
2997        let executable = executable();
2998        let response = future::block_on(
2999            TestApp::new(&executable)
3000                .with_unverified_security_schemes(true)
3001                .call(Request::get("/secure")),
3002        );
3003        assert_eq!(response.status(), 200);
3004
3005        let owned = HttpApp::new(executable).with_unverified_security_schemes(true);
3006        let response = future::block_on(owned.call(Request::get("/secure")));
3007        assert_eq!(response.status(), 200);
3008    }
3009
3010    #[test]
3011    fn routes_emission_lists_every_operation() {
3012        let executable = executable();
3013        let table = super::routes_table_text(executable.definition());
3014        assert!(table.starts_with("METHOD\tPATH\tOPERATION\tSUMMARY\n"));
3015        assert!(table.contains("GET\t/secure\thttp.secure\tReports the normalized connection\n"));
3016        assert!(table.contains("GET\t/plain\thttp.plain\tReports the normalized connection\n"));
3017    }
3018
3019    #[test]
3020    fn openapi_emission_serializes_the_default_document() {
3021        let executable = executable();
3022        let text = super::openapi_document_text(executable.definition())
3023            .expect("the OpenAPI document serializes");
3024        assert!(text.ends_with('\n'));
3025        let document: Value = blazingly_json::from_str(&text).expect("emitted JSON parses");
3026        assert_eq!(document["openapi"].as_str(), Some("3.1.0"));
3027        assert_eq!(
3028            document["paths"]["/secure"]["get"]["operationId"].as_str(),
3029            Some("http.secure")
3030        );
3031        assert_eq!(
3032            document["paths"]["/plain"]["get"]["operationId"].as_str(),
3033            Some("http.plain")
3034        );
3035    }
3036
3037    #[test]
3038    fn unsecured_operations_are_untouched_by_the_guard() {
3039        let executable = executable();
3040        let response = future::block_on(TestApp::new(&executable).call(Request::get("/plain")));
3041
3042        assert_eq!(response.status(), 200);
3043    }
3044
3045    #[test]
3046    fn layered_dispatch_runs_the_same_guard() {
3047        let executable = executable();
3048        let audited = future::block_on(
3049            TestApp::new(&executable)
3050                .with_middleware(AuditLayer)
3051                .call(Request::get("/secure")),
3052        );
3053        assert_eq!(audited.status(), 500);
3054        assert_eq!(error_code(&audited), "security_verifier_missing");
3055
3056        let verified = future::block_on(
3057            TestApp::new(&executable)
3058                .with_middleware(PassthroughLayer)
3059                .call(Request::get("/secure")),
3060        );
3061        assert_eq!(verified.status(), 200);
3062    }
3063
3064    type EventLog = Rc<RefCell<Vec<String>>>;
3065    type HandleExtractor = fn(&InvocationInput<'_>) -> Result<BackgroundTasks, InputRejection>;
3066
3067    struct RecordingLayer {
3068        name: &'static str,
3069        events: EventLog,
3070    }
3071
3072    impl HttpMiddleware for RecordingLayer {
3073        fn on_request(&self, _context: &mut HttpRequestContext<'_>) -> Option<Response> {
3074            self.events
3075                .borrow_mut()
3076                .push(format!("{}:request", self.name));
3077            None
3078        }
3079
3080        fn on_operation(
3081            &self,
3082            _context: &mut HttpRequestContext<'_>,
3083            operation: &OperationDescriptor,
3084            _security_schemes: &[SecuritySchemeDescriptor],
3085        ) -> Option<Response> {
3086            self.events.borrow_mut().push(format!(
3087                "{}:operation:{}",
3088                self.name,
3089                operation.contract.id.as_str()
3090            ));
3091            None
3092        }
3093
3094        fn on_response(
3095            &self,
3096            _context: &HttpRequestContext<'_>,
3097            _operation: Option<&OperationDescriptor>,
3098            _response: &mut Response,
3099        ) {
3100            self.events
3101                .borrow_mut()
3102                .push(format!("{}:response", self.name));
3103        }
3104    }
3105
3106    struct StatusStamp;
3107
3108    impl HttpMiddleware for StatusStamp {
3109        fn on_response(
3110            &self,
3111            _context: &HttpRequestContext<'_>,
3112            _operation: Option<&OperationDescriptor>,
3113            response: &mut Response,
3114        ) {
3115            let status = response.status().to_string();
3116            response.set_header("x-final-status", status);
3117        }
3118
3119        fn verifies_security(&self) -> bool {
3120            false
3121        }
3122    }
3123
3124    struct HouseStyle;
3125
3126    impl HttpErrorHandler for HouseStyle {
3127        fn on_error(&self, error: &HttpError<'_>, response: &mut Response) {
3128            response.set_header("x-error-source", format!("{:?}", error.source()));
3129            match error.source() {
3130                HttpErrorSource::Internal => {
3131                    response.set_status(503);
3132                    response.replace_body(br#"{"error":{"code":"unavailable"}}"#.to_vec());
3133                }
3134                // Deliberately illegal: a typed status is contract, not style.
3135                HttpErrorSource::Domain => response.set_status(500),
3136                HttpErrorSource::Routing
3137                | HttpErrorSource::Request
3138                | HttpErrorSource::Rejection => {}
3139            }
3140        }
3141    }
3142
3143    fn scoped_executable() -> ExecutableApp {
3144        ExecutableApp::new([
3145            connection_operation("/ingest/events", "ingest.events", Vec::new()),
3146            connection_operation("/status", "status.read", Vec::new()),
3147        ])
3148        .expect("scoped operation graph should compile")
3149    }
3150
3151    fn ack_descriptor(path: &str, id: &str) -> OperationDescriptor {
3152        OperationDescriptor::new(
3153            HttpMethod::Get,
3154            path,
3155            id,
3156            "Reports an acknowledgement",
3157            None,
3158            vec![ResponseDescriptor::success(
3159                200,
3160                Some(TypeDescriptor::new("Ack")),
3161            )],
3162        )
3163        .expect("test operation id should be valid")
3164    }
3165
3166    fn outcome_operation(
3167        path: &str,
3168        id: &str,
3169        outcome: fn() -> ExecutionOutcome,
3170    ) -> ExecutableOperation {
3171        ExecutableOperation::typed(ack_descriptor(path, id), move |_| {
3172            Ok(Box::pin(async move { outcome() }) as OperationFuture)
3173        })
3174    }
3175
3176    fn extension_handle(input: &InvocationInput<'_>) -> Result<BackgroundTasks, InputRejection> {
3177        Extension::<BackgroundTasks>::from_invocation(input, "background", true)
3178            .map(|Extension(tasks)| tasks)
3179    }
3180
3181    fn bare_handle(input: &InvocationInput<'_>) -> Result<BackgroundTasks, InputRejection> {
3182        BackgroundTasks::from_invocation(input, "background", true)
3183    }
3184
3185    /// An operation that decides mid-body to schedule after-response work,
3186    /// which the `Background<T>` return type cannot express.
3187    fn scheduling_operation(
3188        path: &str,
3189        id: &str,
3190        log: &EventLog,
3191        extract: HandleExtractor,
3192        outcome: fn() -> ExecutionOutcome,
3193    ) -> ExecutableOperation {
3194        let log = Rc::clone(log);
3195        ExecutableOperation::typed(ack_descriptor(path, id), move |input| {
3196            let tasks = extract(&input)?;
3197            let log = Rc::clone(&log);
3198            tasks.add_infallible(move || async move {
3199                log.borrow_mut().push("task".to_owned());
3200            });
3201            Ok(Box::pin(async move { outcome() }) as OperationFuture)
3202        })
3203    }
3204
3205    fn accepted() -> ExecutionOutcome {
3206        ExecutionOutcome::Success {
3207            status: 200,
3208            headers: Vec::new(),
3209            body: None,
3210            background: Vec::new(),
3211        }
3212    }
3213
3214    fn conflict() -> ExecutionOutcome {
3215        ExecutionOutcome::DomainError(OperationFailure::new(
3216            409,
3217            "conflict",
3218            "the event was already ingested",
3219        ))
3220    }
3221
3222    #[test]
3223    fn a_path_prefix_matches_only_on_segment_boundaries() {
3224        let scope = MiddlewareScope::prefix("/ingest");
3225        assert!(scope.matches_request("/ingest"));
3226        assert!(scope.matches_request("/ingest/events"));
3227        assert!(!scope.matches_request("/ingested"));
3228        assert!(!scope.matches_request("/"));
3229        assert!(MiddlewareScope::prefix("ingest/").matches_request("/ingest/events"));
3230        assert!(MiddlewareScope::all().is_global());
3231        assert!(!scope.is_global());
3232    }
3233
3234    #[test]
3235    fn operation_predicates_select_by_id_after_routing() {
3236        let scope = MiddlewareScope::all().with_operation_prefix("ingest.");
3237        assert!(scope.matches_operation("/anything", "ingest.events"));
3238        assert!(!scope.matches_operation("/anything", "status.read"));
3239        assert!(!scope.matches_request("/anything"));
3240
3241        let scope = MiddlewareScope::all()
3242            .with_operation_filter(|id| id.split('.').next_back() == Some("read"));
3243        assert!(scope.matches_operation("/anything", "status.read"));
3244        assert!(!scope.matches_operation("/anything", "status.write"));
3245
3246        let scope = MiddlewareScope::prefix("/ingest").with_operation("status.read");
3247        assert!(!scope.matches_operation("/status", "status.read"));
3248    }
3249
3250    #[test]
3251    fn a_prefix_scoped_layer_observes_only_its_subtree() {
3252        let executable = scoped_executable();
3253        let events: EventLog = Rc::new(RefCell::new(Vec::new()));
3254        let app = TestApp::new(&executable).with_scoped_middleware(
3255            MiddlewareScope::prefix("/ingest"),
3256            RecordingLayer {
3257                name: "ingest",
3258                events: Rc::clone(&events),
3259            },
3260        );
3261
3262        future::block_on(app.call(Request::get("/ingest/events")));
3263        let recorded = events.borrow().clone();
3264        assert_eq!(
3265            recorded,
3266            [
3267                "ingest:request",
3268                "ingest:operation:ingest.events",
3269                "ingest:response"
3270            ]
3271        );
3272
3273        events.borrow_mut().clear();
3274        future::block_on(app.call(Request::get("/status")));
3275        assert!(events.borrow().is_empty());
3276    }
3277
3278    #[test]
3279    fn an_operation_scoped_layer_starts_after_routing() {
3280        let executable = scoped_executable();
3281        let events: EventLog = Rc::new(RefCell::new(Vec::new()));
3282        let app = TestApp::new(&executable).with_scoped_middleware(
3283            MiddlewareScope::operation("ingest.events"),
3284            RecordingLayer {
3285                name: "op",
3286                events: Rc::clone(&events),
3287            },
3288        );
3289
3290        future::block_on(app.call(Request::get("/ingest/events")));
3291        let recorded = events.borrow().clone();
3292        assert_eq!(recorded, ["op:operation:ingest.events", "op:response"]);
3293
3294        events.borrow_mut().clear();
3295        future::block_on(app.call(Request::get("/status")));
3296        assert!(events.borrow().is_empty());
3297    }
3298
3299    #[test]
3300    fn a_scoped_verifier_does_not_cover_operations_outside_its_scope() {
3301        let executable = executable();
3302        let outside = future::block_on(
3303            TestApp::new(&executable)
3304                .with_scoped_middleware(MiddlewareScope::prefix("/other"), PassthroughLayer)
3305                .call(Request::get("/secure")),
3306        );
3307        assert_eq!(outside.status(), 500);
3308        assert_eq!(error_code(&outside), "security_verifier_missing");
3309
3310        let inside = future::block_on(
3311            TestApp::new(&executable)
3312                .with_scoped_middleware(MiddlewareScope::prefix("/secure"), PassthroughLayer)
3313                .call(Request::get("/secure")),
3314        );
3315        assert_eq!(inside.status(), 200);
3316    }
3317
3318    #[test]
3319    fn an_injected_handle_schedules_work_from_the_handler_body() {
3320        let log: EventLog = Rc::new(RefCell::new(Vec::new()));
3321        let executable = ExecutableApp::new([scheduling_operation(
3322            "/ingest",
3323            "tasks.ok",
3324            &log,
3325            extension_handle,
3326            accepted,
3327        )])
3328        .expect("scheduling operation graph should compile");
3329
3330        let mut response =
3331            future::block_on(TestApp::new(&executable).call(Request::get("/ingest")));
3332        assert_eq!(response.status(), 200);
3333        assert!(log.borrow().is_empty());
3334
3335        future::block_on(response.run_background()).expect("scheduled task");
3336        assert_eq!(log.borrow().clone(), ["task"]);
3337    }
3338
3339    #[test]
3340    fn scheduled_work_survives_a_failed_outcome() {
3341        let log: EventLog = Rc::new(RefCell::new(Vec::new()));
3342        let executable = ExecutableApp::new([scheduling_operation(
3343            "/ingest",
3344            "tasks.conflict",
3345            &log,
3346            bare_handle,
3347            conflict,
3348        )])
3349        .expect("scheduling operation graph should compile");
3350
3351        let mut response =
3352            future::block_on(TestApp::new(&executable).call(Request::get("/ingest")));
3353        assert_eq!(response.status(), 409);
3354        assert_eq!(error_code(&response), "conflict");
3355
3356        future::block_on(response.run_background()).expect("scheduled task");
3357        assert_eq!(log.borrow().clone(), ["task"]);
3358    }
3359
3360    /// An adapter timeout that fires as soon as the handler has scheduled its
3361    /// after-response work.
3362    struct ReadyWhenScheduled {
3363        scheduled: Rc<Cell<bool>>,
3364    }
3365
3366    impl Future for ReadyWhenScheduled {
3367        type Output = ();
3368
3369        fn poll(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<()> {
3370            if self.scheduled.get() {
3371                Poll::Ready(())
3372            } else {
3373                Poll::Pending
3374            }
3375        }
3376    }
3377
3378    fn aborting_operation(log: &EventLog, scheduled: &Rc<Cell<bool>>) -> ExecutableOperation {
3379        let log = Rc::clone(log);
3380        let scheduled = Rc::clone(scheduled);
3381        ExecutableOperation::typed(ack_descriptor("/ingest", "tasks.aborted"), move |input| {
3382            let tasks = bare_handle(&input)?;
3383            let log = Rc::clone(&log);
3384            tasks.add_infallible(move || async move {
3385                log.borrow_mut().push("task".to_owned());
3386            });
3387            scheduled.set(true);
3388            Ok(Box::pin(async move { accepted() }) as OperationFuture)
3389        })
3390    }
3391
3392    #[test]
3393    fn scheduled_work_survives_an_aborted_invocation() {
3394        let log: EventLog = Rc::new(RefCell::new(Vec::new()));
3395        let scheduled = Rc::new(Cell::new(false));
3396        let executable = ExecutableApp::new([aborting_operation(&log, &scheduled)])
3397            .expect("scheduling operation graph should compile");
3398        let control = InvocationControl::new().with_timeout(ReadyWhenScheduled {
3399            scheduled: Rc::clone(&scheduled),
3400        });
3401
3402        let mut response = future::block_on(
3403            TestApp::new(&executable).call_controlled(Request::get("/ingest"), control),
3404        );
3405        assert_eq!(response.status(), 504);
3406
3407        future::block_on(response.run_background()).expect("scheduled task");
3408        assert_eq!(log.borrow().clone(), ["task"]);
3409    }
3410
3411    #[test]
3412    fn an_unused_handle_leaves_the_response_untouched() {
3413        let executable = scoped_executable();
3414        let mut response =
3415            future::block_on(TestApp::new(&executable).call(Request::get("/status")));
3416
3417        assert_eq!(response.status(), 200);
3418        assert!(response.take_background_tasks().is_empty());
3419    }
3420
3421    #[test]
3422    fn an_error_handler_restyles_every_dispatch_failure() {
3423        let executable = executable();
3424        let app = TestApp::new(&executable).with_error_handler(HouseStyle);
3425
3426        let missing = future::block_on(app.call(Request::get("/nope")));
3427        assert_eq!(missing.status(), 404);
3428        assert_eq!(missing.get_header("x-error-source"), Some("Routing"));
3429
3430        let unverified = future::block_on(app.call(Request::get("/secure")));
3431        assert_eq!(unverified.status(), 503);
3432        assert_eq!(unverified.get_header("x-error-source"), Some("Internal"));
3433        assert_eq!(error_code(&unverified), "unavailable");
3434
3435        let allowed = future::block_on(app.call(Request::post("/plain")));
3436        assert_eq!(allowed.status(), 405);
3437        assert_eq!(allowed.get_header("allow"), Some("GET"));
3438        assert_eq!(allowed.get_header("x-error-source"), Some("Routing"));
3439    }
3440
3441    #[test]
3442    fn a_typed_domain_status_survives_the_error_handler() {
3443        let executable = ExecutableApp::new([
3444            outcome_operation("/conflict", "errors.conflict", conflict),
3445            outcome_operation("/broken", "errors.broken", || {
3446                ExecutionOutcome::InternalError {
3447                    code: "boom".to_owned(),
3448                    message: "boom".to_owned(),
3449                }
3450            }),
3451        ])
3452        .expect("error operation graph should compile");
3453        let app = TestApp::new(&executable).with_error_handler(HouseStyle);
3454
3455        let domain = future::block_on(app.call(Request::get("/conflict")));
3456        assert_eq!(domain.status(), 409);
3457        assert_eq!(error_code(&domain), "conflict");
3458        assert_eq!(domain.get_header("x-error-source"), Some("Domain"));
3459
3460        let internal = future::block_on(app.call(Request::get("/broken")));
3461        assert_eq!(internal.status(), 503);
3462        assert_eq!(error_code(&internal), "unavailable");
3463    }
3464
3465    #[test]
3466    fn error_handlers_run_before_middleware_sees_the_response() {
3467        let executable = executable();
3468        let response = future::block_on(
3469            TestApp::new(&executable)
3470                .with_error_handler(HouseStyle)
3471                .with_middleware(StatusStamp)
3472                .call(Request::get("/secure")),
3473        );
3474
3475        assert_eq!(response.status(), 503);
3476        assert_eq!(response.get_header("x-final-status"), Some("503"));
3477    }
3478
3479    #[test]
3480    fn the_owned_adapter_registers_the_same_seams() {
3481        let app = HttpApp::new(executable())
3482            .with_scoped_middleware(MiddlewareScope::prefix("/other"), PassthroughLayer)
3483            .with_error_handler(HouseStyle);
3484        let response = future::block_on(app.call(Request::get("/secure")));
3485
3486        assert_eq!(response.status(), 503);
3487        assert_eq!(response.get_header("x-error-source"), Some("Internal"));
3488    }
3489
3490    #[test]
3491    fn normalized_connection_reaches_the_operation_context() {
3492        let executable = executable();
3493        let request = || {
3494            Request::get("/plain")
3495                .peer_addr(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9000).into())
3496                .header("host", "internal:8080")
3497        };
3498
3499        let unlayered = future::block_on(TestApp::new(&executable).call(request()));
3500        let body: Value = unlayered.json().expect("connection body");
3501        assert_eq!(body["scheme"], "http");
3502        assert_eq!(body["host"], "internal:8080");
3503        assert_eq!(body["clientIp"], "127.0.0.1");
3504
3505        let layered = future::block_on(
3506            TestApp::new(&executable)
3507                .with_middleware(NormalizingProxy)
3508                .call(request()),
3509        );
3510        let body: Value = layered.json().expect("connection body");
3511        assert_eq!(body["scheme"], "https");
3512        assert_eq!(body["host"], "api.example");
3513        assert_eq!(body["clientIp"], "198.51.100.7");
3514    }
3515
3516    fn router_operation(method: HttpMethod, path: &str, id: &str) -> ExecutableOperation {
3517        let inputs = path
3518            .split('/')
3519            .filter_map(|segment| {
3520                segment
3521                    .strip_prefix('{')
3522                    .and_then(|segment| segment.strip_suffix('}'))
3523            })
3524            .map(|name| {
3525                InputDescriptor::new(name, InputSource::Path, true, TypeDescriptor::new("String"))
3526            })
3527            .collect::<Vec<_>>();
3528        let descriptor = OperationDescriptor::new(
3529            method,
3530            path,
3531            id,
3532            "Router fixture",
3533            None,
3534            vec![ResponseDescriptor::success(200, None)],
3535        )
3536        .expect("test operation id should be valid")
3537        .with_inputs(inputs);
3538        ExecutableOperation::typed(descriptor, |_| {
3539            Ok(Box::pin(async move {
3540                ExecutionOutcome::Success {
3541                    status: 200,
3542                    headers: Vec::new(),
3543                    body: None,
3544                    background: Vec::new(),
3545                }
3546            }) as OperationFuture)
3547        })
3548    }
3549
3550    fn router(routes: &[(HttpMethod, &str)]) -> Router {
3551        let operations = routes
3552            .iter()
3553            .enumerate()
3554            .map(|(index, (method, path))| {
3555                router_operation(*method, path, &format!("router.op{index}"))
3556            })
3557            .collect::<Vec<_>>();
3558        let app = ExecutableApp::new(operations).expect("router fixture compiles");
3559        Router::new(&app)
3560    }
3561
3562    fn allowed(router: &Router, method: HttpMethod, path: &str) -> Vec<HttpMethod> {
3563        match router.recognize(method, path) {
3564            Err(RouteError::MethodNotAllowed { allowed }) => allowed,
3565            other => panic!("{method:?} {path} should be a 405, got {:?}", other.is_ok()),
3566        }
3567    }
3568
3569    /// The 405 `Allow` list is rendered by walking the method table low bit
3570    /// first, so its order is the enum's `Ord`, not alphabetical.
3571    #[test]
3572    fn the_method_table_matches_the_http_method_order() {
3573        let mut sorted = super::METHODS;
3574        sorted.sort_unstable();
3575        assert_eq!(sorted, super::METHODS);
3576        for (index, method) in super::METHODS.into_iter().enumerate() {
3577            assert_eq!(super::method_index(method), index);
3578        }
3579    }
3580
3581    #[test]
3582    fn a_static_path_outranks_a_parameter_of_the_same_shape() {
3583        let router = router(&[
3584            (HttpMethod::Get, "/items"),
3585            (HttpMethod::Post, "/items"),
3586            (HttpMethod::Get, "/items/latest"),
3587            (HttpMethod::Put, "/items/latest"),
3588            (HttpMethod::Get, "/items/{id}"),
3589            (HttpMethod::Delete, "/items/{id}"),
3590        ]);
3591
3592        let statically = router
3593            .recognize(HttpMethod::Get, "/items/latest")
3594            .expect("static route");
3595        assert_eq!(statically.path_parameter("id"), None);
3596
3597        // No static DELETE for that path, so the walk falls through to the
3598        // parameter route and captures the segment the static table matched.
3599        let dynamically = router
3600            .recognize(HttpMethod::Delete, "/items/latest")
3601            .expect("parameter route");
3602        assert_eq!(
3603            dynamically.path_parameter("id").as_deref(),
3604            Some("latest"),
3605            "the static probe must not consume the segment"
3606        );
3607    }
3608
3609    #[test]
3610    fn an_allow_list_unions_static_and_parameter_routes_in_enum_order() {
3611        let router = router(&[
3612            (HttpMethod::Get, "/items"),
3613            (HttpMethod::Post, "/items"),
3614            (HttpMethod::Get, "/items/latest"),
3615            (HttpMethod::Put, "/items/latest"),
3616            (HttpMethod::Get, "/items/{id}"),
3617            (HttpMethod::Delete, "/items/{id}"),
3618        ]);
3619
3620        assert_eq!(
3621            allowed(&router, HttpMethod::Patch, "/items"),
3622            vec![HttpMethod::Get, HttpMethod::Post]
3623        );
3624        assert_eq!(
3625            allowed(&router, HttpMethod::Post, "/items/latest"),
3626            vec![HttpMethod::Get, HttpMethod::Put, HttpMethod::Delete],
3627            "PUT precedes DELETE in the enum even though it follows alphabetically"
3628        );
3629        assert_eq!(
3630            allowed(&router, HttpMethod::Post, "/items/7"),
3631            vec![HttpMethod::Get, HttpMethod::Delete]
3632        );
3633    }
3634
3635    #[test]
3636    fn an_unknown_path_is_a_miss_rather_than_a_wrong_method() {
3637        let router = router(&[
3638            (HttpMethod::Get, "/items"),
3639            (HttpMethod::Get, "/items/{id}/tags/{tag}"),
3640        ]);
3641
3642        for path in ["/nope", "/items/7/tags", "/items/7/tags/x/y", "/", ""] {
3643            assert_eq!(
3644                router.recognize(HttpMethod::Get, path).err(),
3645                Some(RouteError::NotFound),
3646                "{path} should not exist"
3647            );
3648        }
3649    }
3650
3651    #[test]
3652    fn a_failed_static_descent_backtracks_into_the_parameter_child() {
3653        let router = router(&[
3654            (HttpMethod::Get, "/a/{p}/x"),
3655            (HttpMethod::Post, "/a/{p}/x"),
3656            (HttpMethod::Get, "/a/b/y"),
3657        ]);
3658
3659        // `b` matches the static child, which dead-ends at `x`; the walk has to
3660        // unwind and take the parameter child with the same segment.
3661        let matched = router
3662            .recognize(HttpMethod::Get, "/a/b/x")
3663            .expect("parameter route after backtracking");
3664        assert_eq!(matched.path_parameter("p").as_deref(), Some("b"));
3665
3666        let statically = router
3667            .recognize(HttpMethod::Get, "/a/b/y")
3668            .expect("static branch");
3669        assert_eq!(statically.path_parameter("p"), None);
3670
3671        assert_eq!(
3672            router.recognize(HttpMethod::Get, "/a/c/y").err(),
3673            Some(RouteError::NotFound)
3674        );
3675        assert_eq!(
3676            allowed(&router, HttpMethod::Delete, "/a/b/x"),
3677            vec![HttpMethod::Get, HttpMethod::Post]
3678        );
3679    }
3680
3681    #[test]
3682    fn captures_survive_a_deep_backtrack() {
3683        let router = router(&[
3684            (HttpMethod::Get, "/{one}/{two}/{three}/leaf"),
3685            (HttpMethod::Get, "/a/b/c/other"),
3686        ]);
3687
3688        let matched = router
3689            .recognize(HttpMethod::Get, "/a/b/c/leaf")
3690            .expect("parameter route");
3691        assert_eq!(matched.path_parameter("one").as_deref(), Some("a"));
3692        assert_eq!(matched.path_parameter("two").as_deref(), Some("b"));
3693        assert_eq!(matched.path_parameter("three").as_deref(), Some("c"));
3694        assert_eq!(matched.path_parameter("four"), None);
3695    }
3696
3697    #[test]
3698    fn a_captured_segment_is_percent_decoded() {
3699        let router = router(&[(HttpMethod::Get, "/files/{name}")]);
3700
3701        let matched = router
3702            .recognize(HttpMethod::Get, "/files/a%20b%2Fc")
3703            .expect("parameter route");
3704        assert_eq!(matched.path_parameter("name").as_deref(), Some("a b/c"));
3705
3706        let plus = router
3707            .recognize(HttpMethod::Get, "/files/a+b")
3708            .expect("parameter route");
3709        assert_eq!(
3710            plus.path_parameter("name").as_deref(),
3711            Some("a+b"),
3712            "a path segment is not form encoded"
3713        );
3714
3715        let invalid = router
3716            .recognize(HttpMethod::Get, "/files/a%2")
3717            .expect("parameter route");
3718        assert_eq!(invalid.path_parameter("name"), None);
3719    }
3720
3721    #[test]
3722    fn empty_segments_are_matched_literally() {
3723        let router = router(&[(HttpMethod::Get, "/files/{name}/meta")]);
3724
3725        let matched = router
3726            .recognize(HttpMethod::Get, "/files//meta")
3727            .expect("empty segments are still segments");
3728        assert_eq!(matched.path_parameter("name").as_deref(), Some(""));
3729    }
3730
3731    #[test]
3732    fn every_method_reaches_its_own_static_slot() {
3733        let bindings = super::METHODS
3734            .into_iter()
3735            .map(|method| (method, "/one"))
3736            .collect::<Vec<_>>();
3737        let router = router(&bindings);
3738
3739        let mut seen = Vec::new();
3740        for method in super::METHODS {
3741            let matched = router.recognize(method, "/one").expect("static route");
3742            seen.push(matched.operation_index());
3743        }
3744        seen.sort_unstable();
3745        seen.dedup();
3746        assert_eq!(seen.len(), super::METHOD_COUNT, "slots must not alias");
3747    }
3748}