1use 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}