use rust_embed::RustEmbed;
use serde_json::Value;
pub mod config;
pub use config::{AgentOptions, Source};
#[derive(RustEmbed)]
#[folder = "ui/"]
struct Assets;
pub fn get_asset(path: &str) -> Option<Vec<u8>> {
Assets::get(path).map(|d| d.data.into())
}
pub fn get_asset_with_mime(path: &str) -> Option<(String, Vec<u8>)> {
let asset = Assets::get(path)?;
let mime_type = get_mime_type(path);
Some((mime_type, asset.data.into()))
}
pub fn get_mime_type(path: &str) -> String {
match path.split('.').next_back() {
Some("html") => "text/html".to_string(),
Some("js") => "application/javascript".to_string(),
Some("css") => "text/css".to_string(),
Some("json") => "application/json".to_string(),
Some("png") => "image/png".to_string(),
Some("svg") => "image/svg+xml".to_string(),
Some("ico") => "image/x-icon".to_string(),
_ => "application/octet-stream".to_string(),
}
}
pub fn render_scalar(config_json: &str, js_bundle_url: Option<&str>) -> String {
let html_template = include_str!("../ui/index.html");
let js_url = js_bundle_url.unwrap_or("https://cdn.jsdelivr.net/npm/@scalar/api-reference");
html_template
.replace("__CONFIGURATION__", config_json)
.replace("__JS_BUNDLE_URL__", js_url)
}
pub fn scalar_html(config: &Value, js_bundle_url: Option<&str>) -> String {
render_scalar(&config.to_string(), js_bundle_url)
}
pub fn scalar_html_default(config: &Value) -> String {
scalar_html(config, None)
}
pub fn scalar_html_from_json(
config_json: &str,
js_bundle_url: Option<&str>,
) -> Result<String, serde_json::Error> {
let config: Value = serde_json::from_str(config_json)?;
Ok(scalar_html(&config, js_bundle_url))
}
pub fn scalar_html_from_json_default(config_json: &str) -> Result<String, serde_json::Error> {
scalar_html_from_json(config_json, None)
}
#[cfg(feature = "axum")]
pub mod axum {
use super::{get_asset_with_mime, scalar_html};
use axum::{
body::Body, http::StatusCode, response::Html, response::Response, routing::get, Router,
};
use serde_json::Value;
pub fn scalar_response(config: &Value, js_bundle_url: Option<&str>) -> Html<String> {
Html(scalar_html(config, js_bundle_url))
}
pub fn scalar_response_from_json(
config_json: &str,
js_bundle_url: Option<&str>,
) -> Result<Html<String>, serde_json::Error> {
let config: Value = serde_json::from_str(config_json)?;
Ok(scalar_response(&config, js_bundle_url))
}
pub fn router(path: &str, config: &Value) -> Router {
let js_path = format!("{}/scalar.js", path);
let config_clone = config.clone();
let js_path_clone = js_path.clone();
Router::new()
.route(
path,
get(move || {
let config = config_clone.clone();
let js_path = js_path_clone.clone();
async move { scalar_response(&config, Some(&js_path)) }
}),
)
.route(
&js_path,
get(|| async {
match get_asset_with_mime("scalar.js") {
Some((mime_type, content)) => Response::builder()
.status(StatusCode::OK)
.header("content-type", mime_type)
.body(Body::from(content))
.unwrap(),
None => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("Not found"))
.unwrap(),
}
}),
)
}
pub fn routes(path: &str, config: &Value) -> (Router, Router) {
let js_path = format!("{}/scalar.js", path);
let config_clone = config.clone();
let js_path_clone = js_path.clone();
let scalar_route = Router::new().route(
path,
get(move || {
let config = config_clone.clone();
let js_path = js_path_clone.clone();
async move { scalar_response(&config, Some(&js_path)) }
}),
);
let asset_route = Router::new().route(
&js_path,
get(|| async {
match get_asset_with_mime("scalar.js") {
Some((mime_type, content)) => Response::builder()
.status(StatusCode::OK)
.header("content-type", mime_type)
.body(Body::from(content))
.unwrap(),
None => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("Not found"))
.unwrap(),
}
}),
);
(scalar_route, asset_route)
}
}
#[cfg(feature = "actix-web")]
pub mod actix_web {
use super::{get_asset_with_mime, scalar_html};
use actix_web::web::ServiceConfig;
use actix_web::{http::header::ContentType, web, HttpResponse, Result};
use serde_json::Value;
pub fn scalar_response(config: &Value, js_bundle_url: Option<&str>) -> HttpResponse {
HttpResponse::Ok()
.content_type(ContentType::html())
.body(scalar_html(config, js_bundle_url))
}
pub fn scalar_response_from_json(
config_json: &str,
js_bundle_url: Option<&str>,
) -> Result<HttpResponse, serde_json::Error> {
let config: Value = serde_json::from_str(config_json)?;
Ok(scalar_response(&config, js_bundle_url))
}
pub fn config(path: &str, config: &Value) -> impl Fn(&mut ServiceConfig) {
let scalar_path = path.to_string();
let js_path = format!("{}/scalar.js", path);
let config_clone = config.clone();
let js_path_clone = js_path.clone();
move |cfg: &mut ServiceConfig| {
let scalar_path = scalar_path.clone();
let js_path = js_path_clone.clone();
let config = config_clone.clone();
let js_path_for_asset = js_path.clone();
cfg.route(
&scalar_path,
web::get().to(move || {
let config = config.clone();
let js_path = js_path.clone();
async move { scalar_response(&config, Some(&js_path)) }
}),
)
.route(
&js_path_for_asset,
web::get().to(|| async {
match get_asset_with_mime("scalar.js") {
Some((mime_type, content)) => {
HttpResponse::Ok().content_type(mime_type).body(content)
}
None => HttpResponse::NotFound().body("Not found"),
}
}),
);
}
}
}
#[cfg(feature = "warp")]
pub mod warp {
use super::{get_asset_with_mime, scalar_html};
use serde_json::Value;
use warp::{reply::html, Filter, Reply};
pub fn scalar_reply(config: &Value, js_bundle_url: Option<&str>) -> impl warp::Reply {
html(scalar_html(config, js_bundle_url))
}
pub fn scalar_reply_from_json(
config_json: &str,
js_bundle_url: Option<&str>,
) -> Result<impl warp::Reply, serde_json::Error> {
let config: Value = serde_json::from_str(config_json)?;
Ok(scalar_reply(&config, js_bundle_url))
}
pub fn routes(
path: &'static str,
config: &Value,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
let config_clone = config.clone();
let clean_path = path.trim_start_matches('/');
let asset_route = create_asset_route(clean_path);
let scalar_route = warp::path(clean_path).and(warp::path::end()).map(move || {
let config = config_clone.clone();
let js_path = format!("{}/scalar.js", clean_path);
scalar_reply(&config, Some(&js_path))
});
asset_route.or(scalar_route)
}
fn create_asset_route(
path: &'static str,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path(path)
.and(warp::path("scalar.js"))
.and_then(move || async move {
match get_asset_with_mime("scalar.js") {
Some((mime_type, content)) => {
Ok::<_, warp::Rejection>(warp::reply::with_header(
warp::reply::with_status(content, warp::http::StatusCode::OK),
"content-type",
mime_type,
))
}
None => Ok::<_, warp::Rejection>(warp::reply::with_header(
warp::reply::with_status(
Vec::<u8>::new(),
warp::http::StatusCode::NOT_FOUND,
),
"content-type",
"text/plain",
)),
}
})
}
pub fn separate_routes(
path: &'static str,
config: &Value,
) -> (
impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone,
impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone,
) {
let config_clone = config.clone();
let clean_path = path.trim_start_matches('/');
let asset_route = warp::path(clean_path)
.and(warp::path("scalar.js"))
.and_then(move || async move {
match get_asset_with_mime("scalar.js") {
Some((mime_type, content)) => {
Ok::<_, warp::Rejection>(warp::reply::with_header(
warp::reply::with_status(content, warp::http::StatusCode::OK),
"content-type",
mime_type,
))
}
None => Ok::<_, warp::Rejection>(warp::reply::with_header(
warp::reply::with_status(
Vec::<u8>::new(),
warp::http::StatusCode::NOT_FOUND,
),
"content-type",
"text/plain",
)),
}
});
let scalar_route = warp::path(clean_path).and(warp::path::end()).map(move || {
let config = config_clone.clone();
let js_path = format!("{}/scalar.js", clean_path);
scalar_reply(&config, Some(&js_path))
});
(scalar_route, asset_route)
}
}
#[cfg(test)]
mod tests {
use crate::{
get_asset, get_asset_with_mime, get_mime_type, scalar_html, scalar_html_default,
scalar_html_from_json, scalar_html_from_json_default, AgentOptions, Source,
};
use serde_json::json;
#[test]
fn test_scalar_html_generation() {
let config = json!({
"url": "/openapi.json",
"theme": "purple"
});
let html1 = scalar_html(&config, Some("/custom-scalar.js"));
assert!(html1.contains("/openapi.json"));
assert!(html1.contains("purple"));
assert!(html1.contains("/custom-scalar.js"));
assert!(html1.contains("<html"));
assert!(html1.contains("</html>"));
let html2 = scalar_html(&config, None);
assert!(html2.contains("/openapi.json"));
assert!(html2.contains("purple"));
assert!(html2.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
assert!(html2.contains("<html"));
assert!(html2.contains("</html>"));
}
#[test]
fn test_scalar_html_from_json() {
let config_json = r#"{"url": "/api.json", "theme": "purple"}"#;
let html1 = scalar_html_from_json(config_json, Some("/bundle.js")).unwrap();
assert!(html1.contains("/api.json"));
assert!(html1.contains("purple"));
assert!(html1.contains("/bundle.js"));
let html2 = scalar_html_from_json(config_json, None).unwrap();
assert!(html2.contains("/api.json"));
assert!(html2.contains("purple"));
assert!(html2.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
}
#[test]
fn test_convenience_functions() {
let config = json!({
"url": "/test.json",
"theme": "kepler"
});
let html1 = scalar_html_default(&config);
assert!(html1.contains("/test.json"));
assert!(html1.contains("kepler"));
assert!(html1.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
let config_json = r#"{"url": "/test2.json", "theme": "purple"}"#;
let html2 = scalar_html_from_json_default(config_json).unwrap();
assert!(html2.contains("/test2.json"));
assert!(html2.contains("purple"));
assert!(html2.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
}
#[test]
fn test_get_asset() {
let html_asset = get_asset("index.html");
assert!(html_asset.is_some());
let js_asset = get_asset("scalar.js");
assert!(js_asset.is_some());
let non_existent = get_asset("non-existent.txt");
assert!(non_existent.is_none());
}
#[test]
fn test_get_asset_with_mime() {
let (mime_type, content) = get_asset_with_mime("index.html").unwrap();
assert_eq!(mime_type, "text/html");
assert!(!content.is_empty());
let (mime_type, content) = get_asset_with_mime("scalar.js").unwrap();
assert_eq!(mime_type, "application/javascript");
assert!(!content.is_empty());
}
#[test]
fn test_mime_type_detection() {
assert_eq!(get_mime_type("index.html"), "text/html");
assert_eq!(get_mime_type("style.css"), "text/css");
assert_eq!(get_mime_type("script.js"), "application/javascript");
assert_eq!(get_mime_type("data.json"), "application/json");
assert_eq!(get_mime_type("image.png"), "image/png");
assert_eq!(get_mime_type("icon.svg"), "image/svg+xml");
assert_eq!(get_mime_type("favicon.ico"), "image/x-icon");
assert_eq!(get_mime_type("unknown.xyz"), "application/octet-stream");
}
#[test]
fn test_error_handling() {
let invalid_json = r#"{"url": "/api.json", "theme": "purple""#; let result = scalar_html_from_json(invalid_json, None);
assert!(result.is_err());
let empty_json = r#"{}"#;
let result = scalar_html_from_json(empty_json, None);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
}
#[test]
fn test_agent_options_in_config() {
let agent = AgentOptions::with_key("test-key");
let config = json!({
"url": "/openapi.json",
"agent": serde_json::to_value(&agent).unwrap()
});
let html = scalar_html(&config, None);
assert!(html.contains("test-key"));
assert!(html.contains("/openapi.json"));
let agent_disabled = AgentOptions::disabled();
let config2 = json!({
"url": "/openapi.json",
"agent": serde_json::to_value(&agent_disabled).unwrap()
});
let html2 = scalar_html(&config2, None);
assert!(html2.contains("true")); }
#[test]
fn test_sources_with_agent_in_config() {
let sources = vec![
Source::new("https://api.example.com/v1.json")
.with_agent(AgentOptions::with_key("doc-key")),
Source::new("https://api.example.com/v2.json"),
];
let config = json!({
"sources": serde_json::to_value(&sources).unwrap()
});
let html = scalar_html(&config, None);
assert!(html.contains("doc-key"));
assert!(html.contains("https://api.example.com/v1.json"));
assert!(html.contains("https://api.example.com/v2.json"));
}
#[test]
fn test_edge_cases() {
let empty_config = json!({});
let html = scalar_html(&empty_config, None);
assert!(html.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
let special_config = json!({
"url": "/api/v1/test?param=value&other=test",
"theme": "purple",
"description": "API with special chars: <>&\"'"
});
let html = scalar_html(&special_config, None);
assert!(html.contains("/api/v1/test?param=value&other=test"));
assert!(html.contains("purple"));
let config_with_special_path = json!({
"url": "/api/test",
"theme": "purple"
});
let html = scalar_html(&config_with_special_path, Some("/custom/path/scalar.js"));
assert!(html.contains("/custom/path/scalar.js"));
}
}
#[cfg(all(test, feature = "axum"))]
mod axum_tests {
use crate::axum::{router, routes, scalar_response, scalar_response_from_json};
use serde_json::json;
#[test]
fn test_scalar_response() {
let config = json!({
"url": "/openapi.json",
"theme": "purple"
});
let response = scalar_response(&config, Some("/custom-scalar.js"));
let html = response.0;
assert!(html.contains("/openapi.json"));
assert!(html.contains("purple"));
assert!(html.contains("/custom-scalar.js"));
let response = scalar_response(&config, None);
let html = response.0;
assert!(html.contains("/openapi.json"));
assert!(html.contains("purple"));
assert!(html.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
}
#[test]
fn test_scalar_response_from_json() {
let config_json = r#"{"url": "/api.json", "theme": "purple"}"#;
let response = scalar_response_from_json(config_json, Some("/bundle.js")).unwrap();
let html = response.0;
assert!(html.contains("/api.json"));
assert!(html.contains("purple"));
assert!(html.contains("/bundle.js"));
let response = scalar_response_from_json(config_json, None).unwrap();
let html = response.0;
assert!(html.contains("/api.json"));
assert!(html.contains("purple"));
assert!(html.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
let invalid_json = r#"{"url": "/api.json", "theme": "purple""#;
let result = scalar_response_from_json(invalid_json, None);
assert!(result.is_err());
}
#[test]
fn test_router_creation() {
let config = json!({
"url": "/openapi.json",
"theme": "purple"
});
let _app = router("/scalar", &config);
}
#[test]
fn test_routes_creation() {
let config = json!({
"url": "/openapi.json",
"theme": "purple"
});
let (_scalar_route, _asset_route) = routes("/scalar", &config);
}
}
#[cfg(all(test, feature = "actix-web"))]
mod actix_tests {
use crate::actix_web::{config, scalar_response, scalar_response_from_json};
use serde_json::json;
#[test]
fn test_scalar_response() {
let config = json!({
"url": "/openapi.json",
"theme": "purple"
});
let _response = scalar_response(&config, Some("/custom-scalar.js"));
let _response = scalar_response(&config, None);
}
#[test]
fn test_scalar_response_from_json() {
let config_json = r#"{"url": "/api.json", "theme": "purple"}"#;
let _response = scalar_response_from_json(config_json, Some("/bundle.js")).unwrap();
let _response = scalar_response_from_json(config_json, None).unwrap();
let invalid_json = r#"{"url": "/api.json", "theme": "purple""#;
let result = scalar_response_from_json(invalid_json, None);
assert!(result.is_err());
}
#[test]
fn test_config_creation() {
let config_json = json!({
"url": "/openapi.json",
"theme": "purple"
});
let _config_fn = config("/scalar", &config_json);
}
}
#[cfg(all(test, feature = "warp"))]
mod warp_tests {
use crate::warp::{routes, scalar_reply, scalar_reply_from_json, separate_routes};
use serde_json::json;
#[test]
fn test_scalar_reply() {
let config = json!({
"url": "/openapi.json",
"theme": "purple"
});
let _reply = scalar_reply(&config, Some("/custom-scalar.js"));
let _reply = scalar_reply(&config, None);
}
#[test]
fn test_scalar_reply_from_json() {
let config_json = r#"{"url": "/api.json", "theme": "purple"}"#;
let _reply = scalar_reply_from_json(config_json, Some("/bundle.js")).unwrap();
let _reply = scalar_reply_from_json(config_json, None).unwrap();
let invalid_json = r#"{"url": "/api.json", "theme": "purple""#;
let result = scalar_reply_from_json(invalid_json, None);
assert!(result.is_err());
}
#[test]
fn test_routes_creation() {
let config = json!({
"url": "/openapi.json",
"theme": "purple"
});
let _filter = routes("scalar", &config);
}
#[test]
fn test_separate_routes_creation() {
let config = json!({
"url": "/openapi.json",
"theme": "purple"
});
let (_scalar_filter, _asset_filter) = separate_routes("scalar", &config);
}
}