use std::borrow::Cow;
mod config;
pub mod oauth;
pub use config::Config;
pub use oauth::Config as OauthConfig;
use rust_embed::RustEmbed;
use salvo_core::http::{HeaderValue, ResBody, StatusError, header};
use salvo_core::routing::redirect_to_dir_url;
use salvo_core::{Depot, Error, FlowCtrl, Handler, Request, Response, Router, async_trait};
use serde::Serialize;
#[derive(RustEmbed)]
#[folder = "src/swagger_ui/v5.32.4"]
struct SwaggerUiDist;
const INDEX_TMPL: &str = r#"
<!DOCTYPE html>
<html charset="UTF-8">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
{{keywords}}
{{description}}
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"></script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"></script>
<script>
window.onload = function() {
let config = {
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
};
window.ui = SwaggerUIBundle(Object.assign(config, {{config}}));
//{{oauth}}
};
</script>
</body>
</html>
"#;
#[derive(Clone, Debug)]
pub struct SwaggerUi {
config: Config<'static>,
pub title: Cow<'static, str>,
pub keywords: Option<Cow<'static, str>>,
pub description: Option<Cow<'static, str>>,
}
impl SwaggerUi {
pub fn new(config: impl Into<Config<'static>>) -> Self {
Self {
config: config.into(),
title: "Swagger UI".into(),
keywords: None,
description: None,
}
}
#[must_use]
pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
self.title = title.into();
self
}
#[must_use]
pub fn keywords(mut self, keywords: impl Into<Cow<'static, str>>) -> Self {
self.keywords = Some(keywords.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn url<U: Into<Url<'static>>>(mut self, url: U) -> Self {
self.config.urls.push(url.into());
self
}
#[must_use]
pub fn urls(mut self, urls: Vec<Url<'static>>) -> Self {
self.config.urls = urls;
self
}
#[must_use]
pub fn oauth(mut self, oauth: oauth::Config) -> Self {
self.config.oauth = Some(oauth);
self
}
pub fn into_router(self, path: impl Into<String>) -> Router {
Router::with_path(format!("{}/{{**}}", path.into())).goal(self)
}
}
#[async_trait]
impl Handler for SwaggerUi {
async fn handle(
&self,
req: &mut Request,
_depot: &mut Depot,
res: &mut Response,
_ctrl: &mut FlowCtrl,
) {
let path = req.params().tail().unwrap_or_default();
if path.is_empty() && !req.uri().path().ends_with('/') {
redirect_to_dir_url(req.uri(), res);
return;
}
let keywords = self
.keywords
.as_ref()
.map(|s| {
format!(
"<meta name=\"keywords\" content=\"{}\">",
s.split(',').map(|s| s.trim()).collect::<Vec<_>>().join(",")
)
})
.unwrap_or_default();
let description = self
.description
.as_ref()
.map(|s| format!("<meta name=\"description\" content=\"{s}\">"))
.unwrap_or_default();
match serve(path, &self.title, &keywords, &description, &self.config) {
Ok(Some(file)) => {
res.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_str(&file.content_type).expect("content type parse failed"),
);
res.body(ResBody::Once(file.bytes.to_vec().into()));
}
Ok(None) => {
tracing::warn!(path, "swagger ui file not found");
res.render(StatusError::not_found());
}
Err(e) => {
tracing::error!(error = ?e, path, "failed to fetch swagger ui file");
res.render(StatusError::internal_server_error());
}
}
}
}
#[non_exhaustive]
#[derive(Default, Serialize, Clone, Debug)]
pub struct Url<'a> {
name: Cow<'a, str>,
url: Cow<'a, str>,
#[serde(skip)]
primary: bool,
}
impl<'a> Url<'a> {
#[must_use]
pub fn new(name: &'a str, url: &'a str) -> Self {
Self {
name: Cow::Borrowed(name),
url: Cow::Borrowed(url),
..Default::default()
}
}
#[must_use]
pub fn with_primary(name: &'a str, url: &'a str, primary: bool) -> Self {
Self {
name: Cow::Borrowed(name),
url: Cow::Borrowed(url),
primary,
}
}
}
impl<'a> From<&'a str> for Url<'a> {
fn from(url: &'a str) -> Self {
Self {
url: Cow::Borrowed(url),
..Default::default()
}
}
}
impl From<String> for Url<'_> {
fn from(url: String) -> Self {
Self {
url: Cow::Owned(url),
..Default::default()
}
}
}
impl From<Cow<'static, str>> for Url<'_> {
fn from(url: Cow<'static, str>) -> Self {
Self {
url,
..Default::default()
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct SwaggerFile<'a> {
pub bytes: Cow<'a, [u8]>,
pub content_type: String,
}
pub fn serve<'a>(
path: &str,
title: &str,
keywords: &str,
description: &str,
config: &Config<'a>,
) -> Result<Option<SwaggerFile<'a>>, Error> {
let path = if path.is_empty() || path == "/" {
"index.html"
} else {
path
};
let bytes = if path == "index.html" {
let config_json = serde_json::to_string(&config)?;
let mut index = INDEX_TMPL
.replacen("{{config}}", &config_json, 1)
.replacen("{{description}}", description, 1)
.replacen("{{keywords}}", keywords, 1)
.replacen("{{title}}", title, 1);
if let Some(oauth) = &config.oauth {
let oauth_json = serde_json::to_string(oauth)?;
index = index.replace(
"//{{oauth}}",
&format!("window.ui.initOAuth({});", &oauth_json),
);
}
Some(Cow::Owned(index.as_bytes().to_vec()))
} else {
SwaggerUiDist::get(path).map(|f| f.data)
};
let file = bytes.map(|bytes| SwaggerFile {
bytes,
content_type: mime_infer::from_path(path)
.first_or_octet_stream()
.to_string(),
});
Ok(file)
}