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_named_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_named_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::PartialSchema + utoipa::ToSchema,
{
if op.responses.responses.contains_key("200") {
return;
}
let name = schema_component_name::<T>();
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 = schema_component_name::<E>();
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")
}
pub(crate) fn has_collision_prone_name<T: utoipa::ToSchema>() -> bool {
let rust_name = std::any::type_name::<T>();
if !rust_name.contains('<') {
return false;
}
let schema_name = <T as utoipa::ToSchema>::name();
!schema_name.contains('<') && !schema_name.contains('_')
}
pub(crate) fn composed_schema_name<T: utoipa::ToSchema>() -> String {
let rust_name = std::any::type_name::<T>();
let outer = <T as utoipa::ToSchema>::name();
let mut composed = String::from(outer.as_ref());
for segment in split_top_level_generic_args(rust_name) {
composed.push('_');
composed.push_str(last_path_segment(segment));
}
composed
}
fn split_top_level_generic_args(type_name: &str) -> Vec<&str> {
let Some(open) = type_name.find('<') else {
return Vec::new();
};
let Some(close) = type_name.rfind('>') else {
return Vec::new();
};
if close <= open {
return Vec::new();
}
let body = &type_name[open + 1..close];
let mut out = Vec::new();
let mut depth: i32 = 0;
let mut start = 0;
for (i, ch) in body.char_indices() {
match ch {
'<' => depth += 1,
'>' => depth -= 1,
',' if depth == 0 => {
out.push(body[start..i].trim());
start = i + 1;
}
_ => {}
}
}
let tail = body[start..].trim();
if !tail.is_empty() {
out.push(tail);
}
out
}
fn last_path_segment(path: &str) -> &str {
let path = path.trim().trim_start_matches('&').trim();
let prefix_end = path.find('<').unwrap_or(path.len());
let prefix = &path[..prefix_end];
let last_sep = prefix.rfind("::").map(|i| i + 2).unwrap_or(0);
&path[last_sep..]
}
pub(crate) fn register_named_schema<T>(out: &mut Vec<(String, RefOr<utoipa::openapi::Schema>)>)
where
T: utoipa::PartialSchema + utoipa::ToSchema,
{
let name = schema_component_name::<T>();
if !name.is_empty() && !out.iter().any(|(n, _)| *n == name) {
out.push((name, <T as utoipa::PartialSchema>::schema()));
}
<T as utoipa::ToSchema>::schemas(out);
}
pub(crate) fn schema_component_name<T: utoipa::PartialSchema + utoipa::ToSchema>() -> String {
if has_collision_prone_name::<T>() {
composed_schema_name::<T>()
} else {
<T as utoipa::ToSchema>::name().into_owned()
}
}