use utoipa::openapi::path::Operation;
use utoipa::openapi::response::ResponseBuilder;
use utoipa::openapi::{Content, RefOr, Schema};
pub trait DocResponseBody {
fn describe(op: &mut Operation, schemas: &mut Vec<(String, RefOr<Schema>)>);
}
impl<T> DocResponseBody for axum::Json<T>
where
T: utoipa::PartialSchema + utoipa::ToSchema + 'static,
{
fn describe(op: &mut Operation, schemas: &mut Vec<(String, RefOr<Schema>)>) {
if looks_nominal::<T>() {
register_schema::<T>(schemas);
insert_ref_json_200::<T>(op);
} else {
insert_inline_json_200::<T>(op);
<T as utoipa::ToSchema>::schemas(schemas);
}
}
}
impl<E, S> DocResponseBody for crate::SseStream<E, S>
where
E: utoipa::PartialSchema + utoipa::ToSchema + 'static,
{
fn describe(op: &mut Operation, schemas: &mut Vec<(String, RefOr<Schema>)>) {
insert_sse_200::<E>(op);
register_schema::<E>(schemas);
}
}
impl<Ok, Err> DocResponseBody for Result<Ok, Err>
where
Ok: DocResponseBody,
{
fn describe(op: &mut Operation, schemas: &mut Vec<(String, RefOr<Schema>)>) {
<Ok as DocResponseBody>::describe(op, schemas)
}
}
fn insert_ref_json_200<T>(op: &mut Operation)
where
T: utoipa::ToSchema,
{
if op.responses.responses.contains_key("200") {
return;
}
let name = <T as utoipa::ToSchema>::name();
let reference = RefOr::Ref(utoipa::openapi::Ref::new(format!(
"#/components/schemas/{name}"
)));
let content = Content::new(Some(reference));
let response = ResponseBuilder::new()
.description("")
.content("application/json", content)
.build();
op.responses
.responses
.insert("200".to_string(), RefOr::T(response));
}
fn insert_inline_json_200<T>(op: &mut Operation)
where
T: utoipa::PartialSchema,
{
if op.responses.responses.contains_key("200") {
return;
}
let content = Content::new(Some(<T as utoipa::PartialSchema>::schema()));
let response = ResponseBuilder::new()
.description("")
.content("application/json", content)
.build();
op.responses
.responses
.insert("200".to_string(), RefOr::T(response));
}
fn insert_sse_200<E>(op: &mut Operation)
where
E: utoipa::PartialSchema + utoipa::ToSchema,
{
if op.responses.responses.contains_key("200") {
return;
}
let name = <E as utoipa::ToSchema>::name();
let schema = if name.is_empty() {
<E as utoipa::PartialSchema>::schema()
} else {
RefOr::Ref(utoipa::openapi::Ref::new(format!(
"#/components/schemas/{name}"
)))
};
let content = Content::new(Some(schema));
let response = ResponseBuilder::new()
.description("")
.content("text/event-stream", content)
.build();
op.responses
.responses
.insert("200".to_string(), RefOr::T(response));
crate::sse::mark_sse_response(op);
}
fn looks_nominal<T: utoipa::PartialSchema + utoipa::ToSchema>() -> bool {
if <T as utoipa::ToSchema>::name().is_empty() {
return false;
}
let schema = <T as utoipa::PartialSchema>::schema();
let Ok(value) = serde_json::to_value(&schema) else {
return false;
};
let Some(obj) = value.as_object() else {
return false;
};
if obj.contains_key("$ref") {
return true;
}
!matches!(obj.get("type"), Some(serde_json::Value::String(s)) if s == "array")
}
fn register_schema<T: utoipa::PartialSchema + utoipa::ToSchema>(
out: &mut Vec<(String, RefOr<utoipa::openapi::Schema>)>,
) {
let name = <T as utoipa::ToSchema>::name();
if !name.is_empty() && !out.iter().any(|(n, _)| n == name.as_ref()) {
out.push((name.into_owned(), <T as utoipa::PartialSchema>::schema()));
}
<T as utoipa::ToSchema>::schemas(out);
}