Skip to main content

serverkit/
app.rs

1use std::{
2    any::TypeId,
3    collections::HashMap,
4    fmt::{self, Write},
5    sync::Arc,
6};
7
8use crate::{
9    Case, Dispatch, Dispatcher, Error, ErrorFormat, Handler, IntoResponse, Middleware, OpenApi,
10    OpenApiDocument, Request, Response, Routes, Scope,
11    error::JsonErrorFormat,
12    middleware::{MiddlewareEntry, MiddlewareTerminal, run as run_middleware},
13    router::{join_paths, validate_scope_prefix},
14};
15
16#[derive(Clone)]
17pub struct Config {
18    prefix: String,
19    error_format: Arc<dyn ErrorFormat>,
20    json_case: Option<Case>,
21}
22
23impl Default for Config {
24    fn default() -> Self {
25        Self {
26            prefix: String::new(),
27            error_format: Arc::new(JsonErrorFormat),
28            json_case: None,
29        }
30    }
31}
32
33impl fmt::Debug for Config {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        formatter
36            .debug_struct("Config")
37            .field("prefix", &self.prefix)
38            .field("json_case", &self.json_case)
39            .finish_non_exhaustive()
40    }
41}
42
43impl Config {
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
49        self.prefix = prefix.into();
50        self
51    }
52
53    pub fn error_format(mut self, format: impl ErrorFormat) -> Self {
54        self.error_format = Arc::new(format);
55        self
56    }
57
58    pub fn json_case(mut self, case: Case) -> Self {
59        self.json_case = Some(case);
60        self
61    }
62}
63
64pub struct Router {
65    dispatcher: Dispatcher,
66    scope: Scope,
67    openapi: Option<PublishedOpenApi>,
68    error_format: Arc<dyn ErrorFormat>,
69    json_case: Option<Case>,
70}
71
72struct PublishedOpenApi {
73    path: String,
74    configuration: OpenApi,
75    document: OpenApiDocument,
76    scalar_page: String,
77}
78
79impl Router {
80    pub fn new(config: Config, routes: impl Routes) -> Self {
81        let Config {
82            prefix,
83            error_format,
84            json_case,
85        } = config;
86        let prefix = normalize_prefix(prefix);
87        validate_scope_prefix(&prefix)
88            .unwrap_or_else(|error| panic!("invalid Router prefix `{prefix}`: {error}"));
89        let mut router = Self {
90            dispatcher: Dispatcher::new(),
91            scope: Scope::new(prefix),
92            openapi: None,
93            error_format,
94            json_case,
95        };
96
97        routes.apply(&mut router);
98
99        router
100    }
101
102    pub(crate) fn register<
103        Arguments: 'static,
104        Input: 'static,
105        H: Handler<Arguments, Input> + Send + Sync + 'static,
106    >(
107        &mut self,
108        method: crate::Method,
109        path: &'static str,
110        handler: H,
111        operation: crate::Operation,
112        middlewares: Vec<MiddlewareEntry>,
113        excluded_middlewares: Vec<TypeId>,
114    ) {
115        let path = join_paths(self.scope.prefix(), path);
116        self.dispatcher.register(
117            method,
118            path,
119            handler,
120            operation,
121            middlewares,
122            excluded_middlewares,
123        );
124    }
125
126    pub fn route(mut self, routes: impl Routes) -> Self {
127        routes.apply(&mut self);
128        self.refresh_openapi();
129        self
130    }
131
132    pub fn at(mut self, prefix: impl Into<String>) -> Self {
133        let prefix = normalize_prefix(prefix.into());
134        validate_scope_prefix(&prefix)
135            .unwrap_or_else(|error| panic!("invalid Router mount `{prefix}`: {error}"));
136
137        if prefix.is_empty() {
138            return self;
139        }
140
141        self.dispatcher.prepend(&prefix);
142        self.scope.prepend(&prefix);
143        if let Some(published) = &mut self.openapi {
144            published.path = join_paths(&prefix, &published.path);
145        }
146        self.refresh_openapi();
147        self
148    }
149
150    pub(crate) fn register_router(&mut self, mut router: Router) {
151        let parent_prefix = self.scope.prefix().to_owned();
152        if !parent_prefix.is_empty() {
153            router = router.at(parent_prefix);
154        }
155
156        router.dispatcher.add_scope(router.scope);
157        self.dispatcher.merge(router.dispatcher);
158    }
159
160    pub fn fallback<Arguments: 'static, Input: 'static>(
161        mut self,
162        handler: impl Handler<Arguments, Input> + Send + Sync + 'static,
163    ) -> Self {
164        self.dispatcher.set_fallback(self.scope.prefix(), handler);
165        self.refresh_openapi();
166        self
167    }
168
169    pub fn state<T: Send + Sync + 'static>(mut self, state: T) -> Self {
170        self.scope.state(state);
171        self
172    }
173
174    pub fn body_limit(mut self, limit: usize) -> Self {
175        self.scope.body_limit(limit);
176        self
177    }
178
179    pub fn middleware<M: Middleware>(mut self, middleware: M) -> Self {
180        self.scope.middleware(MiddlewareEntry::new(middleware));
181        self
182    }
183
184    pub fn openapi(mut self, path: impl Into<String>, configuration: OpenApi) -> Self {
185        let path = join_paths(self.scope.prefix(), &path.into());
186        self.publish_openapi(path, configuration);
187        self
188    }
189
190    pub fn openapi_document(&self) -> Option<&OpenApiDocument> {
191        self.openapi.as_ref().map(|published| &published.document)
192    }
193
194    pub async fn handle(&self, mut request: Request) -> Response {
195        let head = request.method == crate::Method::HEAD;
196        request.set_json_case(self.json_case);
197        let mut terminal = match self.openapi.as_ref() {
198            Some(published) if request.path == published.path => RouterTerminal::OpenApi(published),
199            _ => RouterTerminal::Dispatch(self.dispatcher.resolve(&request)),
200        };
201        let params = match &mut terminal {
202            RouterTerminal::Dispatch(Dispatch::Route { captures, .. }) => std::mem::take(captures),
203            _ => Vec::new(),
204        };
205        request.set_params(params);
206
207        let exclusions = terminal.excluded_middlewares();
208        let mut states = HashMap::new();
209        let mut body_limit = None;
210        let mut middlewares = Vec::new();
211
212        if self.scope.matches(&request.path) {
213            apply_scope(
214                &self.scope,
215                exclusions,
216                &mut states,
217                &mut body_limit,
218                &mut middlewares,
219            );
220        }
221
222        let dispatch = match &terminal {
223            RouterTerminal::Dispatch(dispatch) => Some(dispatch),
224            RouterTerminal::OpenApi(_) => None,
225        };
226        for scope in self.dispatcher.matching_scopes(&request.path, dispatch) {
227            apply_scope(
228                scope,
229                exclusions,
230                &mut states,
231                &mut body_limit,
232                &mut middlewares,
233            );
234        }
235
236        middlewares.extend(terminal.route_middlewares());
237        request.set_states(states);
238        request.set_body_limit(body_limit);
239
240        let response = run_middleware(&middlewares, &terminal, request).await;
241        let response = self.finalize_error(response);
242
243        if head {
244            response.without_body()
245        } else {
246            response
247        }
248    }
249
250    fn finalize_error(&self, mut response: Response) -> Response {
251        let (error, validation) = match response.take_error() {
252            Some(error) => error.into_parts(),
253            None if (400..=599).contains(&response.status()) => (
254                Error::new(
255                    response.status(),
256                    format!("http.{}", response.status()),
257                    response_error_message(&response),
258                ),
259                None,
260            ),
261            None => return response,
262        };
263
264        let status = error.status();
265        let mut source_chain = String::new();
266        let mut source = error
267            .source()
268            .map(|source| source as &(dyn std::error::Error + 'static));
269
270        while let Some(cause) = source {
271            if !source_chain.is_empty() {
272                source_chain.push_str(": ");
273            }
274
275            let _ = write!(source_chain, "{cause}");
276            source = cause.source();
277        }
278
279        if status >= 500 || !source_chain.is_empty() {
280            tracing::error!(
281                http.status = status,
282                error.code = error.code(),
283                error.message = error.message(),
284                error.source = source_chain,
285                "request failed"
286            );
287        }
288
289        let mut headers = response.take_headers();
290        remove_representation_headers(&mut headers);
291        let mut formatted = match validation.as_ref() {
292            Some(validation) => self.error_format.format_validation(&error, validation),
293            None => self.error_format.format(&error),
294        };
295
296        if let Some(format_error) = formatted.take_error() {
297            let (format_error, format_validation) = format_error.into_parts();
298            formatted = match format_validation.as_ref() {
299                Some(validation) => JsonErrorFormat.format_validation(&format_error, validation),
300                None => JsonErrorFormat.format(&format_error),
301            };
302        }
303
304        formatted.set_status(status);
305        formatted.merge_headers(headers);
306        formatted
307    }
308
309    fn publish_openapi(&mut self, path: String, configuration: OpenApi) {
310        self.dispatcher.validate_openapi_path(&path);
311        let document = self.build_openapi(&configuration);
312        let scalar_page = configuration.scalar_page(&document);
313        self.openapi = Some(PublishedOpenApi {
314            path,
315            configuration,
316            document,
317            scalar_page,
318        });
319    }
320
321    fn refresh_openapi(&mut self) {
322        let Some((path, configuration)) = self
323            .openapi
324            .as_ref()
325            .map(|published| (published.path.clone(), published.configuration.clone()))
326        else {
327            return;
328        };
329
330        self.publish_openapi(path, configuration);
331    }
332
333    fn build_openapi(&self, configuration: &OpenApi) -> OpenApiDocument {
334        configuration.build(self.dispatcher.openapi_routes(), self.json_case)
335    }
336}
337
338fn response_error_message(response: &Response) -> String {
339    std::str::from_utf8(response.body())
340        .ok()
341        .filter(|message| !message.is_empty())
342        .map(str::to_owned)
343        .unwrap_or_else(|| status_message(response.status()).to_owned())
344}
345
346fn status_message(status: u16) -> &'static str {
347    match status {
348        400 => "Bad Request",
349        401 => "Unauthorized",
350        402 => "Payment Required",
351        403 => "Forbidden",
352        404 => "Not Found",
353        405 => "Method Not Allowed",
354        406 => "Not Acceptable",
355        408 => "Request Timeout",
356        409 => "Conflict",
357        410 => "Gone",
358        411 => "Length Required",
359        412 => "Precondition Failed",
360        413 => "Payload Too Large",
361        414 => "URI Too Long",
362        415 => "Unsupported Media Type",
363        416 => "Range Not Satisfiable",
364        417 => "Expectation Failed",
365        422 => "Unprocessable Content",
366        426 => "Upgrade Required",
367        429 => "Too Many Requests",
368        500 => "Internal Server Error",
369        501 => "Not Implemented",
370        502 => "Bad Gateway",
371        503 => "Service Unavailable",
372        504 => "Gateway Timeout",
373        505 => "HTTP Version Not Supported",
374        _ => "HTTP Error",
375    }
376}
377
378fn remove_representation_headers(headers: &mut crate::Headers) {
379    for name in [
380        "Content-Encoding",
381        "Content-Length",
382        "Content-Type",
383        "Transfer-Encoding",
384    ] {
385        headers.remove(name);
386    }
387}
388
389fn normalize_prefix(prefix: String) -> String {
390    if prefix == "/" { String::new() } else { prefix }
391}
392
393fn apply_scope<'scope>(
394    scope: &'scope Scope,
395    exclusions: &[TypeId],
396    states: &mut HashMap<TypeId, std::sync::Arc<dyn std::any::Any + Send + Sync>>,
397    body_limit: &mut Option<usize>,
398    middlewares: &mut Vec<&'scope MiddlewareEntry>,
399) {
400    states.extend(
401        scope
402            .states()
403            .iter()
404            .map(|(type_id, state)| (*type_id, state.clone())),
405    );
406    if let Some(limit) = scope.configured_body_limit() {
407        *body_limit = Some(limit);
408    }
409    middlewares.extend(
410        scope
411            .middlewares()
412            .iter()
413            .filter(|middleware| !exclusions.contains(&middleware.type_id())),
414    );
415}
416
417enum RouterTerminal<'router> {
418    OpenApi(&'router PublishedOpenApi),
419    Dispatch(Dispatch<'router>),
420}
421
422impl RouterTerminal<'_> {
423    fn excluded_middlewares(&self) -> &[TypeId] {
424        match self {
425            Self::OpenApi(_) => &[],
426            Self::Dispatch(dispatch) => dispatch.excluded_middlewares(),
427        }
428    }
429
430    fn route_middlewares(&self) -> &[MiddlewareEntry] {
431        match self {
432            Self::OpenApi(_) => &[],
433            Self::Dispatch(dispatch) => dispatch.route_middlewares(),
434        }
435    }
436}
437
438impl MiddlewareTerminal for RouterTerminal<'_> {
439    fn call(&self, request: Request) -> crate::middleware::MiddlewareFuture<'_> {
440        Box::pin(async move {
441            match self {
442                Self::OpenApi(published) => {
443                    let mut response =
444                        Response::bytes(200, published.scalar_page.as_bytes().to_vec());
445                    response.set_header("Content-Type", "text/html; charset=utf-8");
446
447                    let mut response = match request.method.as_str() {
448                        "GET" => response,
449                        "HEAD" => response,
450                        "OPTIONS" => Response::empty(),
451                        _ => Error::new(405, "route.method_not_allowed", "Method Not Allowed")
452                            .into_response(),
453                    };
454                    response.set_header("Allow", "GET, HEAD, OPTIONS");
455                    response
456                }
457                Self::Dispatch(dispatch) => dispatch.call(request).await,
458            }
459        })
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use std::{
466        error::Error as StdError,
467        fmt,
468        future::Future,
469        task::{Context, Poll, Waker},
470    };
471
472    use crate::{
473        Config, Error, Form, Headers, Method, Middleware, Next, OpenApi, Path, Query, Request,
474        RequestStream, Response, RouteMethods, Router, StreamError,
475    };
476
477    #[derive(crate::Schema)]
478    struct ItemPath {
479        id: u64,
480    }
481
482    #[derive(crate::Schema)]
483    #[allow(dead_code)]
484    struct SearchQuery {
485        query: String,
486        page: Option<u32>,
487    }
488
489    #[derive(crate::Schema)]
490    struct CreateItem {
491        name: String,
492    }
493
494    #[cfg(feature = "json")]
495    #[derive(crate::Schema, serde::Deserialize, serde::Serialize)]
496    struct JsonItem {
497        name: String,
498    }
499
500    #[cfg(feature = "json")]
501    #[derive(Debug, PartialEq, crate::Schema, serde::Deserialize, serde::Serialize)]
502    struct JsonCaseChild {
503        child_value: u64,
504    }
505
506    #[cfg(feature = "json")]
507    #[derive(Debug, PartialEq, crate::Schema, serde::Deserialize, serde::Serialize)]
508    struct JsonCaseItem {
509        display_name: String,
510        #[serde(rename = "external_id")]
511        internal_id: u64,
512        #[schema(nested)]
513        child_record: JsonCaseChild,
514    }
515
516    #[cfg(feature = "json")]
517    #[derive(crate::Schema, serde::Deserialize, serde::Serialize)]
518    #[serde(rename_all = "camelCase")]
519    struct SerdeNamedItem {
520        fixed_by_serde: bool,
521    }
522
523    struct EmptyStream;
524
525    impl RequestStream for EmptyStream {
526        fn poll_next(
527            &mut self,
528            _context: &mut Context<'_>,
529        ) -> Poll<Option<Result<(), StreamError>>> {
530            Poll::Ready(None)
531        }
532
533        fn chunk(&self) -> &[u8] {
534            &[]
535        }
536    }
537
538    #[cfg(feature = "json")]
539    struct TestBody {
540        bytes: Vec<u8>,
541        sent: bool,
542    }
543
544    #[cfg(feature = "json")]
545    impl RequestStream for TestBody {
546        fn poll_next(
547            &mut self,
548            _context: &mut Context<'_>,
549        ) -> Poll<Option<Result<(), StreamError>>> {
550            if self.sent {
551                Poll::Ready(None)
552            } else {
553                self.sent = true;
554                Poll::Ready(Some(Ok(())))
555            }
556        }
557
558        fn chunk(&self) -> &[u8] {
559            &self.bytes
560        }
561    }
562
563    async fn item(Path(path): Path<ItemPath>, Query(query): Query<SearchQuery>) -> String {
564        format!("{}:{}", path.id, query.query)
565    }
566
567    async fn create(Form(item): Form<CreateItem>) -> String {
568        item.name
569    }
570
571    async fn failure() -> Result<&'static str, Error> {
572        Err(Error::new(
573            409,
574            "sample.failed",
575            "The sample operation failed",
576        ))
577    }
578
579    #[derive(Debug)]
580    struct DatabaseError;
581
582    impl fmt::Display for DatabaseError {
583        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
584            formatter.write_str("database connection lost")
585        }
586    }
587
588    impl StdError for DatabaseError {}
589
590    async fn internal_failure() -> Result<&'static str, Error> {
591        Err(DatabaseError)?
592    }
593
594    async fn raw_error() -> Response {
595        Response::text(418, "custom raw error")
596    }
597
598    struct AddErrorHeader;
599
600    impl Middleware for AddErrorHeader {
601        async fn handle(&self, request: Request, next: Next<'_>) -> Response {
602            let mut response = next.run(request).await;
603            response
604                .headers()
605                .set("X-Error-Scope", "middleware")
606                .unwrap();
607            response
608        }
609    }
610
611    #[cfg(feature = "json")]
612    async fn create_json(crate::Json(item): crate::Json<JsonItem>) -> crate::Json<JsonItem> {
613        crate::Json(item)
614    }
615
616    #[cfg(feature = "json")]
617    async fn json_case_item() -> crate::Json<JsonCaseItem> {
618        crate::Json(JsonCaseItem {
619            display_name: "Ada".to_owned(),
620            internal_id: 7,
621            child_record: JsonCaseChild { child_value: 9 },
622        })
623    }
624
625    #[cfg(feature = "json")]
626    async fn echo_json_case(
627        crate::Json(item): crate::Json<JsonCaseItem>,
628    ) -> crate::Json<JsonCaseItem> {
629        crate::Json(item)
630    }
631
632    #[cfg(feature = "json")]
633    async fn serde_named_item() -> crate::Json<SerdeNamedItem> {
634        crate::Json(SerdeNamedItem {
635            fixed_by_serde: true,
636        })
637    }
638
639    fn request(method: &str, path: &str) -> Request {
640        Request::from_parts(
641            Method::try_from(method).unwrap(),
642            path,
643            None,
644            Headers::new(),
645            Box::new(EmptyStream),
646        )
647    }
648
649    #[cfg(feature = "json")]
650    fn json_request(path: &str, body: &'static [u8]) -> Request {
651        let mut headers = Headers::new();
652        headers.set("Content-Type", "application/json").unwrap();
653        Request::from_parts(
654            Method::POST,
655            path,
656            None,
657            headers,
658            Box::new(TestBody {
659                bytes: body.to_vec(),
660                sent: false,
661            }),
662        )
663    }
664
665    fn block_on<F: Future>(future: F) -> F::Output {
666        let mut future = std::pin::pin!(future);
667        let waker = Waker::noop();
668        let mut context = Context::from_waker(waker);
669
670        loop {
671            match future.as_mut().poll(&mut context) {
672                Poll::Ready(output) => return output,
673                Poll::Pending => std::thread::yield_now(),
674            }
675        }
676    }
677
678    #[test]
679    fn publishes_route_and_schema_metadata() {
680        let application = Router::new(
681            Config::new(),
682            (
683                "/items/:id"
684                    .GET(item)
685                    .summary("Read an item")
686                    .description("Reads one item by ID")
687                    .tag("items")
688                    .operation_id("readItem")
689                    .openapi(|operation| {
690                        operation.response_header(
691                            200,
692                            "X-Request-Id",
693                            "Request identifier",
694                            crate::SchemaMetadata::new(crate::SchemaKind::String).format("uuid"),
695                        );
696                    }),
697                "/items".POST(create),
698            ),
699        )
700        .openapi("/docs", OpenApi::new("Items", "1.0"));
701        let document = application.openapi_document().unwrap().as_str();
702
703        assert!(document.contains("\"/items/{id}\""));
704        assert!(document.contains("\"name\":\"id\",\"in\":\"path\""));
705        assert!(document.contains("\"name\":\"query\",\"in\":\"query\""));
706        assert!(document.contains("application/x-www-form-urlencoded"));
707        assert!(document.contains("\"413\""));
708        assert!(document.contains("\"summary\":\"Read an item\""));
709        assert!(document.contains("\"operationId\":\"readItem\""));
710        assert!(document.contains("\"X-Request-Id\""));
711
712        #[cfg(feature = "json")]
713        serde_json::from_str::<serde_json::Value>(document).unwrap();
714    }
715
716    #[cfg(feature = "json")]
717    #[test]
718    fn json_case_only_changes_the_configured_router_path() {
719        let unchanged = Router::new(Config::new(), "/item".GET(json_case_item));
720        let response = block_on(unchanged.handle(request("GET", "/item")));
721        assert_eq!(
722            response.body(),
723            br#"{"display_name":"Ada","external_id":7,"child_record":{"child_value":9}}"#,
724        );
725
726        let camel = Router::new(
727            Config::new().json_case(crate::Case::Camel),
728            ("/item".GET(json_case_item), "/item".POST(echo_json_case)),
729        )
730        .openapi("/docs", OpenApi::new("JSON case", "1.0"));
731
732        let response = block_on(camel.handle(request("GET", "/item")));
733        assert_eq!(
734            response.body(),
735            br#"{"displayName":"Ada","external_id":7,"childRecord":{"childValue":9}}"#,
736        );
737
738        let response = block_on(camel.handle(json_request(
739            "/item",
740            br#"{"displayName":"Grace","external_id":11,"childRecord":{"childValue":13}}"#,
741        )));
742        assert_eq!(response.status(), 200);
743        assert_eq!(
744            response.body(),
745            br#"{"displayName":"Grace","external_id":11,"childRecord":{"childValue":13}}"#,
746        );
747
748        let document = camel.openapi_document().unwrap().as_str();
749        assert!(document.contains("\"displayName\""));
750        assert!(document.contains("\"external_id\""));
751        assert!(document.contains("\"childRecord\""));
752        assert!(document.contains("\"childValue\""));
753        assert!(!document.contains("\"display_name\""));
754
755        let serde_named = Router::new(
756            Config::new().json_case(crate::Case::Snake),
757            "/serde".GET(serde_named_item),
758        )
759        .openapi("/docs", OpenApi::new("Serde names", "1.0"));
760        let response = block_on(serde_named.handle(request("GET", "/serde")));
761        assert_eq!(response.body(), br#"{"fixedBySerde":true}"#);
762        let document = serde_named.openapi_document().unwrap().as_str();
763        assert!(document.contains("\"fixedBySerde\""));
764        assert!(!document.contains("\"fixed_by_serde\""));
765    }
766
767    #[test]
768    fn applies_one_custom_error_format_and_preserves_http_semantics() {
769        let application = Router::new(
770            Config::new().error_format(|error: &Error| {
771                Response::text(200, format!("{}:{}", error.code(), error.message()))
772            }),
773            "/failure".GET(failure),
774        )
775        .middleware(AddErrorHeader);
776        let mut response = block_on(application.handle(request("GET", "/failure")));
777
778        assert_eq!(response.status(), 409);
779        assert_eq!(response.content_type(), Some("text/plain; charset=utf-8"));
780        assert_eq!(
781            response.headers().get("x-error-scope"),
782            Some(b"middleware".as_slice()),
783        );
784        assert_eq!(
785            response.body(),
786            b"sample.failed:The sample operation failed",
787        );
788    }
789
790    #[test]
791    fn lets_a_custom_format_hide_internal_error_messages() {
792        let application = Router::new(
793            Config::new().error_format(|error: &Error| {
794                let message = if error.is_internal() {
795                    "Internal Server Error"
796                } else {
797                    error.message()
798                };
799
800                Response::text(error.status(), message)
801            }),
802            "/failure".GET(internal_failure),
803        );
804        let response = block_on(application.handle(request("GET", "/failure")));
805
806        assert_eq!(response.status(), 500);
807        assert_eq!(response.body(), b"Internal Server Error");
808    }
809
810    #[test]
811    fn normalizes_raw_error_responses_with_the_default_json_format() {
812        let application = Router::new(Config::new(), "/failure".GET(raw_error));
813        let response = block_on(application.handle(request("GET", "/failure")));
814
815        assert_eq!(response.status(), 418);
816        assert_eq!(response.content_type(), Some("application/json"));
817        assert_eq!(
818            response.body(),
819            br#"{"error":{"code":"http.418","message":"custom raw error","fields":[]}}"#,
820        );
821    }
822
823    #[test]
824    fn suppresses_a_formatted_error_body_for_head_requests() {
825        let application = Router::new(Config::new(), "/failure".GET(failure));
826        let get = block_on(application.handle(request("GET", "/failure")));
827        let expected_length = get.body().len().to_string();
828        let mut head = block_on(application.handle(request("HEAD", "/failure")));
829
830        assert_eq!(head.status(), 409);
831        assert_eq!(
832            head.headers().get("content-length"),
833            Some(expected_length.as_bytes()),
834        );
835        assert!(head.body().is_empty());
836    }
837
838    #[test]
839    fn suppresses_a_not_found_body_for_head_requests() {
840        let application = Router::new(Config::new(), "/ok".GET(|| async { "ok" }));
841        let get = block_on(application.handle(request("GET", "/missing")));
842        let expected_length = get.body().len().to_string();
843        let mut head = block_on(application.handle(request("HEAD", "/missing")));
844
845        assert_eq!(head.status(), 404);
846        assert_eq!(head.content_type(), Some("application/json"));
847        assert_eq!(
848            head.headers().get("content-length"),
849            Some(expected_length.as_bytes()),
850        );
851        assert!(head.body().is_empty());
852    }
853
854    #[test]
855    fn the_parent_router_controls_nested_error_formatting() {
856        let child = Router::new(
857            Config::new().error_format(|error: &Error| {
858                Response::text(200, format!("child:{}", error.code()))
859            }),
860            "/failure".GET(failure),
861        )
862        .at("/child");
863        let application = Router::new(
864            Config::new().error_format(|error: &Error| {
865                Response::text(200, format!("parent:{}", error.code()))
866            }),
867            child,
868        );
869        let response = block_on(application.handle(request("GET", "/child/failure")));
870
871        assert_eq!(response.status(), 409);
872        assert_eq!(response.body(), b"parent:sample.failed");
873    }
874
875    #[test]
876    fn omits_methods_without_openapi_operation_fields() {
877        let propfind = Method::from_bytes(b"PROPFIND").unwrap();
878        let application = Router::new(
879            Config::new(),
880            (
881                "/visible".GET(|| async { "visible" }),
882                "/tunnel".CONNECT(|| async { "tunnel" }),
883                "/properties".on(propfind, || async { "properties" }),
884            ),
885        )
886        .openapi("/docs", OpenApi::new("Methods", "1.0"));
887        let document = application.openapi_document().unwrap().as_str();
888
889        assert!(document.contains("\"/visible\""));
890        assert!(!document.contains("\"/tunnel\""));
891        assert!(!document.contains("\"/properties\""));
892    }
893
894    #[test]
895    fn serves_the_openapi_reference_with_head_and_method_handling() {
896        let application =
897            Router::new(Config::new(), ()).openapi("/docs", OpenApi::new("Empty", "1.0"));
898        let response = block_on(application.handle(request("GET", "/docs")));
899
900        assert_eq!(response.status(), 200);
901        assert_eq!(response.content_type(), Some("text/html; charset=utf-8"));
902        assert!(!response.body().is_empty());
903
904        let response = block_on(application.handle(request("HEAD", "/docs")));
905        assert_eq!(response.status(), 200);
906        assert!(response.body().is_empty());
907
908        let mut response = block_on(application.handle(request("POST", "/docs")));
909        assert_eq!(response.status(), 405);
910        assert_eq!(
911            response.headers().get("allow"),
912            Some(b"GET, HEAD, OPTIONS".as_slice()),
913        );
914    }
915
916    #[test]
917    fn serves_a_scalar_reference_for_the_openapi_document() {
918        let application = Router::new(Config::new(), "/items/:id".GET(item))
919            .openapi("/reference", OpenApi::new("Items & API", "1.0"));
920        let response = block_on(application.handle(request("GET", "/reference")));
921        let body = std::str::from_utf8(response.body()).unwrap();
922
923        assert_eq!(response.status(), 200);
924        assert_eq!(response.content_type(), Some("text/html; charset=utf-8"));
925        assert!(body.contains("<title>Items &amp; API</title>"));
926        assert!(body.contains("Scalar.createApiReference('#app',{content:\"{"));
927        assert!(body.contains("\\\"openapi\\\":\\\"3.1.0\\\""));
928        assert!(body.contains("/items/{id}"));
929        assert!(!body.contains("Scalar.createApiReference('#app',{url:"));
930        assert!(body.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
931    }
932
933    #[test]
934    fn scopes_openapi_routes_and_reference_with_config_and_mount_prefixes() {
935        let router = Router::new(Config::new().prefix("/v1"), "/items/:id".GET(item))
936            .openapi("/docs", OpenApi::new("Items", "1.0"))
937            .at("/service");
938        let document = router.openapi_document().unwrap().as_str();
939
940        assert!(document.contains("\"/service/v1/items/{id}\""));
941        assert_eq!(
942            block_on(router.handle(request("GET", "/service/v1/docs"))).status(),
943            200,
944        );
945        assert_eq!(
946            block_on(router.handle(request("GET", "/v1/service/docs"))).status(),
947            404,
948        );
949    }
950
951    #[cfg(feature = "json")]
952    #[test]
953    fn derives_json_request_and_response_components_from_schema() {
954        let application = Router::new(Config::new(), "/items".POST(create_json))
955            .openapi("/docs", OpenApi::new("Items", "1.0"));
956        let document = application.openapi_document().unwrap().as_str();
957
958        assert!(document.contains("\"application/json\":{\"schema\":{\"$ref\":"));
959        assert!(document.contains("\"components\":{\"schemas\":"));
960        serde_json::from_str::<serde_json::Value>(document).unwrap();
961    }
962}