1use std::borrow::Cow;
8
9mod config;
10pub mod oauth;
11pub use config::Config;
12pub use oauth::Config as OauthConfig;
13use rust_embed::RustEmbed;
14use salvo_core::http::uri::{Parts as UriParts, Uri};
15use salvo_core::http::{header, HeaderValue, ResBody, StatusError};
16use salvo_core::writing::Redirect;
17use salvo_core::{async_trait, Depot, Error, FlowCtrl, Handler, Request, Response, Router};
18use serde::Serialize;
19
20#[derive(RustEmbed)]
21#[folder = "src/swagger_ui/v5.18.3"]
22struct SwaggerUiDist;
23
24const INDEX_TMPL: &str = r#"
25<!DOCTYPE html>
26<html charset="UTF-8">
27 <head>
28 <meta charset="UTF-8">
29 <title>{{title}}</title>
30 {{keywords}}
31 {{description}}
32 <link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
33 <style>
34 html {
35 box-sizing: border-box;
36 overflow: -moz-scrollbars-vertical;
37 overflow-y: scroll;
38 }
39 *,
40 *:before,
41 *:after {
42 box-sizing: inherit;
43 }
44 body {
45 margin: 0;
46 background: #fafafa;
47 }
48 </style>
49 </head>
50
51 <body>
52 <div id="swagger-ui"></div>
53 <script src="./swagger-ui-bundle.js" charset="UTF-8"></script>
54 <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"></script>
55 <script>
56 window.onload = function() {
57 let config = {
58 dom_id: '#swagger-ui',
59 deepLinking: true,
60 presets: [
61 SwaggerUIBundle.presets.apis,
62 SwaggerUIStandalonePreset
63 ],
64 plugins: [
65 SwaggerUIBundle.plugins.DownloadUrl
66 ],
67 layout: "StandaloneLayout"
68 };
69 window.ui = SwaggerUIBundle(Object.assign(config, {{config}}));
70 //{{oauth}}
71 };
72 </script>
73 </body>
74</html>
75"#;
76
77#[derive(Clone, Debug)]
79pub struct SwaggerUi {
80 config: Config<'static>,
81 pub title: Cow<'static, str>,
83 pub keywords: Option<Cow<'static, str>>,
85 pub description: Option<Cow<'static, str>>,
87}
88impl SwaggerUi {
89 pub fn new(config: impl Into<Config<'static>>) -> Self {
101 Self {
102 config: config.into(),
103 title: "Swagger UI".into(),
104 keywords: None,
105 description: None,
106 }
107 }
108
109 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
111 self.title = title.into();
112 self
113 }
114
115 pub fn keywords(mut self, keywords: impl Into<Cow<'static, str>>) -> Self {
117 self.keywords = Some(keywords.into());
118 self
119 }
120
121 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
123 self.description = Some(description.into());
124 self
125 }
126
127 pub fn url<U: Into<Url<'static>>>(mut self, url: U) -> Self {
141 self.config.urls.push(url.into());
142 self
143 }
144
145 pub fn urls(mut self, urls: Vec<Url<'static>>) -> Self {
168 self.config.urls = urls;
169 self
170 }
171
172 pub fn oauth(mut self, oauth: oauth::Config) -> Self {
192 self.config.oauth = Some(oauth);
193 self
194 }
195
196 pub fn into_router(self, path: impl Into<String>) -> Router {
198 Router::with_path(format!("{}/{{**}}", path.into())).goal(self)
199 }
200}
201
202#[inline]
203pub(crate) fn redirect_to_dir_url(req_uri: &Uri, res: &mut Response) {
204 let UriParts {
205 scheme,
206 authority,
207 path_and_query,
208 ..
209 } = req_uri.clone().into_parts();
210 let mut builder = Uri::builder();
211 if let Some(scheme) = scheme {
212 builder = builder.scheme(scheme);
213 }
214 if let Some(authority) = authority {
215 builder = builder.authority(authority);
216 }
217 if let Some(path_and_query) = path_and_query {
218 if let Some(query) = path_and_query.query() {
219 builder = builder.path_and_query(format!("{}/?{}", path_and_query.path(), query));
220 } else {
221 builder = builder.path_and_query(format!("{}/", path_and_query.path()));
222 }
223 }
224 match builder.build() {
225 Ok(redirect_uri) => res.render(Redirect::found(redirect_uri)),
226 Err(e) => {
227 tracing::error!(error = ?e, "failed to build redirect uri");
228 res.render(StatusError::internal_server_error());
229 }
230 }
231}
232
233#[async_trait]
234impl Handler for SwaggerUi {
235 async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) {
236 let path = req.params().tail().unwrap_or_default();
237 if path.is_empty() && !req.uri().path().ends_with('/') {
239 redirect_to_dir_url(req.uri(), res);
240 return;
241 }
242
243 let keywords = self
244 .keywords
245 .as_ref()
246 .map(|s| {
247 format!(
248 "<meta name=\"keywords\" content=\"{}\">",
249 s.split(',').map(|s| s.trim()).collect::<Vec<_>>().join(",")
250 )
251 })
252 .unwrap_or_default();
253 let description = self
254 .description
255 .as_ref()
256 .map(|s| format!("<meta name=\"description\" content=\"{}\">", s))
257 .unwrap_or_default();
258 match serve(path, &self.title, &keywords, &description, &self.config) {
259 Ok(Some(file)) => {
260 res.headers_mut()
261 .insert(header::CONTENT_TYPE, HeaderValue::from_str(&file.content_type).expect("content type parse failed"));
262 res.body(ResBody::Once(file.bytes.to_vec().into()));
263 }
264 Ok(None) => {
265 tracing::warn!(path, "swagger ui file not found");
266 res.render(StatusError::not_found());
267 }
268 Err(e) => {
269 tracing::error!(error = ?e, path, "failed to fetch swagger ui file");
270 res.render(StatusError::internal_server_error());
271 }
272 }
273 }
274}
275
276#[non_exhaustive]
278#[derive(Default, Serialize, Clone, Debug)]
279pub struct Url<'a> {
280 name: Cow<'a, str>,
281 url: Cow<'a, str>,
282 #[serde(skip)]
283 primary: bool,
284}
285
286impl<'a> Url<'a> {
287 pub fn new(name: &'a str, url: &'a str) -> Self {
300 Self {
301 name: Cow::Borrowed(name),
302 url: Cow::Borrowed(url),
303 ..Default::default()
304 }
305 }
306
307 pub fn with_primary(name: &'a str, url: &'a str, primary: bool) -> Self {
325 Self {
326 name: Cow::Borrowed(name),
327 url: Cow::Borrowed(url),
328 primary,
329 }
330 }
331}
332
333impl<'a> From<&'a str> for Url<'a> {
334 fn from(url: &'a str) -> Self {
335 Self {
336 url: Cow::Borrowed(url),
337 ..Default::default()
338 }
339 }
340}
341
342impl From<String> for Url<'_> {
343 fn from(url: String) -> Self {
344 Self {
345 url: Cow::Owned(url),
346 ..Default::default()
347 }
348 }
349}
350
351impl From<Cow<'static, str>> for Url<'_> {
352 fn from(url: Cow<'static, str>) -> Self {
353 Self {
354 url,
355 ..Default::default()
356 }
357 }
358}
359
360#[non_exhaustive]
363pub struct SwaggerFile<'a> {
364 pub bytes: Cow<'a, [u8]>,
366 pub content_type: String,
368}
369
370pub fn serve<'a>(
385 path: &str,
386 title: &str,
387 keywords: &str,
388 description: &str,
389 config: &Config<'a>,
390) -> Result<Option<SwaggerFile<'a>>, Error> {
391 let path = if path.is_empty() || path == "/" {
392 "index.html"
393 } else {
394 path
395 };
396
397 let bytes = if path == "index.html" {
398 let config_json = serde_json::to_string(&config)?;
399
400 let mut index = INDEX_TMPL
402 .replacen("{{config}}", &config_json, 1)
403 .replacen("{{description}}", description, 1)
404 .replacen("{{keywords}}", keywords, 1)
405 .replacen("{{title}}", title, 1);
406
407 if let Some(oauth) = &config.oauth {
408 let oauth_json = serde_json::to_string(oauth)?;
409 index = index.replace("//{{oauth}}", &format!("window.ui.initOAuth({});", &oauth_json));
410 }
411 Some(Cow::Owned(index.as_bytes().to_vec()))
412 } else {
413 SwaggerUiDist::get(path).map(|f| f.data)
414 };
415 let file = bytes.map(|bytes| SwaggerFile {
416 bytes,
417 content_type: mime_infer::from_path(path).first_or_octet_stream().to_string(),
418 });
419
420 Ok(file)
421}