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
29pub const EMIT_VARIABLE: &str = "BLAZINGLY_EMIT";
32
33#[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
44pub 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 fn take_body_stream(&self) -> Option<StreamingBody> {
56 None
57 }
58
59 fn peer_addr(&self) -> Option<SocketAddr> {
61 None
62 }
63
64 #[allow(clippy::unnecessary_literal_bound)]
66 fn scheme(&self) -> &str {
67 "http"
68 }
69}
70
71pub 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 #[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#[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 #[must_use]
181 pub const fn client_ip(&self) -> Option<IpAddr> {
182 self.client_ip
183 }
184
185 #[must_use]
187 pub fn scheme(&self) -> &str {
188 &self.scheme
189 }
190
191 #[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
206pub trait HttpMiddleware {
212 fn on_request(&self, _context: &mut HttpRequestContext<'_>) -> Option<Response> {
214 None
215 }
216
217 fn on_operation(
219 &self,
220 _context: &mut HttpRequestContext<'_>,
221 _operation: &OperationDescriptor,
222 _security_schemes: &[SecuritySchemeDescriptor],
223 ) -> Option<Response> {
224 None
225 }
226
227 fn on_response(
230 &self,
231 _context: &HttpRequestContext<'_>,
232 _operation: Option<&OperationDescriptor>,
233 _response: &mut Response,
234 ) {
235 }
236
237 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#[derive(Clone, Default)]
282pub struct MiddlewareScope {
283 prefixes: Vec<Box<str>>,
284 operations: Vec<OperationPredicate>,
285}
286
287impl MiddlewareScope {
288 #[must_use]
290 pub const fn all() -> Self {
291 Self {
292 prefixes: Vec::new(),
293 operations: Vec::new(),
294 }
295 }
296
297 #[must_use]
299 pub fn prefix(prefix: &str) -> Self {
300 Self::all().with_prefix(prefix)
301 }
302
303 #[must_use]
305 pub fn operation(operation_id: &str) -> Self {
306 Self::all().with_operation(operation_id)
307 }
308
309 #[must_use]
313 pub fn with_prefix(mut self, prefix: &str) -> Self {
314 self.prefixes.push(normalize_prefix(prefix));
315 self
316 }
317
318 #[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 #[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 #[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 #[must_use]
349 pub fn is_global(&self) -> bool {
350 self.prefixes.is_empty() && self.operations.is_empty()
351 }
352
353 #[must_use]
355 pub fn matches_request(&self, path: &str) -> bool {
356 self.operations.is_empty() && self.matches_path(path)
357 }
358
359 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
426pub enum HttpErrorSource {
427 Routing,
429 Request,
431 Rejection,
433 Domain,
435 Internal,
438}
439
440pub 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 #[must_use]
454 pub const fn source(&self) -> HttpErrorSource {
455 self.source
456 }
457
458 #[must_use]
460 pub const fn status(&self) -> u16 {
461 self.status
462 }
463
464 #[must_use]
466 pub const fn code(&self) -> &str {
467 self.code
468 }
469
470 #[must_use]
472 pub const fn message(&self) -> &str {
473 self.message
474 }
475
476 #[must_use]
478 pub const fn method(&self) -> HttpMethod {
479 self.method
480 }
481
482 #[must_use]
484 pub const fn path(&self) -> &str {
485 self.path
486 }
487
488 #[must_use]
490 pub const fn operation(&self) -> Option<&OperationDescriptor> {
491 self.operation
492 }
493}
494
495pub trait HttpErrorHandler {
511 fn on_error(&self, error: &HttpError<'_>, response: &mut Response);
513}
514
515#[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 pub fn add_task(&self, task: BackgroundTask) {
539 self.tasks.borrow_mut().push(task);
540 }
541
542 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 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 #[must_use]
562 pub fn len(&self) -> usize {
563 self.tasks.borrow().len()
564 }
565
566 #[must_use]
568 pub fn is_empty(&self) -> bool {
569 self.tasks.borrow().is_empty()
570 }
571
572 #[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 #[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 #[must_use]
703 pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
704 self.scheme = scheme.into();
705 self
706 }
707
708 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#[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 #[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 #[must_use]
801 pub fn empty(status: u16) -> Self {
802 Self::from_bytes(status, Vec::new())
803 }
804
805 #[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 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 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 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 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 #[must_use]
862 pub const fn is_streaming(&self) -> bool {
863 self.stream.is_some()
864 }
865
866 #[must_use]
872 pub fn take_body_stream(&mut self) -> Option<StreamingBody> {
873 self.stream.take()
874 }
875
876 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 pub fn take_upgrade(&mut self) -> Option<HttpUpgrade> {
892 self.upgrade.take()
893 }
894
895 pub fn take_background_tasks(&mut self) -> Vec<BackgroundTask> {
898 std::mem::take(&mut self.background)
899 }
900
901 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 #[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 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 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 pub fn text(&self) -> Result<&str, Utf8Error> {
978 std::str::from_utf8(&self.body)
979 }
980
981 pub fn json<T: DeserializeOwned>(&self) -> Result<T, blazingly_json::Error> {
987 blazingly_json::from_slice(&self.body)
988 }
989}
990
991#[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
1014pub 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 #[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 #[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 pub async fn startup(&self) -> Result<(), DependencyError> {
1088 self.app.startup().await
1089 }
1090
1091 pub async fn shutdown(&self) -> Result<(), DependencyError> {
1097 self.app.shutdown().await
1098 }
1099
1100 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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
1215fn 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
1235fn 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
1261fn 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
1269fn 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
1286pub 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 #[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 pub async fn startup(&self) -> Result<(), DependencyError> {
1334 self.app.startup().await
1335 }
1336
1337 pub async fn shutdown(&self) -> Result<(), DependencyError> {
1343 self.app.shutdown().await
1344 }
1345
1346 #[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 #[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 #[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 #[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 #[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 #[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 #[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
1440struct 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
1451struct 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 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 if failure.source == HttpErrorSource::Domain {
1638 response.status = failure.status;
1639 }
1640 response
1641 }
1642
1643 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
1669struct 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
1781const 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#[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 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
1861struct StaticChild {
1863 head: u8,
1864 node: usize,
1865 segment: Box<str>,
1866}
1867
1868#[derive(Default)]
1869struct RouteNode {
1870 static_children: Vec<StaticChild>,
1873 parameter_child: Option<usize>,
1874 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#[derive(Default)]
1902struct StaticSlot {
1903 methods: u16,
1905 endpoints: [usize; METHOD_COUNT],
1906}
1907
1908pub struct Router {
1910 nodes: Vec<RouteNode>,
1911 endpoints: Vec<CompiledEndpoint>,
1913 static_routes: PathMap<StaticSlot>,
1914 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 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 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 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
2115fn 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
2124const fn head_byte(segment: &str) -> u8 {
2127 match segment.as_bytes().first() {
2128 Some(byte) => *byte,
2129 None => 0,
2130 }
2131}
2132
2133fn 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#[derive(Clone, Debug, Eq, PartialEq)]
2145pub enum RouteError {
2146 MethodNotAllowed { allowed: Vec<HttpMethod> },
2147 NotFound,
2148}
2149
2150pub 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#[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 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 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 fn background_tasks(&self) -> &BackgroundTasks {
2272 self.background.get_or_init(BackgroundTasks::new)
2273 }
2274
2275 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 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 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 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 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 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 #[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 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 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}