Skip to main content

salvo_oapi/
rapidoc.rs

1//! This crate implements necessary boiler plate code to serve RapiDoc via web server. It
2//! works as a bridge for serving the OpenAPI documentation created with [`salvo`][salvo] library in the
3//! RapiDoc.
4//!
5//! [salvo]: <https://docs.rs/salvo/>
6//!
7use std::borrow::Cow;
8
9use salvo_core::writing::Text;
10use salvo_core::{async_trait, Depot, FlowCtrl, Handler, Request, Response, Router};
11
12use crate::html::{description_meta, escape_html, keywords_meta};
13
14const INDEX_TMPL: &str = r#"
15<!doctype html>
16<html>
17  <head>
18    <title>{{title}}</title>
19    {{keywords}}
20    {{description}}
21    <meta charset="utf-8">
22    <script type="module" src="{{lib_url}}"></script>
23  </head>
24  <body>
25    <rapi-doc spec-url="{{spec_url}}"></rapi-doc>
26  </body>
27</html>
28"#;
29
30/// Implements [`Handler`] for serving RapiDoc.
31#[non_exhaustive]
32#[derive(Clone, Debug)]
33pub struct RapiDoc {
34    /// The title of the html page. The default title is "RapiDoc".
35    pub title: Cow<'static, str>,
36    /// The version of the html page.
37    pub keywords: Option<Cow<'static, str>>,
38    /// The description of the html page.
39    pub description: Option<Cow<'static, str>>,
40    /// The lib url path.
41    pub lib_url: Cow<'static, str>,
42    /// The spec url path.
43    pub spec_url: Cow<'static, str>,
44}
45impl RapiDoc {
46    /// Create a new [`RapiDoc`] for given path.
47    ///
48    /// Path argument will expose the RapiDoc to the user and should be something that
49    /// the underlying application framework / library supports.
50    ///
51    /// # Examples
52    ///
53    /// ```rust
54    /// # use salvo_oapi::rapidoc::RapiDoc;
55    /// let doc = RapiDoc::new("/openapi.json");
56    /// ```
57    #[must_use]
58    pub fn new(spec_url: impl Into<Cow<'static, str>>) -> Self {
59        Self {
60            title: "RapiDoc".into(),
61            keywords: None,
62            description: None,
63            lib_url: "https://unpkg.com/rapidoc/dist/rapidoc-min.js".into(),
64            spec_url: spec_url.into(),
65        }
66    }
67
68    /// Set title of the html page. The default title is "RapiDoc".
69    #[must_use]
70    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
71        self.title = title.into();
72        self
73    }
74
75    /// Set keywords of the html page.
76    #[must_use]
77    pub fn keywords(mut self, keywords: impl Into<Cow<'static, str>>) -> Self {
78        self.keywords = Some(keywords.into());
79        self
80    }
81
82    /// Set description of the html page.
83    #[must_use]
84    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
85        self.description = Some(description.into());
86        self
87    }
88
89    /// Set the lib url path.
90    #[must_use]
91    pub fn lib_url(mut self, lib_url: impl Into<Cow<'static, str>>) -> Self {
92        self.lib_url = lib_url.into();
93        self
94    }
95
96    /// Consumes the [`RapiDoc`] and returns [`Router`] with the [`RapiDoc`] as handler.
97    pub fn into_router(self, path: impl Into<String>) -> Router {
98        Router::with_path(path.into()).goal(self)
99    }
100}
101
102#[async_trait]
103impl Handler for RapiDoc {
104    async fn handle(&self, _req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) {
105        let keywords = self
106            .keywords
107            .as_ref()
108            .map(|s| keywords_meta(s))
109            .unwrap_or_default();
110        let description = self
111            .description
112            .as_ref()
113            .map(|s| description_meta(s))
114            .unwrap_or_default();
115        let html = INDEX_TMPL
116            .replacen("{{spec_url}}", &escape_html(&self.spec_url), 1)
117            .replacen("{{lib_url}}", &escape_html(&self.lib_url), 1)
118            .replacen("{{description}}", &description, 1)
119            .replacen("{{keywords}}", &keywords, 1)
120            .replacen("{{title}}", &escape_html(&self.title), 1);
121        res.render(Text::Html(html));
122    }
123}