use utoipa::openapi::{
info::InfoBuilder,
path::{PathItem, PathItemBuilder, PathsBuilder},
schema::ComponentsBuilder,
tag::TagBuilder,
OpenApi, OpenApiBuilder,
};
pub struct RegisteredPath {
pub append: fn(PathsBuilder) -> PathsBuilder,
pub schemas: fn(
&mut Vec<(
String,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
)>,
),
}
inventory::collect!(RegisteredPath);
#[derive(Debug, Clone, Copy)]
pub struct TagMeta {
pub name: &'static str,
pub description: Option<&'static str>,
}
#[derive(Debug, Clone, Copy)]
pub struct SpecOptions {
pub title: &'static str,
pub version: &'static str,
pub description: Option<&'static str>,
pub tags: &'static [TagMeta],
}
pub fn append_tagged_path<P>(paths: PathsBuilder) -> PathsBuilder
where
P: utoipa::Path,
P: for<'t> utoipa::__dev::Tags<'t>,
{
let mut operation = P::operation();
let tags: Vec<String> = P::tags().into_iter().map(str::to_string).collect();
if !tags.is_empty() {
operation.tags = Some(tags);
}
let methods = P::methods();
let path_item = if methods.len() == 1 {
PathItem::new(
methods
.into_iter()
.next()
.expect("path must declare at least one method"),
operation,
)
} else {
methods
.into_iter()
.fold(PathItemBuilder::new(), |item, method| {
item.operation(method, operation.clone())
})
.build()
};
paths.path(P::path(), path_item)
}
pub fn build_openapi(opts: &SpecOptions) -> OpenApi {
let mut paths = PathsBuilder::new();
let mut schema_list: Vec<(
String,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
)> = Vec::new();
for reg in inventory::iter::<RegisteredPath> {
paths = (reg.append)(paths);
(reg.schemas)(&mut schema_list);
}
let mut seen = std::collections::HashSet::new();
schema_list.retain(|(name, _)| seen.insert(name.clone()));
let mut info = InfoBuilder::new()
.title(opts.title)
.version(opts.version);
if let Some(desc) = opts.description {
info = info.description(Some(desc));
}
let tags: Vec<_> = opts
.tags
.iter()
.map(|t| {
let mut b = TagBuilder::new().name(t.name);
if let Some(d) = t.description {
b = b.description(Some(d));
}
b.build()
})
.collect();
OpenApiBuilder::new()
.info(info.build())
.paths(paths.build())
.components(Some(
ComponentsBuilder::new()
.schemas_from_iter(schema_list)
.build(),
))
.tags(if tags.is_empty() { None } else { Some(tags) })
.build()
}
#[derive(Debug, Clone)]
pub struct ScalarOptions<'a> {
pub title: &'a str,
pub openapi_url: &'a str,
pub custom_css: &'a str,
pub dark_mode: bool,
pub hide_agent: bool,
pub hide_mcp: bool,
pub hide_client_button: bool,
pub hide_clients: bool,
}
impl Default for ScalarOptions<'static> {
fn default() -> Self {
Self {
title: "API Reference",
openapi_url: "/api/openapi.json",
custom_css: "",
dark_mode: true,
hide_agent: true,
hide_mcp: true,
hide_client_button: true,
hide_clients: true,
}
}
}
pub fn scalar_html(opts: &ScalarOptions<'_>) -> String {
let css_for_js = opts
.custom_css
.replace('\\', "\\\\")
.replace('`', "\\`")
.replace("${", "\\${");
let dark_class = if opts.dark_mode { "dark-mode" } else { "" };
let color_scheme = if opts.dark_mode { "dark" } else { "light" };
format!(
r#"<!doctype html>
<html class="{dark_class}">
<head>
<title>{title}</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="color-scheme" content="{color_scheme}"/>
<style>{css}</style>
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
<script>
Scalar.createApiReference('#app', {{
url: '{openapi_url}',
theme: 'none',
darkMode: {dark_mode},
forceDarkModeState: '{force_dark}',
hideDarkModeToggle: {hide_toggle},
withDefaultFonts: false,
agent: {{ disabled: {hide_agent} }},
mcp: {{ disabled: {hide_mcp} }},
hideClientButton: {hide_client_button},
hiddenClients: {hide_clients},
customCss: `{css_js}`,
}});
</script>
</body>
</html>
"#,
dark_class = dark_class,
title = opts.title,
color_scheme = color_scheme,
css = opts.custom_css,
openapi_url = opts.openapi_url,
dark_mode = opts.dark_mode,
force_dark = if opts.dark_mode { "dark" } else { "light" },
hide_toggle = opts.dark_mode,
hide_agent = opts.hide_agent,
hide_mcp = opts.hide_mcp,
hide_client_button = opts.hide_client_button,
hide_clients = opts.hide_clients,
css_js = css_for_js,
)
}