salvo_oapi/scalar/
mod.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    pub fn new(spec_url: impl Into<Cow<'static, str>>) -> Self {
65        Self {
66            title: "Scalar".into(),
67            keywords: None,
68            description: None,
69            style: Some(Cow::from(DEFAULT_STYLE)),
70            header: None,
71            lib_url: "https://cdn.jsdelivr.net/npm/@scalar/api-reference".into(),
72            spec_url: spec_url.into(),
73        }
74    }
75
76    /// Set title of the html page. The default title is "Scalar".
77    pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
78        self.title = title.into();
79        self
80    }
81
82    /// Set keywords of the html page.
83    pub fn keywords(mut self, keywords: impl Into<Cow<'static, str>>) -> Self {
84        self.keywords = Some(keywords.into());
85        self
86    }
87
88    /// Set description of the html page.
89    pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
90        self.description = Some(description.into());
91        self
92    }
93
94    /// Set the lib url path.
95    pub fn lib_url(mut self, lib_url: impl Into<Cow<'static, str>>) -> Self {
96        self.lib_url = lib_url.into();
97        self
98    }
99
100    /// Consusmes the [`Scalar`] and returns [`Router`] with the [`Scalar`] as handler.
101    pub fn into_router(self, path: impl Into<String>) -> Router {
102        Router::with_path(path.into()).goal(self)
103    }
104}
105#[async_trait]
106impl Handler for Scalar {
107    async fn handle(&self, _req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) {
108        let keywords = self
109            .keywords
110            .as_ref()
111            .map(|s| {
112                format!(
113                    "<meta name=\"keywords\" content=\"{}\">",
114                    s.split(',').map(|s| s.trim()).collect::<Vec<_>>().join(",")
115                )
116            })
117            .unwrap_or_default();
118        let description = self
119            .description
120            .as_ref()
121            .map(|s| format!("<meta name=\"description\" content=\"{}\">", s))
122            .unwrap_or_default();
123        let style = self
124            .style
125            .as_ref()
126            .map(|s| format!("<style>{}</style>", s))
127            .unwrap_or_default();
128        let html = INDEX_TMPL
129            .replacen("{{lib_url}}", &self.lib_url, 1)
130            .replacen("{{spec_url}}", &self.spec_url, 1)
131            .replacen("{{header}}", self.header.as_deref().unwrap_or_default(), 1)
132            .replacen("{{style}}", &style, 1)
133            .replacen("{{description}}", &description, 1)
134            .replacen("{{keywords}}", &keywords, 1)
135            .replacen("{{title}}", &self.title, 1);
136        res.render(Text::Html(html));
137    }
138}
139
140const DEFAULT_STYLE: &str = r#":root {
141    --theme-font: 'Inter', var(--system-fonts);
142  }
143  /* basic theme */
144  .light-mode {
145    --theme-color-1: #2c3d50;
146    --theme-color-2: #38495c;
147    --theme-color-3: #445569;
148    --theme-color-accent: #3faf7c;
149
150    --theme-background-1: #fff;
151    --theme-background-2: #f6f6f6;
152    --theme-background-3: #e7e7e7;
153    --theme-background-accent: #8ab4f81f;
154
155    --theme-border-color: rgba(0, 0, 0, 0.1);
156  }
157  .dark-mode {
158    --theme-color-1: rgb(150, 167, 183, 1);
159    --theme-color-2: rgba(150, 167, 183, 0.72);
160    --theme-color-3: rgba(150, 167, 183, 0.54);
161    --theme-color-accent: #329066;
162
163    --theme-background-1: #22272e;
164    --theme-background-2: #282c34;
165    --theme-background-3: #343841;
166    --theme-background-accent: #3290661f;
167
168    --theme-border-color: rgba(255, 255, 255, 0.1);
169  }
170  /* Document header */
171  .light-mode .t-doc__header {
172    --header-background-1: var(--theme-background-1);
173    --header-border-color: var(--theme-border-color);
174    --header-color-1: var(--theme-color-1);
175    --header-color-2: var(--theme-color-2);
176    --header-background-toggle: var(--theme-color-3);
177    --header-call-to-action-color: var(--theme-color-accent);
178  }
179
180  .dark-mode .t-doc__header {
181    --header-background-1: var(--theme-background-1);
182    --header-border-color: var(--theme-border-color);
183    --header-color-1: var(--theme-color-1);
184    --header-color-2: var(--theme-color-2);
185    --header-background-toggle: var(--theme-color-3);
186    --header-call-to-action-color: var(--theme-color-accent);
187  }
188  /* Document Sidebar */
189  .light-mode .t-doc__sidebar,
190  .dark-mode .t-doc__sidebar {
191    --sidebar-background-1: var(--theme-background-1);
192    --sidebar-item-hover-color: var(--theme-color-accent);
193    --sidebar-item-hover-background: transparent;
194    --sidebar-item-active-background: transparent;
195    --sidebar-border-color: var(--theme-border-color);
196    --sidebar-color-1: var(--theme-color-1);
197    --sidebar-color-2: var(--theme-color-2);
198    --sidebar-color-active: var(--theme-color-accent);
199    --sidebar-search-background: transparent;
200    --sidebar-search-border-color: var(--theme-border-color);
201    --sidebar-search--color: var(--theme-color-3);
202  }
203  .light-mode .t-doc__sidebar .active_page.sidebar-heading,
204  .dark-mode .t-doc__sidebar .active_page.sidebar-heading {
205    background: transparent !important;
206    box-shadow: inset 3px 0 0 var(--theme-color-accent);
207  }
208
209  /* advanced */
210  .light-mode {
211    --theme-button-1: rgb(49 53 56);
212    --theme-button-1-color: #fff;
213    --theme-button-1-hover: rgb(28 31 33);
214
215    --theme-color-green: #069061;
216    --theme-color-red: #ef0006;
217    --theme-color-yellow: #edbe20;
218    --theme-color-blue: #0082d0;
219    --theme-color-orange: #fb892c;
220    --theme-color-purple: #5203d1;
221
222    --theme-scrollbar-color: rgba(0, 0, 0, 0.18);
223    --theme-scrollbar-color-active: rgba(0, 0, 0, 0.36);
224  }
225  .dark-mode {
226    --theme-button-1: #f6f6f6;
227    --theme-button-1-color: #000;
228    --theme-button-1-hover: #e7e7e7;
229
230    --theme-color-green: #00b648;
231    --theme-color-red: #dc1b19;
232    --theme-color-yellow: #ffc90d;
233    --theme-color-blue: #4eb3ec;
234    --theme-color-orange: #ff8d4d;
235    --theme-color-purple: #b191f9;
236
237    --theme-scrollbar-color: var(--theme-color-accent);
238    --theme-scrollbar-color-active: var(--theme-color-accent);
239  }
240  body {margin: 0; padding: 0;}
241  .dark-mode .show-api-client-button span,
242  .light-mode .show-api-client-button span,
243  .light-mode .show-api-client-button svg,
244  .dark-mode .show-api-client-button svg {
245    color: white;
246  }
247  .t-doc__header .header-item-logo {
248    height: 32px;
249  }
250"#;