Skip to main content

serverkit/
app.rs

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