use std::sync::Arc;
use bytes::Bytes;
use tork_core::constants::TEXT_HTML_UTF8;
use tork_core::{
bytes_response, BoxFuture, HandlerFn, Method, RequestContext, Response, Result, Route,
StatusCode,
};
use crate::spec::{check_guard, DocGuard};
const SCALAR_CDN_URL: &str =
"https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.59.3/dist/browser/standalone.min.js";
const SCALAR_SRI: &str = "sha384-irPuG6Dqh5tfvLv4Yl+FeLzXKTA6CfA5aON/ACBCOuvhKXG8yK4umxZg8E7rBxQf";
pub(crate) fn docs_route(
path: &str,
title: &str,
spec_url: &str,
guard: Option<DocGuard>,
) -> Route {
let body = Bytes::from(render_html(title, spec_url));
let handler: HandlerFn = Arc::new(
move |ctx: RequestContext| -> BoxFuture<'static, Result<Response>> {
let body = body.clone();
let guard = guard.clone();
Box::pin(async move {
check_guard(&guard, &ctx)?;
Ok(bytes_response(StatusCode::OK, TEXT_HTML_UTF8, body))
})
},
);
Route::new(Method::GET, path.to_owned(), handler).summary("API documentation")
}
fn render_html(title: &str, spec_url: &str) -> String {
let title = html_escape(title);
let spec_url = html_escape(spec_url);
format!(
"<!doctype html>\n\
<html>\n\
<head>\n \
<meta charset=\"utf-8\" />\n \
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n \
<title>{title}</title>\n\
</head>\n\
<body>\n \
<script id=\"api-reference\" data-url=\"{spec_url}\"></script>\n \
<script src=\"{SCALAR_CDN_URL}\" integrity=\"{SCALAR_SRI}\" crossorigin=\"anonymous\"></script>\n\
</body>\n\
</html>\n"
)
}
fn html_escape(input: &str) -> String {
input
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
.replace('`', "`")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn html_escape_replaces_reserved_characters() {
assert_eq!(
html_escape(r#"<Tork & "Docs">"#),
"<Tork & "Docs">"
);
assert_eq!(html_escape("a'b`c"), "a'b`c");
}
#[test]
fn render_html_embeds_escaped_title_and_spec_url() {
let html = render_html(r#"Tork "Docs""#, "/openapi.json?x=<tag>");
assert!(html.contains("<title>Tork "Docs"</title>"));
assert!(html.contains("data-url=\"/openapi.json?x=<tag>\""));
assert!(html.contains(SCALAR_CDN_URL));
}
#[test]
fn render_html_pins_the_cdn_and_adds_integrity() {
let html = render_html("API", "/openapi.json");
assert!(html.contains("@scalar/api-reference@1.59.3/"));
assert!(!html.contains("npm/@scalar/api-reference\""));
assert!(html.contains(&format!("integrity=\"{SCALAR_SRI}\"")));
assert!(html.contains("crossorigin=\"anonymous\""));
}
#[test]
fn docs_route_uses_requested_path() {
let route = docs_route("/docs", "API", "/openapi.json", None);
assert_eq!(route.path(), "/docs");
assert_eq!(route.method(), Method::GET);
assert_eq!(route.meta().summary.as_deref(), Some("API documentation"));
}
}