Skip to main content

salvo_oapi/
scalar.rs

1//! This crate implements necessary boiler plate code to serve Scalar via web server. It
2//! works as a bridge for serving the OpenAPI documentation created with [`salvo`][salvo] library in the
3//! Scalar.
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    <meta name="viewport" content="width=device-width, initial-scale=1">
21    <style>
22      {{style}}
23    </style>
24  </head>
25
26  <body>{{header}}
27    <script id="api-reference" data-url="{{spec_url}}"></script>
28    <script src="{{lib_url}}"></script>
29  </body>
30</html>
31"#;
32
33/// Implements [`Handler`] for serving Scalar.
34#[non_exhaustive]
35#[derive(Clone, Debug)]
36pub struct Scalar {
37    /// The title of the html page. The default title is "Scalar".
38    pub title: Cow<'static, str>,
39    /// The version of the html page.
40    pub keywords: Option<Cow<'static, str>>,
41    /// The description of the html page.
42    pub description: Option<Cow<'static, str>>,
43    /// Custom style for the html page.
44    pub style: Option<Cow<'static, str>>,
45    /// Custom header for the html page.
46    pub header: Option<Cow<'static, str>>,
47    /// The lib url path.
48    pub lib_url: Cow<'static, str>,
49    /// The spec url path.
50    pub spec_url: Cow<'static, str>,
51}
52impl Scalar {
53    /// Create a new [`Scalar`] for given path.
54    ///
55    /// Path argument will expose the Scalar to the user and should be something that
56    /// the underlying application framework / library supports.
57    ///
58    /// # Examples
59    ///
60    /// ```rust
61    /// # use salvo_oapi::scalar::Scalar;
62    /// let doc = Scalar::new("/openapi.json");
63    /// ```
64    #[must_use]
65    pub fn new(spec_url: impl Into<Cow<'static, str>>) -> Self {
66        Self {
67            title: "Scalar".into(),
68            keywords: None,
69            description: None,
70            style: Some(Cow::from(DEFAULT_STYLE)),
71            header: None,
72            lib_url: "https://cdn.jsdelivr.net/npm/@scalar/api-reference".into(),
73            spec_url: spec_url.into(),
74        }
75    }
76
77    /// Set title of the html page. The default title is "Scalar".
78    #[must_use]
79    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
80        self.title = title.into();
81        self
82    }
83
84    /// Set keywords of the html page.
85    #[must_use]
86    pub fn keywords(mut self, keywords: impl Into<Cow<'static, str>>) -> Self {
87        self.keywords = Some(keywords.into());
88        self
89    }
90
91    /// Set description of the html page.
92    #[must_use]
93    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
94        self.description = Some(description.into());
95        self
96    }
97
98    /// Set the lib url path.
99    #[must_use]
100    pub fn lib_url(mut self, lib_url: impl Into<Cow<'static, str>>) -> Self {
101        self.lib_url = lib_url.into();
102        self
103    }
104
105    /// Consusmes the [`Scalar`] and returns [`Router`] with the [`Scalar`] as handler.
106    pub fn into_router(self, path: impl Into<String>) -> Router {
107        Router::with_path(path.into()).goal(self)
108    }
109}
110#[async_trait]
111impl Handler for Scalar {
112    async fn handle(&self, _req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) {
113        let keywords = self
114            .keywords
115            .as_ref()
116            .map(|s| {
117                format!(
118                    "<meta name=\"keywords\" content=\"{}\">",
119                    s.split(',').map(|s| s.trim()).collect::<Vec<_>>().join(",")
120                )
121            })
122            .unwrap_or_default();
123        let description = self
124            .description
125            .as_ref()
126            .map(|s| format!("<meta name=\"description\" content=\"{s}\">"))
127            .unwrap_or_default();
128        let style = self
129            .style
130            .as_ref()
131            .map(|s| format!("<style>{s}</style>"))
132            .unwrap_or_default();
133        let html = INDEX_TMPL
134            .replacen("{{lib_url}}", &self.lib_url, 1)
135            .replacen("{{spec_url}}", &self.spec_url, 1)
136            .replacen("{{header}}", self.header.as_deref().unwrap_or_default(), 1)
137            .replacen("{{style}}", &style, 1)
138            .replacen("{{description}}", &description, 1)
139            .replacen("{{keywords}}", &keywords, 1)
140            .replacen("{{title}}", &self.title, 1);
141        res.render(Text::Html(html));
142    }
143}
144
145const DEFAULT_STYLE: &str = r#":root {
146    --theme-font: 'Inter', var(--system-fonts);
147  }
148  /* basic theme */
149  .light-mode {
150    --theme-color-1: #2c3d50;
151    --theme-color-2: #38495c;
152    --theme-color-3: #445569;
153    --theme-color-accent: #3faf7c;
154
155    --theme-background-1: #fff;
156    --theme-background-2: #f6f6f6;
157    --theme-background-3: #e7e7e7;
158    --theme-background-accent: #8ab4f81f;
159
160    --theme-border-color: rgba(0, 0, 0, 0.1);
161  }
162  .dark-mode {
163    --theme-color-1: rgb(150, 167, 183, 1);
164    --theme-color-2: rgba(150, 167, 183, 0.72);
165    --theme-color-3: rgba(150, 167, 183, 0.54);
166    --theme-color-accent: #329066;
167
168    --theme-background-1: #22272e;
169    --theme-background-2: #282c34;
170    --theme-background-3: #343841;
171    --theme-background-accent: #3290661f;
172
173    --theme-border-color: rgba(255, 255, 255, 0.1);
174  }
175  /* Document header */
176  .light-mode .t-doc__header {
177    --header-background-1: var(--theme-background-1);
178    --header-border-color: var(--theme-border-color);
179    --header-color-1: var(--theme-color-1);
180    --header-color-2: var(--theme-color-2);
181    --header-background-toggle: var(--theme-color-3);
182    --header-call-to-action-color: var(--theme-color-accent);
183  }
184
185  .dark-mode .t-doc__header {
186    --header-background-1: var(--theme-background-1);
187    --header-border-color: var(--theme-border-color);
188    --header-color-1: var(--theme-color-1);
189    --header-color-2: var(--theme-color-2);
190    --header-background-toggle: var(--theme-color-3);
191    --header-call-to-action-color: var(--theme-color-accent);
192  }
193  /* Document Sidebar */
194  .light-mode .t-doc__sidebar,
195  .dark-mode .t-doc__sidebar {
196    --sidebar-background-1: var(--theme-background-1);
197    --sidebar-item-hover-color: var(--theme-color-accent);
198    --sidebar-item-hover-background: transparent;
199    --sidebar-item-active-background: transparent;
200    --sidebar-border-color: var(--theme-border-color);
201    --sidebar-color-1: var(--theme-color-1);
202    --sidebar-color-2: var(--theme-color-2);
203    --sidebar-color-active: var(--theme-color-accent);
204    --sidebar-search-background: transparent;
205    --sidebar-search-border-color: var(--theme-border-color);
206    --sidebar-search--color: var(--theme-color-3);
207  }
208  .light-mode .t-doc__sidebar .active_page.sidebar-heading,
209  .dark-mode .t-doc__sidebar .active_page.sidebar-heading {
210    background: transparent !important;
211    box-shadow: inset 3px 0 0 var(--theme-color-accent);
212  }
213
214  /* advanced */
215  .light-mode {
216    --theme-button-1: rgb(49 53 56);
217    --theme-button-1-color: #fff;
218    --theme-button-1-hover: rgb(28 31 33);
219
220    --theme-color-green: #069061;
221    --theme-color-red: #ef0006;
222    --theme-color-yellow: #edbe20;
223    --theme-color-blue: #0082d0;
224    --theme-color-orange: #fb892c;
225    --theme-color-purple: #5203d1;
226
227    --theme-scrollbar-color: rgba(0, 0, 0, 0.18);
228    --theme-scrollbar-color-active: rgba(0, 0, 0, 0.36);
229  }
230  .dark-mode {
231    --theme-button-1: #f6f6f6;
232    --theme-button-1-color: #000;
233    --theme-button-1-hover: #e7e7e7;
234
235    --theme-color-green: #00b648;
236    --theme-color-red: #dc1b19;
237    --theme-color-yellow: #ffc90d;
238    --theme-color-blue: #4eb3ec;
239    --theme-color-orange: #ff8d4d;
240    --theme-color-purple: #b191f9;
241
242    --theme-scrollbar-color: var(--theme-color-accent);
243    --theme-scrollbar-color-active: var(--theme-color-accent);
244  }
245  body {margin: 0; padding: 0;}
246  .dark-mode .show-api-client-button span,
247  .light-mode .show-api-client-button span,
248  .light-mode .show-api-client-button svg,
249  .dark-mode .show-api-client-button svg {
250    color: white;
251  }
252  .t-doc__header .header-item-logo {
253    height: 32px;
254  }
255"#;