Skip to main content

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