mcaptcha/
docs.rs

1// Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
2// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
3//
4// SPDX-License-Identifier: AGPL-3.0-or-later
5
6use std::borrow::Cow;
7
8use actix_web::body::BoxBody;
9use actix_web::{http::header, web, HttpResponse, Responder};
10use mime_guess::from_path;
11use rust_embed::RustEmbed;
12
13use crate::CACHE_AGE;
14
15pub const DOCS: routes::Docs = routes::Docs::new();
16
17pub mod routes {
18    pub struct Docs {
19        pub home: &'static str,
20        pub spec: &'static str,
21        pub assets: &'static str,
22    }
23
24    impl Docs {
25        pub const fn new() -> Self {
26            Docs {
27                home: "/docs/",
28                spec: "/docs/openapi.yaml",
29                assets: "/docs/{_:.*}",
30            }
31        }
32    }
33}
34
35pub fn services(cfg: &mut web::ServiceConfig) {
36    cfg.service(index).service(spec).service(dist);
37}
38
39#[derive(RustEmbed)]
40#[folder = "static/openapi/"]
41struct Asset;
42
43pub fn handle_embedded_file(path: &str) -> HttpResponse {
44    match Asset::get(path) {
45        Some(content) => {
46            let body: BoxBody = match content.data {
47                Cow::Borrowed(bytes) => BoxBody::new(bytes),
48                Cow::Owned(bytes) => BoxBody::new(bytes),
49            };
50
51            HttpResponse::Ok()
52                .insert_header(header::CacheControl(vec![
53                    header::CacheDirective::Public,
54                    header::CacheDirective::Extension("immutable".into(), None),
55                    header::CacheDirective::MaxAge(CACHE_AGE),
56                ]))
57                .content_type(from_path(path).first_or_octet_stream().as_ref())
58                .body(body)
59        }
60        None => HttpResponse::NotFound().body("404 Not Found"),
61    }
62}
63
64#[my_codegen::get(path = "DOCS.assets")]
65async fn dist(path: web::Path<String>) -> impl Responder {
66    handle_embedded_file(&path)
67}
68const OPEN_API_SPEC: &str = include_str!("../docs/openapi/dist/openapi.yaml");
69
70#[my_codegen::get(path = "DOCS.spec")]
71async fn spec() -> HttpResponse {
72    HttpResponse::Ok()
73        .content_type("text/yaml")
74        .body(OPEN_API_SPEC)
75}
76
77#[my_codegen::get(path = "&DOCS.home[0..DOCS.home.len() -1]")]
78async fn index() -> HttpResponse {
79    handle_embedded_file("index.html")
80}
81
82#[cfg(test)]
83mod tests {
84    use actix_web::http::StatusCode;
85    use actix_web::test;
86
87    use super::*;
88    use crate::*;
89
90    #[actix_rt::test]
91    async fn docs_works() {
92        const FILE: &str = "favicon-32x32.png";
93
94        let app = test::init_service(
95            App::new()
96                .wrap(actix_middleware::NormalizePath::new(
97                    actix_middleware::TrailingSlash::Trim,
98                ))
99                .configure(services),
100        )
101        .await;
102
103        let resp = test::call_service(
104            &app,
105            test::TestRequest::get().uri(DOCS.home).to_request(),
106        )
107        .await;
108        assert_eq!(resp.status(), StatusCode::OK);
109
110        let resp = test::call_service(
111            &app,
112            test::TestRequest::get().uri(DOCS.spec).to_request(),
113        )
114        .await;
115        assert_eq!(resp.status(), StatusCode::OK);
116
117        let uri = format!("{}{}", DOCS.home, FILE);
118
119        let resp =
120            test::call_service(&app, test::TestRequest::get().uri(&uri).to_request())
121                .await;
122        assert_eq!(resp.status(), StatusCode::OK);
123    }
124}