Skip to main content

doxa_docs/
doc_responses.rs

1//! Role trait through which a handler's return type contributes the
2//! success response to an OpenAPI [`Operation`].
3//!
4//! Parallel to the per-argument role traits in [`crate::doc_traits`] —
5//! query/path/header/request-body/security are per-argument, whereas
6//! the response body is per-return-type. The method-shortcut macros
7//! invoke the trait via autoref-specialized dispatch (see
8//! [`crate::__private::ResponseBodyContribution`]) once per handler,
9//! so types that do not implement [`DocResponseBody`] silently
10//! contribute nothing instead of failing compilation.
11//!
12//! # Extension for third-party response wrappers
13//!
14//! Implement [`DocResponseBody`] on the type a handler returns. The
15//! impl mutates the operation's 200 response to add the appropriate
16//! content-type and schema reference, and optionally registers any
17//! referenced schemas on the output vector so they land in
18//! `components.schemas`.
19//!
20//! ```ignore
21//! use doxa::DocResponseBody;
22//! use utoipa::openapi::path::Operation;
23//! use utoipa::openapi::{RefOr, Schema};
24//!
25//! pub struct Csv<T>(pub T);
26//!
27//! impl<T: utoipa::PartialSchema + 'static> DocResponseBody for Csv<T> {
28//!     fn describe(op: &mut Operation, _: &mut Vec<(String, RefOr<Schema>)>) {
29//!         // add a 200 / text/csv / $ref to T here
30//!         let _ = op;
31//!     }
32//! }
33//! ```
34
35use utoipa::openapi::path::Operation;
36use utoipa::openapi::response::ResponseBuilder;
37use utoipa::openapi::{Content, RefOr, Schema};
38
39/// Describe how a handler's return type contributes to its
40/// OpenAPI operation's 200 response.
41///
42/// Invoked once per handler at spec-build time via the
43/// [`crate::__private::ResponseBodyContribution`] autoref dispatch.
44/// Implementors mutate `op.responses.responses` (typically inserting
45/// an entry at `"200"`) and append any schema components the response
46/// references to `schemas` so they can be registered on
47/// `components.schemas` by the surrounding
48/// [`crate::ApidocHandlerSchemas`] machinery.
49///
50/// The blanket impl on [`Result<Ok, Err>`] means handlers returning
51/// `Result<Foo, MyError>` transparently defer to `Foo`'s impl; the
52/// error half is handled separately by utoipa's
53/// [`utoipa::IntoResponses`] from the macro's existing `responses(E)`
54/// emission.
55pub trait DocResponseBody {
56    /// Add the success response entry to `op` and append any referenced
57    /// schemas to `schemas`.
58    fn describe(op: &mut Operation, schemas: &mut Vec<(String, RefOr<Schema>)>);
59}
60
61// ---------------------------------------------------------------------------
62// axum::Json<T> — 200 application/json
63// ---------------------------------------------------------------------------
64
65impl<T> DocResponseBody for axum::Json<T>
66where
67    T: utoipa::PartialSchema + utoipa::ToSchema + 'static,
68{
69    fn describe(op: &mut Operation, schemas: &mut Vec<(String, RefOr<Schema>)>) {
70        if looks_nominal::<T>() {
71            // Nominal types (objects, enums) get a `$ref` to
72            // `components.schemas.<name>` plus the schema registered
73            // there — matches utoipa's native `body = T` output for
74            // doc compactness.
75            register_schema::<T>(schemas);
76            insert_ref_json_200::<T>(op);
77        } else {
78            // Generic containers (Vec<T>, Option<T>, …) lack a nominal
79            // component name of their own — render their schema
80            // inline. utoipa's derived `PartialSchema::schema()`
81            // already embeds `$ref`s to their nominal element types
82            // where appropriate, so nested refs still resolve.
83            insert_inline_json_200::<T>(op);
84            <T as utoipa::ToSchema>::schemas(schemas);
85        }
86    }
87}
88
89// ---------------------------------------------------------------------------
90// SseStream<E, S> — 200 text/event-stream with x-sse-stream marker
91// ---------------------------------------------------------------------------
92
93impl<E, S> DocResponseBody for crate::SseStream<E, S>
94where
95    E: utoipa::PartialSchema + utoipa::ToSchema + 'static,
96{
97    fn describe(op: &mut Operation, schemas: &mut Vec<(String, RefOr<Schema>)>) {
98        insert_sse_200::<E>(op);
99        register_schema::<E>(schemas);
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Result<Ok, Err> — passthrough on the success side
105// ---------------------------------------------------------------------------
106
107impl<Ok, Err> DocResponseBody for Result<Ok, Err>
108where
109    Ok: DocResponseBody,
110{
111    fn describe(op: &mut Operation, schemas: &mut Vec<(String, RefOr<Schema>)>) {
112        <Ok as DocResponseBody>::describe(op, schemas)
113    }
114}
115
116// ---------------------------------------------------------------------------
117// Helpers
118// ---------------------------------------------------------------------------
119
120fn insert_ref_json_200<T>(op: &mut Operation)
121where
122    T: utoipa::ToSchema,
123{
124    if op.responses.responses.contains_key("200") {
125        // Caller supplied an explicit override via `responses(...)` —
126        // don't overwrite.
127        return;
128    }
129    let name = <T as utoipa::ToSchema>::name();
130    let reference = RefOr::Ref(utoipa::openapi::Ref::new(format!(
131        "#/components/schemas/{name}"
132    )));
133    let content = Content::new(Some(reference));
134    let response = ResponseBuilder::new()
135        .description("")
136        .content("application/json", content)
137        .build();
138    op.responses
139        .responses
140        .insert("200".to_string(), RefOr::T(response));
141}
142
143fn insert_inline_json_200<T>(op: &mut Operation)
144where
145    T: utoipa::PartialSchema,
146{
147    if op.responses.responses.contains_key("200") {
148        return;
149    }
150    let content = Content::new(Some(<T as utoipa::PartialSchema>::schema()));
151    let response = ResponseBuilder::new()
152        .description("")
153        .content("application/json", content)
154        .build();
155    op.responses
156        .responses
157        .insert("200".to_string(), RefOr::T(response));
158}
159
160fn insert_sse_200<E>(op: &mut Operation)
161where
162    E: utoipa::PartialSchema + utoipa::ToSchema,
163{
164    if op.responses.responses.contains_key("200") {
165        return;
166    }
167    // SSE responses use a `$ref` directly to the event enum by name —
168    // the 3.2 post-process in `ApiDocBuilder::build` relies on that
169    // shape to rewrite `schema` → `itemSchema`. Nominal tagged enums
170    // always have a non-empty `ToSchema::name`.
171    let name = <E as utoipa::ToSchema>::name();
172    let schema = if name.is_empty() {
173        <E as utoipa::PartialSchema>::schema()
174    } else {
175        RefOr::Ref(utoipa::openapi::Ref::new(format!(
176            "#/components/schemas/{name}"
177        )))
178    };
179    let content = Content::new(Some(schema));
180    let response = ResponseBuilder::new()
181        .description("")
182        .content("text/event-stream", content)
183        .build();
184    op.responses
185        .responses
186        .insert("200".to_string(), RefOr::T(response));
187    // Tag the text/event-stream content entry so
188    // `ApiDocBuilder::build`'s post-process can rewrite it under the
189    // selected `SseSpecVersion`. See `crate::sse::mark_sse_response`.
190    crate::sse::mark_sse_response(op);
191}
192
193/// Runtime heuristic that distinguishes a "nominal" schema type
194/// (struct, enum, union — user-derived with `#[derive(ToSchema)]`)
195/// from a generic container type (`Vec<T>`, `Option<T>`, arrays, …).
196///
197/// utoipa's macro layer distinguishes these at compile time via type
198/// tree analysis. At runtime we only have the schema value and the
199/// name, so we serialize `T`'s schema and inspect its shape:
200///
201/// - `"type": "array"` → container, **not** nominal.
202/// - `"$ref": "…"` → already a reference; treat as nominal.
203/// - `"oneOf"` / `"allOf"` / `"anyOf"` → tagged enum / polymorphic → nominal.
204/// - Otherwise (including `"type": "object"` and the absence of a `"type"` key)
205///   → nominal.
206///
207/// Nominal types get a `$ref` response and are registered on
208/// `components.schemas`; non-nominal types render inline.
209fn looks_nominal<T: utoipa::PartialSchema + utoipa::ToSchema>() -> bool {
210    if <T as utoipa::ToSchema>::name().is_empty() {
211        return false;
212    }
213    let schema = <T as utoipa::PartialSchema>::schema();
214    let Ok(value) = serde_json::to_value(&schema) else {
215        return false;
216    };
217    let Some(obj) = value.as_object() else {
218        return false;
219    };
220    if obj.contains_key("$ref") {
221        return true;
222    }
223    !matches!(obj.get("type"), Some(serde_json::Value::String(s)) if s == "array")
224}
225
226/// Register a nominal type `T` under its [`utoipa::ToSchema::name`] in
227/// the component-schemas collection, plus every schema transitively
228/// referenced by it.
229///
230/// `ToSchema::schemas` walks transitive dependencies but does not
231/// always include the root type itself (particularly for
232/// `#[serde(tag, content)]` enums). We insert the root under its
233/// name so `$ref`s resolve. Callers should only invoke this for
234/// types that [`looks_nominal`] considers nominal — passing a
235/// container like `Vec<T>` would register a bogus `Vec` component.
236fn register_schema<T: utoipa::PartialSchema + utoipa::ToSchema>(
237    out: &mut Vec<(String, RefOr<utoipa::openapi::Schema>)>,
238) {
239    let name = <T as utoipa::ToSchema>::name();
240    if !name.is_empty() && !out.iter().any(|(n, _)| n == name.as_ref()) {
241        out.push((name.into_owned(), <T as utoipa::PartialSchema>::schema()));
242    }
243    <T as utoipa::ToSchema>::schemas(out);
244}