use axum::extract::Request;
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use serde_json::{Map, Value};
use crate::errors;
use crate::registry::Registry;
use std::sync::Arc;
#[derive(Clone)]
pub struct OpenApiDoc(Arc<Value>);
impl axum::handler::Handler<(), ()> for OpenApiDoc {
type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Response> + Send>>;
fn call(self, _req: Request, _state: ()) -> Self::Future {
Box::pin(async move { respond_doc(&self.0) })
}
}
pub fn openapi_handler(reg: &Registry, app_name: &str, app_version: &str) -> OpenApiDoc {
OpenApiDoc(Arc::new(build_doc(reg, app_name, app_version)))
}
fn build_doc(reg: &Registry, app_name: &str, app_version: &str) -> Value {
let mut paths: Map<String, Value> = Map::new();
let mut order: Vec<(String, String, String)> = Vec::new();
for e in reg.all() {
if e.http.skip || e.cli.daemon || e.http.path.is_empty() {
continue;
}
for method in crate::httpapi::http_methods(&e) {
order.push((e.http.path.clone(), method, e.name.clone()));
}
}
order.sort();
for (path, method, name) in order {
let Some(e) = reg.all().into_iter().find(|e| e.name == name) else {
continue;
};
let op = build_operation(&e, &method);
let path_obj = paths
.entry(path)
.or_insert_with(|| Value::Object(Map::new()));
if let Some(obj) = path_obj.as_object_mut() {
obj.insert(method.to_lowercase(), op);
}
}
let mut doc = Map::new();
doc.insert("openapi".to_string(), Value::String("3.0.3".to_string()));
let mut info = Map::new();
let title = if app_name.is_empty() {
"example service".to_string()
} else {
app_name.to_string()
};
let version = if app_version.is_empty() {
"1".to_string()
} else {
app_version.to_string()
};
info.insert("title".to_string(), Value::String(title));
info.insert("version".to_string(), Value::String(version));
doc.insert("info".to_string(), Value::Object(info));
doc.insert("paths".to_string(), Value::Object(paths));
Value::Object(doc)
}
fn build_operation(e: &crate::spec::Entry, method: &str) -> Value {
let mut op = Map::new();
if !e.summary.is_empty() {
op.insert("summary".to_string(), Value::String(e.summary.clone()));
}
if !e.description.is_empty() {
op.insert(
"description".to_string(),
Value::String(e.description.clone()),
);
}
let mut params: Vec<Value> = Vec::new();
for f in &e.root.children {
if f.skip {
continue;
}
let location = if f.http.location.is_empty() {
"query"
} else {
f.http.location.as_str()
};
if location != "path" && location != "query" && location != "header" {
continue;
}
let mut p = Map::new();
p.insert(
"name".to_string(),
Value::String(crate::httpapi::http_name(f).to_string()),
);
p.insert("in".to_string(), Value::String(location.to_string()));
let required = if location == "path" { true } else { f.required };
p.insert("required".to_string(), Value::Bool(required));
if !f.description.is_empty() {
p.insert(
"description".to_string(),
Value::String(f.description.clone()),
);
}
p.insert(
"schema".to_string(),
crate::spec::schema::schema_to_value(&crate::spec::schema::field_schema(f)),
);
params.push(Value::Object(p));
}
if !params.is_empty() {
op.insert("parameters".to_string(), Value::Array(params));
}
if matches!(method, "POST" | "PUT" | "PATCH") {
let body = serde_json::json!({
"content": {
"application/json": {
"schema": crate::spec::schema::schema_to_value(&e.input_schema),
}
}
});
op.insert("requestBody".to_string(), body);
}
let mut ok_resp = Map::new();
ok_resp.insert("description".to_string(), Value::String("ok".to_string()));
if let Some(out) = &e.output_schema {
ok_resp.insert(
"content".to_string(),
serde_json::json!({
"application/json": {
"schema": crate::spec::schema::schema_to_value(out),
}
}),
);
}
let mut responses = Map::new();
responses.insert("200".to_string(), Value::Object(ok_resp));
responses.insert(
"400".to_string(),
serde_json::json!({ "description": errors::Kind::InvalidInput.as_str() }),
);
responses.insert(
"404".to_string(),
serde_json::json!({ "description": errors::Kind::NotFound.as_str() }),
);
responses.insert(
"500".to_string(),
serde_json::json!({ "description": errors::Kind::Internal.as_str() }),
);
op.insert("responses".to_string(), Value::Object(responses));
Value::Object(op)
}
fn respond_doc(doc: &Arc<Value>) -> Response {
let s = serde_json::to_string_pretty(&**doc).unwrap_or_else(|_| "{}".to_string());
let mut b = s;
b.push('\n');
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json; charset=utf-8")],
b,
)
.into_response()
}