107 lines
3.6 KiB
Rust
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");
|