Skip to main content

blazingly_http/
lib.rs

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