Skip to main content

doxa_docs/
doc_traits.rs

1//! Role-based traits through which handler-argument types contribute
2//! to an OpenAPI [`Operation`]. Mirrors how axum splits request
3//! handling across [`FromRequestParts`][frp] / [`FromRequest`][fr]:
4//! each extractor plays one or more documentation roles, and each
5//! role is one trait.
6//!
7//! [frp]: https://docs.rs/axum/latest/axum/extract/trait.FromRequestParts.html
8//! [fr]: https://docs.rs/axum/latest/axum/extract/trait.FromRequest.html
9//!
10//! ## Why separate traits per role
11//!
12//! A single unified trait would force every implementor to opt in or
13//! out of every role up-front and would collide under coherence when
14//! multiple blanket impls tried to cover overlapping wrapper types.
15//! Splitting by role lets a transparent wrapper such as `Valid<T>`
16//! forward each role independently without coupling them.
17//!
18//! ## Extension for third-party extractors
19//!
20//! A wrapper that is semantically transparent (forwards all behavior
21//! to its inner extractor) adds one blanket impl per role it wants to
22//! surface. For example:
23//!
24//! ```ignore
25//! impl<T: DocQueryParams> DocQueryParams for MyValidator<T> {
26//!     fn describe(op: &mut Operation) { T::describe(op) }
27//! }
28//! ```
29//!
30//! The method macro emits unconditional trait calls for every handler
31//! argument via autoref-specialized dispatch (see
32//! [`crate::__private`]); types that do not implement a given role
33//! silently no-op instead of failing compilation.
34//!
35//! ## Path parameter names
36//!
37//! The method macro parses `{name}` segments from the route template
38//! and passes them to [`DocPathParams::describe`] as the
39//! `path_param_names` slice. Struct-form `Path<T: IntoParams>` ignores
40//! the slice (names come from the struct). Scalar and tuple `Path<T>`
41//! use the slice by index — a scalar `Path<Uuid>` takes names[0]; a
42//! tuple `Path<(A, B)>` takes names[0] and names[1] — because the
43//! URL-template segment names, not the handler's binding names, are
44//! authoritative in OpenAPI.
45
46use utoipa::openapi::path::{Operation, ParameterBuilder, ParameterIn};
47use utoipa::openapi::schema::{KnownFormat, SchemaFormat};
48use utoipa::openapi::{ObjectBuilder, RefOr, Required, Schema, Type};
49use utoipa::{IntoParams, PartialSchema};
50
51use crate::headers::DocumentedHeader;
52
53/// Contributes query parameters to an operation.
54///
55/// Implemented by [`axum::extract::Query<T>`] for any `T: IntoParams`,
56/// and by transparent wrappers (e.g. `Valid<Query<T>>`) via blanket
57/// impls that forward to the inner type.
58pub trait DocQueryParams {
59    /// Append this extractor's query parameters to `op.parameters`.
60    fn describe(op: &mut Operation);
61}
62
63/// Contributes path parameters to an operation.
64///
65/// Handles three shapes via separate impls:
66/// - struct form — `Path<T: IntoParams>` with `#[into_params(parameter_in =
67///   Path)]` on `T`; names come from the struct's field identifiers.
68/// - scalar form — `Path<T: PathScalar>` for primitives; `names[0]` provides
69///   the parameter name from the route template.
70/// - tuple form — `Path<(T1, …, Tn)>` where every element is `PathScalar`;
71///   `names[i]` provides the i-th parameter name.
72pub trait DocPathParams {
73    /// Append path parameters to `op.parameters`.
74    ///
75    /// `path_param_names` is the ordered list of `{name}` segments
76    /// parsed from the route template.
77    fn describe(op: &mut Operation, path_param_names: &[&'static str]);
78}
79
80/// Sealed trait for primitives usable as scalar `Path` parameters.
81/// Implementations supply the OpenAPI schema for the parameter —
82/// done manually (rather than via `utoipa::PartialSchema`) because
83/// common scalar path types like `Uuid` are recognized by utoipa
84/// only via token inspection in derives, not through a `PartialSchema`
85/// impl.
86pub trait PathScalar: sealed::Sealed {
87    /// OpenAPI schema to embed for this scalar parameter.
88    fn path_scalar_schema() -> RefOr<Schema>;
89}
90
91mod sealed {
92    pub trait Sealed {}
93}
94
95/// Contributes header parameters to an operation.
96pub trait DocHeaderParams {
97    /// Append header parameters to `op.parameters`.
98    fn describe(op: &mut Operation);
99}
100
101/// Contributes the request body schema to an operation.
102///
103/// Only one extractor per handler should implement this — a handler
104/// with two request bodies is ill-formed.
105pub trait DocRequestBody {
106    /// Set `op.request_body` to describe the body this extractor consumes.
107    fn describe(op: &mut Operation);
108}
109
110/// Extractor-side contribution of per-operation security/permission
111/// metadata. Implemented by per-route guards (e.g. a permission
112/// extractor that names the action it requires) so the resulting
113/// OpenAPI operation documents the requirement.
114///
115/// Prefer this over [`crate::DocumentedLayer`] when the requirement
116/// varies per handler — `DocumentedLayer` stamps the same contribution
117/// on every operation a layer covers, which is the right tool for
118/// blanket "must be authenticated" declarations but the wrong tool for
119/// per-route permissions.
120///
121/// Implementations typically emit both a standard
122/// [`SecurityRequirement`](utoipa::openapi::security::SecurityRequirement)
123/// (so OpenAPI codegen sees the required scope) and an
124/// `x-required-permissions` extension (so doc UIs surface a
125/// human-readable badge). The
126/// [`crate::record_required_permission`] helper does the dual write
127/// in one call.
128pub trait DocOperationSecurity {
129    /// Append this extractor's security/permission metadata to `op`.
130    fn describe(op: &mut Operation);
131}
132
133// ---------------------------------------------------------------------------
134// Built-in extractor impls
135// ---------------------------------------------------------------------------
136
137impl<T: IntoParams> DocQueryParams for axum::extract::Query<T> {
138    fn describe(op: &mut Operation) {
139        let params = T::into_params(|| Some(ParameterIn::Query));
140        if params.is_empty() {
141            return;
142        }
143        op.parameters.get_or_insert_with(Vec::new).extend(params);
144    }
145}
146
147/// Struct-form path impl — names come from `T::into_params`.
148impl<T: IntoParams> DocPathParams for axum::extract::Path<T> {
149    fn describe(op: &mut Operation, _path_param_names: &[&'static str]) {
150        let params = T::into_params(|| Some(ParameterIn::Path));
151        if params.is_empty() {
152            return;
153        }
154        op.parameters.get_or_insert_with(Vec::new).extend(params);
155    }
156}
157
158/// Helper: push one scalar path parameter built from schema `T` and a
159/// route-template name.
160fn push_scalar_path<T: PathScalar>(op: &mut Operation, name: &str) {
161    let param = ParameterBuilder::new()
162        .name(name)
163        .parameter_in(ParameterIn::Path)
164        .required(Required::True)
165        .schema(Some(T::path_scalar_schema()))
166        .build();
167    op.parameters.get_or_insert_with(Vec::new).push(param);
168}
169
170/// Build an inline schema of a given OpenAPI [`Type`], optionally
171/// with a known format hint (e.g. `int32`).
172fn scalar_schema(ty: Type, fmt: Option<KnownFormat>) -> RefOr<Schema> {
173    let mut b = ObjectBuilder::new().schema_type(ty);
174    if let Some(f) = fmt {
175        b = b.format(Some(SchemaFormat::KnownFormat(f)));
176    }
177    RefOr::T(Schema::Object(b.build()))
178}
179
180/// Fetch `path_param_names[index]` or fall back to a synthetic name.
181/// The fallback keeps the spec syntactically valid when the handler's
182/// pattern arity disagrees with the URL template.
183fn name_at(names: &[&'static str], index: usize) -> String {
184    names
185        .get(index)
186        .map(|s| (*s).to_string())
187        .unwrap_or_else(|| format!("param{index}"))
188}
189
190// `PathScalar` impls for primitives and well-known types. Schemas
191// mirror what utoipa emits for the same types in a struct-derived
192// schema (see utoipa-gen `schema_type::SchemaTypeInner` for the
193// reference set).
194macro_rules! impl_path_scalar {
195    ($($t:ty => ($ty_enum:expr, $fmt:expr)),* $(,)?) => {
196        $(
197            impl sealed::Sealed for $t {}
198            impl PathScalar for $t {
199                fn path_scalar_schema() -> RefOr<Schema> {
200                    scalar_schema($ty_enum, $fmt)
201                }
202            }
203        )*
204    };
205}
206
207impl_path_scalar!(
208    bool => (Type::Boolean, None),
209    i8 => (Type::Integer, Some(KnownFormat::Int32)),
210    i16 => (Type::Integer, Some(KnownFormat::Int32)),
211    i32 => (Type::Integer, Some(KnownFormat::Int32)),
212    i64 => (Type::Integer, Some(KnownFormat::Int64)),
213    i128 => (Type::Integer, None),
214    isize => (Type::Integer, None),
215    u8 => (Type::Integer, Some(KnownFormat::Int32)),
216    u16 => (Type::Integer, Some(KnownFormat::Int32)),
217    u32 => (Type::Integer, Some(KnownFormat::Int32)),
218    u64 => (Type::Integer, Some(KnownFormat::Int64)),
219    u128 => (Type::Integer, None),
220    usize => (Type::Integer, None),
221    f32 => (Type::Number, Some(KnownFormat::Float)),
222    f64 => (Type::Number, Some(KnownFormat::Double)),
223    String => (Type::String, None),
224);
225
226impl sealed::Sealed for uuid::Uuid {}
227impl PathScalar for uuid::Uuid {
228    fn path_scalar_schema() -> RefOr<Schema> {
229        scalar_schema(Type::String, Some(KnownFormat::Uuid))
230    }
231}
232
233// Note: scalar `Path<T>` must NOT overlap with the struct-form impl
234// `Path<T: IntoParams>` above. Rust coherence rejects two blanket
235// impls with overlapping bounds, so we emit scalar and tuple impls
236// via a separate trait [`DocPathParamsScalarOrTuple`] that
237// `DocPathParams for &Path<...>` lifts — the autoref-specialization
238// layer in [`crate::__private`] selects between them. The struct
239// impl wins when `T: IntoParams`; the scalar/tuple impl wins
240// otherwise via the probe's fallback chain.
241//
242// For now we provide scalar/tuple support via direct impls at a
243// *different* receiver ref-depth than the struct impl, using a
244// distinct trait below. The method macro's dispatch struct invokes
245// both probes; at most one contributes.
246
247/// Scalar/tuple path impl trait. Kept separate from
248/// [`DocPathParams`] to avoid overlap with the struct-form impl.
249pub trait DocPathScalar {
250    /// Append scalar/tuple path parameter(s) to `op.parameters`.
251    fn describe_scalar(op: &mut Operation, path_param_names: &[&'static str]);
252}
253
254impl<T: PathScalar> DocPathScalar for axum::extract::Path<T> {
255    fn describe_scalar(op: &mut Operation, path_param_names: &[&'static str]) {
256        let name = name_at(path_param_names, 0);
257        push_scalar_path::<T>(op, &name);
258    }
259}
260
261// Tuple arity impls via declarative macro — mirrors axum's
262// `all_the_tuples!` pattern so new arities stay easy to add.
263macro_rules! impl_tuple_path {
264    ($($idx:tt => $T:ident),+ $(,)?) => {
265        impl<$($T: PathScalar),+> DocPathScalar for axum::extract::Path<($($T,)+)> {
266            fn describe_scalar(op: &mut Operation, path_param_names: &[&'static str]) {
267                $(
268                    let name = name_at(path_param_names, $idx);
269                    push_scalar_path::<$T>(op, &name);
270                )+
271            }
272        }
273    };
274}
275
276impl_tuple_path!(0 => T1, 1 => T2);
277impl_tuple_path!(0 => T1, 1 => T2, 2 => T3);
278impl_tuple_path!(0 => T1, 1 => T2, 2 => T3, 3 => T4);
279impl_tuple_path!(0 => T1, 1 => T2, 2 => T3, 3 => T4, 4 => T5);
280impl_tuple_path!(0 => T1, 1 => T2, 2 => T3, 3 => T4, 4 => T5, 5 => T6);
281impl_tuple_path!(0 => T1, 1 => T2, 2 => T3, 3 => T4, 4 => T5, 5 => T6, 6 => T7);
282impl_tuple_path!(0 => T1, 1 => T2, 2 => T3, 3 => T4, 4 => T5, 5 => T6, 6 => T7, 7 => T8);
283
284impl<H: DocumentedHeader> DocHeaderParams for crate::extractor::Header<H> {
285    fn describe(op: &mut Operation) {
286        use utoipa::openapi::path::{ParameterBuilder, ParameterIn as InLoc};
287        use utoipa::openapi::{ObjectBuilder, RefOr, Required, Schema, Type};
288        let mut b = ParameterBuilder::new()
289            .name(H::name())
290            .parameter_in(InLoc::Header)
291            .required(Required::True)
292            .schema(Some(RefOr::T(Schema::Object(
293                ObjectBuilder::new().schema_type(Type::String).build(),
294            ))));
295        let desc = H::description();
296        if !desc.is_empty() {
297            b = b.description(Some(desc.to_string()));
298        }
299        if let Some(ex) = H::example() {
300            b = b.example(Some(serde_json::Value::String(ex.to_string())));
301        }
302        op.parameters.get_or_insert_with(Vec::new).push(b.build());
303    }
304}
305
306impl<T: utoipa::ToSchema + PartialSchema + 'static> DocRequestBody for axum::Json<T> {
307    fn describe(op: &mut Operation) {
308        use utoipa::openapi::request_body::RequestBodyBuilder;
309        use utoipa::openapi::{ContentBuilder, Required};
310        let content = ContentBuilder::new().schema(Some(T::schema())).build();
311        let body = RequestBodyBuilder::new()
312            .content("application/json", content)
313            .required(Some(Required::True))
314            .build();
315        op.request_body = Some(body);
316    }
317}