Files
RustProxy/src/admin/documentation.rs
T
2026-10-05 12:22:41 +02:00

107 lines
3.6 KiB
Rust

//! Authenticated documentation Reader and OpenAPI/Swagger UI on the admin listener.
//! Documentation and vendor assets are embedded at compile time; no runtime filesystem or CDN access.
use axum::{
response::{Html, IntoResponse, Response},
Json,
};
use http::StatusCode;
use serde_json::{json, Value};
use std::sync::OnceLock;
// Keep Reader input independent from build-script output. `include_str!` makes every
// document a direct compiler input, so a clean or partially removed target directory
// cannot leave the crate pointing at a missing OUT_DIR artifact.
mod documents;
use documents::READER_DOCUMENTS;
pub(super) async fn index() -> Html<&'static str> {
Html(include_str!("web/docs.html"))
}
pub(super) async fn script() -> impl IntoResponse {
(
[("content-type", "text/javascript; charset=utf-8")],
include_str!("web/docs.js"),
)
}
pub(super) async fn style() -> impl IntoResponse {
(
[("content-type", "text/css; charset=utf-8")],
include_str!("web/docs.css"),
)
}
fn reader_bundle() -> &'static [u8] {
static BUNDLE: OnceLock<Vec<u8>> = OnceLock::new();
BUNDLE
.get_or_init(|| {
let documents: Vec<_> = READER_DOCUMENTS
.iter()
.copied()
.map(|(path, category, content)| {
let file_name = path.rsplit('/').next().unwrap_or(path);
let title = content
.lines()
.find_map(|line| line.strip_prefix("# "))
.filter(|title| !title.trim().is_empty())
.unwrap_or_else(|| file_name.trim_end_matches(".md"));
json!({
"path": path,
"title": title,
"category": category,
"content": content,
})
})
.collect();
serde_json::to_vec(&json!({ "documents": documents }))
.expect("embedded documentation must serialize")
})
.as_slice()
}
pub(super) async fn content() -> impl IntoResponse {
(
[("content-type", "application/json; charset=utf-8")],
reader_bundle(),
)
}
pub(super) async fn schema() -> Json<Value> {
static DOCUMENT: OnceLock<Value> = OnceLock::new();
Json(
DOCUMENT
.get_or_init(|| {
let mut value: Value = serde_json::from_str(include_str!("openapi.json"))
.expect("validated embedded OpenAPI");
value["info"]["version"] = Value::String(env!("CARGO_PKG_VERSION").into());
value
})
.clone(),
)
}
pub(super) fn asset(name: &'static str) -> Response {
// Bytes are embedded for debug AND release, and always served through the
// authenticated router. Never read a file or fetch a CDN on an HTTP request.
let (content_type, bytes): (&'static str, &'static [u8]) = match name {
"swagger-ui-bundle.js" => (
"text/javascript; charset=utf-8",
include_bytes!(concat!(env!("OUT_DIR"), "/swagger-ui/swagger-ui-bundle.js")),
),
"swagger-ui.css" => (
"text/css; charset=utf-8",
include_bytes!(concat!(env!("OUT_DIR"), "/swagger-ui/swagger-ui.css")),
),
"licenses.txt" => (
"text/plain; charset=utf-8",
include_bytes!(concat!(env!("OUT_DIR"), "/swagger-ui/licenses.txt")),
),
_ => return StatusCode::NOT_FOUND.into_response(),
};
([("content-type", content_type)], bytes).into_response()
}
#[cfg(test)]
include!("documentation/tests.rs");