use crate::response::{Response, ResponseBody};
#[derive(Debug, Clone)]
pub struct DocsConfig {
pub docs_path: Option<String>,
pub redoc_path: Option<String>,
pub openapi_path: String,
pub title: String,
pub swagger_ui_parameters: Option<String>,
pub swagger_ui_init_oauth: Option<String>,
pub favicon_url: Option<String>,
pub swagger_cdn_url: String,
pub redoc_cdn_url: String,
}
impl Default for DocsConfig {
fn default() -> Self {
Self {
docs_path: Some("/docs".to_string()),
redoc_path: Some("/redoc".to_string()),
openapi_path: "/openapi.json".to_string(),
title: "API Documentation".to_string(),
swagger_ui_parameters: None,
swagger_ui_init_oauth: None,
favicon_url: None,
swagger_cdn_url: "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5".to_string(),
redoc_cdn_url: "https://cdn.jsdelivr.net/npm/redoc@latest".to_string(),
}
}
}
impl DocsConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn docs_path(mut self, path: impl Into<Option<String>>) -> Self {
self.docs_path = path.into();
self
}
#[must_use]
pub fn redoc_path(mut self, path: impl Into<Option<String>>) -> Self {
self.redoc_path = path.into();
self
}
#[must_use]
pub fn openapi_path(mut self, path: impl Into<String>) -> Self {
self.openapi_path = path.into();
self
}
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
#[must_use]
pub fn swagger_ui_parameters(mut self, params: impl Into<String>) -> Self {
self.swagger_ui_parameters = Some(params.into());
self
}
#[must_use]
pub fn swagger_ui_init_oauth(mut self, config: impl Into<String>) -> Self {
self.swagger_ui_init_oauth = Some(config.into());
self
}
#[must_use]
pub fn favicon_url(mut self, url: impl Into<String>) -> Self {
self.favicon_url = Some(url.into());
self
}
#[must_use]
pub fn swagger_cdn_url(mut self, url: impl Into<String>) -> Self {
self.swagger_cdn_url = url.into();
self
}
#[must_use]
pub fn redoc_cdn_url(mut self, url: impl Into<String>) -> Self {
self.redoc_cdn_url = url.into();
self
}
}
#[must_use]
pub fn swagger_ui_html(config: &DocsConfig, openapi_url: &str) -> String {
let title = html_escape(&config.title);
let swagger_cdn = &config.swagger_cdn_url;
let favicon = config.favicon_url.as_ref().map_or_else(
|| format!(r#"<link rel="icon" type="image/png" href="{swagger_cdn}/favicon-32x32.png" sizes="32x32" />"#),
|url| format!(r#"<link rel="icon" href="{}" />"#, html_escape(url)),
);
let ui_parameters = config
.swagger_ui_parameters
.as_ref()
.map_or_else(|| "{}".to_string(), String::clone);
let init_oauth = config
.swagger_ui_init_oauth
.as_ref()
.map_or_else(String::new, |o| format!("ui.initOAuth({});", o));
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
{favicon}
<link rel="stylesheet" type="text/css" href="{swagger_cdn}/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="{swagger_cdn}/swagger-ui-bundle.js"></script>
<script src="{swagger_cdn}/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {{
const ui = SwaggerUIBundle(Object.assign({{
url: "{openapi_url}",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
}}, {ui_parameters}));
{init_oauth}
window.ui = ui;
}};
</script>
</body>
</html>"#,
title = title,
favicon = favicon,
swagger_cdn = swagger_cdn,
openapi_url = html_escape(openapi_url),
ui_parameters = ui_parameters,
init_oauth = init_oauth,
)
}
#[must_use]
pub fn redoc_html(config: &DocsConfig, openapi_url: &str) -> String {
let title = html_escape(&config.title);
let redoc_cdn = &config.redoc_cdn_url;
let favicon = config.favicon_url.as_ref().map_or_else(String::new, |url| {
format!(r#"<link rel="icon" href="{}" />"#, html_escape(url))
});
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
{favicon}
<style>
body {{
margin: 0;
padding: 0;
}}
</style>
</head>
<body>
<redoc spec-url="{openapi_url}"></redoc>
<script src="{redoc_cdn}/bundles/redoc.standalone.js"></script>
</body>
</html>"#,
title = title,
favicon = favicon,
openapi_url = html_escape(openapi_url),
redoc_cdn = redoc_cdn,
)
}
#[must_use]
pub fn oauth2_redirect_html() -> &'static str {
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run() {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = window.location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function(v, i, _arr) { _arr[i] = '"' + v.replace('=', '":"') + '"'; });
qp = qp ? JSON.parse('{' + arr.join(',') + '}',
function(key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code") &&
!oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
var oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "[" + qp.error + "]: " +
(qp.error_description ? qp.error_description + ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: " + qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function() {
run();
});
}
</script>
</body>
</html>"#
}
#[must_use]
pub fn swagger_ui_response(config: &DocsConfig, openapi_url: &str) -> Response {
let html = swagger_ui_html(config, openapi_url);
Response::ok()
.header("content-type", b"text/html; charset=utf-8".to_vec())
.body(ResponseBody::Bytes(html.into_bytes()))
}
#[must_use]
pub fn redoc_response(config: &DocsConfig, openapi_url: &str) -> Response {
let html = redoc_html(config, openapi_url);
Response::ok()
.header("content-type", b"text/html; charset=utf-8".to_vec())
.body(ResponseBody::Bytes(html.into_bytes()))
}
#[must_use]
pub fn oauth2_redirect_response() -> Response {
Response::ok()
.header("content-type", b"text/html; charset=utf-8".to_vec())
.body(ResponseBody::Bytes(
oauth2_redirect_html().as_bytes().to_vec(),
))
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = DocsConfig::default();
assert_eq!(config.docs_path, Some("/docs".to_string()));
assert_eq!(config.redoc_path, Some("/redoc".to_string()));
assert_eq!(config.openapi_path, "/openapi.json");
assert_eq!(config.title, "API Documentation");
}
#[test]
fn test_config_builder() {
let config = DocsConfig::new()
.docs_path(Some("/api-docs".to_string()))
.redoc_path(None::<String>)
.openapi_path("/spec.json")
.title("My API")
.swagger_ui_parameters(r#"{"docExpansion": "none"}"#)
.swagger_ui_init_oauth(r#"{"clientId": "test"}"#);
assert_eq!(config.docs_path, Some("/api-docs".to_string()));
assert_eq!(config.redoc_path, None);
assert_eq!(config.openapi_path, "/spec.json");
assert_eq!(config.title, "My API");
assert!(config.swagger_ui_parameters.is_some());
assert!(config.swagger_ui_init_oauth.is_some());
}
#[test]
fn test_swagger_ui_html() {
let config = DocsConfig::new().title("Test API");
let html = swagger_ui_html(&config, "/openapi.json");
assert!(html.contains("<title>Test API</title>"));
assert!(html.contains("swagger-ui-bundle.js"));
assert!(html.contains("url: \"/openapi.json\""));
}
#[test]
fn test_redoc_html() {
let config = DocsConfig::new().title("Test API");
let html = redoc_html(&config, "/openapi.json");
assert!(html.contains("<title>Test API</title>"));
assert!(html.contains("redoc.standalone.js"));
assert!(html.contains("spec-url=\"/openapi.json\""));
}
#[test]
fn test_oauth2_redirect_html() {
let html = oauth2_redirect_html();
assert!(html.contains("OAuth2 Redirect"));
assert!(html.contains("swaggerUIRedirectOauth2"));
}
#[test]
fn test_html_escape() {
assert_eq!(html_escape("<script>"), "<script>");
assert_eq!(html_escape("a&b"), "a&b");
assert_eq!(html_escape("\"test\""), ""test"");
}
#[test]
fn test_swagger_ui_with_custom_params() {
let config = DocsConfig::new().swagger_ui_parameters(r#"{"filter": true}"#);
let html = swagger_ui_html(&config, "/openapi.json");
assert!(html.contains(r#"{"filter": true}"#));
}
#[test]
fn test_swagger_ui_with_oauth() {
let config = DocsConfig::new().swagger_ui_init_oauth(r#"{"clientId": "my-app"}"#);
let html = swagger_ui_html(&config, "/openapi.json");
assert!(html.contains(r#"ui.initOAuth({"clientId": "my-app"});"#));
}
#[test]
fn test_custom_cdn_urls() {
let config = DocsConfig::new()
.swagger_cdn_url("https://custom.cdn/swagger")
.redoc_cdn_url("https://custom.cdn/redoc");
let swagger_html = swagger_ui_html(&config, "/spec.json");
let redoc_html = redoc_html(&config, "/spec.json");
assert!(swagger_html.contains("https://custom.cdn/swagger"));
assert!(redoc_html.contains("https://custom.cdn/redoc"));
}
}