mod config;
mod oauth;
pub use config::Config;
use hypers_core::{
async_trait,
prelude::{Request, Response},
fs::redirect_to_dir_url, Handler,
};
pub use oauth::OauthConfig;
use rust_embed::RustEmbed;
use serde::Serialize;
use std::borrow::Cow;
#[derive(RustEmbed)]
#[folder = "src/swagger_ui/v5.17.14"]
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>
"#;
#[non_exhaustive]
pub struct SwaggerFile<'a> {
pub bytes: Cow<'a, [u8]>,
pub content_type: String,
}
#[derive(Clone, Debug)]
pub struct SwaggerUi {
pub 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,
}
}
pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
self.title = title.into();
self
}
pub fn keywords(mut self, keywords: impl Into<Cow<'static, str>>) -> Self {
self.keywords = Some(keywords.into());
self
}
pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
self.description = Some(description.into());
self
}
pub fn url<U: Into<Url<'static>>>(mut self, url: U) -> Self {
self.config.urls.push(url.into());
self
}
pub fn urls(mut self, urls: Vec<Url<'static>>) -> Self {
self.config.urls = urls;
self
}
pub fn oauth(mut self, oauth: oauth::OauthConfig) -> Self {
self.config.oauth = Some(oauth);
self
}
pub fn serve<'a>(
path: &str,
title: &str,
keywords: &str,
description: &str,
config: &Config<'a>,
) -> Result<Option<SwaggerFile<'a>>, serde_json::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_guess::from_path(path)
.first_or_octet_stream()
.to_string(),
});
Ok(file)
}
}
#[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> {
pub fn new(name: &'a str, url: &'a str) -> Self {
Self {
name: Cow::Borrowed(name),
url: Cow::Borrowed(url),
..Default::default()
}
}
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<'a> From<Cow<'static, str>> for Url<'a> {
fn from(url: Cow<'static, str>) -> Self {
Self {
url,
..Default::default()
}
}
}
#[async_trait]
impl Handler for SwaggerUi {
#[inline]
async fn handle(&self, req: Request) -> Response {
let path = req.param::<&str>("*1").map(|s| s.trim_start_matches('/'));
let mut res = Response::default();
if path.is_err() && !req.uri().path().ends_with('/') {
return redirect_to_dir_url(req.uri(), res);
}
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 SwaggerUi::serve(
path.unwrap(),
&self.title,
&keywords,
&description,
&self.config,
) {
Ok(Some(file)) => {
res.content_type(&file.content_type)
.body(file.bytes.to_vec());
res
}
Ok(None) => {
res.status(500);
res
}
Err(e) => {
res.status(500).body(e.to_string());
res
}
}
}
}