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::{HeaderValue, ResBody, StatusError, header};
16use salvo_core::writing::Redirect;
17use salvo_core::{Depot, Error, FlowCtrl, Handler, Request, Response, Router, async_trait};
18use serde::Serialize;
19
20#[derive(RustEmbed)]
21#[folder = "src/swagger_ui/v5.29.0"]
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 #[must_use]
111 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
112 self.title = title.into();
113 self
114 }
115
116 #[must_use]
118 pub fn keywords(mut self, keywords: impl Into<Cow<'static, str>>) -> Self {
119 self.keywords = Some(keywords.into());
120 self
121 }
122
123 #[must_use]
125 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
126 self.description = Some(description.into());
127 self
128 }
129
130 #[must_use]
144 pub fn url<U: Into<Url<'static>>>(mut self, url: U) -> Self {
145 self.config.urls.push(url.into());
146 self
147 }
148
149 #[must_use]
172 pub fn urls(mut self, urls: Vec<Url<'static>>) -> Self {
173 self.config.urls = urls;
174 self
175 }
176
177 #[must_use]
197 pub fn oauth(mut self, oauth: oauth::Config) -> Self {
198 self.config.oauth = Some(oauth);
199 self
200 }
201
202 pub fn into_router(self, path: impl Into<String>) -> Router {
204 Router::with_path(format!("{}/{{**}}", path.into())).goal(self)
205 }
206}
207
208#[inline]
209pub(crate) fn redirect_to_dir_url(req_uri: &Uri, res: &mut Response) {
210 let UriParts {
211 scheme,
212 authority,
213 path_and_query,
214 ..
215 } = req_uri.clone().into_parts();
216 let mut builder = Uri::builder();
217 if let Some(scheme) = scheme {
218 builder = builder.scheme(scheme);
219 }
220 if let Some(authority) = authority {
221 builder = builder.authority(authority);
222 }
223 if let Some(path_and_query) = path_and_query {
224 if let Some(query) = path_and_query.query() {
225 builder = builder.path_and_query(format!("{}/?{}", path_and_query.path(), query));
226 } else {
227 builder = builder.path_and_query(format!("{}/", path_and_query.path()));
228 }
229 }
230 match builder.build() {
231 Ok(redirect_uri) => res.render(Redirect::found(redirect_uri)),
232 Err(e) => {
233 tracing::error!(error = ?e, "failed to build redirect uri");
234 res.render(StatusError::internal_server_error());
235 }
236 }
237}
238
239#[async_trait]
240impl Handler for SwaggerUi {
241 async fn handle(
242 &self,
243 req: &mut Request,
244 _depot: &mut Depot,
245 res: &mut Response,
246 _ctrl: &mut FlowCtrl,
247 ) {
248 let path = req.params().tail().unwrap_or_default();
249 if path.is_empty() && !req.uri().path().ends_with('/') {
251 redirect_to_dir_url(req.uri(), res);
252 return;
253 }
254
255 let keywords = self
256 .keywords
257 .as_ref()
258 .map(|s| {
259 format!(
260 "<meta name=\"keywords\" content=\"{}\">",
261 s.split(',').map(|s| s.trim()).collect::<Vec<_>>().join(",")
262 )
263 })
264 .unwrap_or_default();
265 let description = self
266 .description
267 .as_ref()
268 .map(|s| format!("<meta name=\"description\" content=\"{s}\">"))
269 .unwrap_or_default();
270 match serve(path, &self.title, &keywords, &description, &self.config) {
271 Ok(Some(file)) => {
272 res.headers_mut().insert(
273 header::CONTENT_TYPE,
274 HeaderValue::from_str(&file.content_type).expect("content type parse failed"),
275 );
276 res.body(ResBody::Once(file.bytes.to_vec().into()));
277 }
278 Ok(None) => {
279 tracing::warn!(path, "swagger ui file not found");
280 res.render(StatusError::not_found());
281 }
282 Err(e) => {
283 tracing::error!(error = ?e, path, "failed to fetch swagger ui file");
284 res.render(StatusError::internal_server_error());
285 }
286 }
287 }
288}
289
290#[non_exhaustive]
292#[derive(Default, Serialize, Clone, Debug)]
293pub struct Url<'a> {
294 name: Cow<'a, str>,
295 url: Cow<'a, str>,
296 #[serde(skip)]
297 primary: bool,
298}
299
300impl<'a> Url<'a> {
301 #[must_use]
314 pub fn new(name: &'a str, url: &'a str) -> Self {
315 Self {
316 name: Cow::Borrowed(name),
317 url: Cow::Borrowed(url),
318 ..Default::default()
319 }
320 }
321
322 #[must_use]
340 pub fn with_primary(name: &'a str, url: &'a str, primary: bool) -> Self {
341 Self {
342 name: Cow::Borrowed(name),
343 url: Cow::Borrowed(url),
344 primary,
345 }
346 }
347}
348
349impl<'a> From<&'a str> for Url<'a> {
350 fn from(url: &'a str) -> Self {
351 Self {
352 url: Cow::Borrowed(url),
353 ..Default::default()
354 }
355 }
356}
357
358impl From<String> for Url<'_> {
359 fn from(url: String) -> Self {
360 Self {
361 url: Cow::Owned(url),
362 ..Default::default()
363 }
364 }
365}
366
367impl From<Cow<'static, str>> for Url<'_> {
368 fn from(url: Cow<'static, str>) -> Self {
369 Self {
370 url,
371 ..Default::default()
372 }
373 }
374}
375
376#[non_exhaustive]
379#[derive(Clone, Debug)]
380pub struct SwaggerFile<'a> {
381 pub bytes: Cow<'a, [u8]>,
383 pub content_type: String,
385}
386
387pub fn serve<'a>(
402 path: &str,
403 title: &str,
404 keywords: &str,
405 description: &str,
406 config: &Config<'a>,
407) -> Result<Option<SwaggerFile<'a>>, Error> {
408 let path = if path.is_empty() || path == "/" {
409 "index.html"
410 } else {
411 path
412 };
413
414 let bytes = if path == "index.html" {
415 let config_json = serde_json::to_string(&config)?;
416
417 let mut index = INDEX_TMPL
419 .replacen("{{config}}", &config_json, 1)
420 .replacen("{{description}}", description, 1)
421 .replacen("{{keywords}}", keywords, 1)
422 .replacen("{{title}}", title, 1);
423
424 if let Some(oauth) = &config.oauth {
425 let oauth_json = serde_json::to_string(oauth)?;
426 index = index.replace(
427 "//{{oauth}}",
428 &format!("window.ui.initOAuth({});", &oauth_json),
429 );
430 }
431 Some(Cow::Owned(index.as_bytes().to_vec()))
432 } else {
433 SwaggerUiDist::get(path).map(|f| f.data)
434 };
435 let file = bytes.map(|bytes| SwaggerFile {
436 bytes,
437 content_type: mime_infer::from_path(path)
438 .first_or_octet_stream()
439 .to_string(),
440 });
441
442 Ok(file)
443}