salvo_oapi/redoc/
mod.rs

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