use std::collections::BTreeMap;
use bytes::Bytes;
use uuid::Uuid;
use crate::apidocs::{ApiDocGenerator, ApiMeta, DocViewer};
use crate::callables::Operation;
use crate::routes::AxumRouter;
use crate::Site;
use super::{Bundle, BundleError};
#[derive(Clone, Debug)]
pub struct OpenApiConf {
pub doc_path: String,
pub spec_path: String,
pub meta: ApiMeta,
pub viewer: DocViewer,
}
pub(super) struct DocNode {
spec_op_id: Uuid,
doc_op_id: Uuid,
operation_ids: Vec<Uuid>,
meta: ApiMeta,
viewer: DocViewer,
}
pub(crate) struct DocEngine {
nodes: Vec<DocNode>,
}
impl DocEngine {
pub(super) fn new() -> Self {
Self { nodes: Vec::new() }
}
pub(super) fn register(&mut self, node: DocNode) {
self.nodes.push(node);
}
pub(crate) fn merge(&mut self, other: DocEngine) {
self.nodes.extend(other.nodes);
}
pub(crate) fn setup(
&self,
router: &mut AxumRouter<Site>,
ops: &BTreeMap<Uuid, Operation>,
) -> Result<(), BundleError> {
for node in &self.nodes {
let spec_path = ops
.get(&node.spec_op_id)
.map(|op| op.path.clone())
.unwrap_or_else(|| node.spec_op_id.to_string());
let doc_path = ops
.get(&node.doc_op_id)
.map(|op| op.path.clone())
.unwrap_or_else(|| node.doc_op_id.to_string());
let views: Vec<&Operation> = node
.operation_ids
.iter()
.filter_map(|id| ops.get(id))
.collect();
let spec_bytes = generate_spec(&views, &node.meta)?;
let viewer_html = generate_viewer(&doc_path, &spec_path, node.viewer);
let spec_route = {
let b = spec_bytes;
axum::routing::get(move || {
let body = b.clone();
async move {
use axum::http::{StatusCode, header};
(StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], body)
}
})
};
let viewer_route = {
let h = viewer_html;
axum::routing::get(move || {
let body = h.clone();
async move {
use axum::http::{StatusCode, header};
(StatusCode::OK, [(header::CONTENT_TYPE, "text/html; charset=utf-8")], body)
}
})
};
*router = std::mem::take(router)
.route(&spec_path, spec_route)
.route(&doc_path, viewer_route);
}
Ok(())
}
}
impl Bundle {
pub fn with_openapi(mut self, conf: OpenApiConf) -> Self {
let operation_ids: Vec<Uuid> = self
.ops
.values()
.filter(|op| !op.hidden)
.map(|op| op.id)
.collect();
let spec_op = crate::callables::Operation::from_api_doc(
&format!("__spec__{}", conf.spec_path),
&conf.spec_path,
);
let doc_op = crate::callables::Operation::from_api_doc(
&format!("__doc__{}", conf.doc_path),
&conf.doc_path,
);
let spec_op_id = spec_op.id;
let doc_op_id = doc_op.id;
self.ops.insert(spec_op_id, spec_op);
self.ops.insert(doc_op_id, doc_op);
self.doc_engine.register(DocNode {
spec_op_id,
doc_op_id,
operation_ids,
meta: conf.meta,
viewer: conf.viewer,
});
self
}
}
fn generate_spec(views: &[&Operation], meta: &ApiMeta) -> Result<Bytes, BundleError> {
let doc_gen = ApiDocGenerator::new(meta.clone());
let api = doc_gen.generate(views).map_err(|e| BundleError::DocGen(e.to_string()))?;
let vec = serde_json::to_vec(&api).map_err(|e| BundleError::DocGen(e.to_string()))?;
Ok(Bytes::from(vec))
}
fn generate_viewer(doc_path: &str, spec_path: &str, viewer: DocViewer) -> String {
let from_dir = doc_path.rfind('/').map(|i| &doc_path[..=i]).unwrap_or("/");
let relative = spec_path
.strip_prefix(from_dir)
.unwrap_or(spec_path);
let html = ApiDocGenerator::serve_doc(relative, viewer);
html.0
}