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