aide/operation.rs
1//! Traits and utilities for schema generation for operations (handlers).
2
3use indexmap::IndexMap;
4use schemars::Schema;
5
6use crate::generate::GenContext;
7use crate::openapi::{
8 self, Operation, Parameter, ParameterData, QueryStyle, ReferenceOr, RequestBody, Response,
9};
10use crate::Error;
11
12#[cfg(feature = "macros")]
13pub use aide_macros::OperationIo;
14
15/// A trait for operation input schema generation.
16///
17/// This must be implemented for all extractors
18/// that appear in documented handlers.
19///
20/// All method implementations are optional.
21///
22/// # Examples
23///
24/// In order to allow an extractor to appear in a handler,
25/// the following is enough:
26///
27/// ```
28/// use aide::OperationInput;
29///
30/// struct MyExtractor;
31///
32/// impl OperationInput for MyExtractor {}
33/// ```
34///
35/// This will enable usage of the extractor in handlers,
36/// but will not add anything to the documentation.
37/// To extend the generated documentation refer to some of the provided
38/// implementations in this crate.
39///
40/// For simpler cases or wrappers the [`OperationIo`] derive macro
41/// can be used to implement this trait.
42#[allow(unused_variables)]
43pub trait OperationInput {
44 /// Modify the operation.
45 ///
46 /// This method gets mutable access to the
47 /// entire operation, it's the implementer's responsibility
48 /// to detect errors and only modify the operation as much as needed.
49 ///
50 /// There are reusable helpers in [`aide::operation`](crate::operation)
51 /// to help with both boilerplate and error detection.
52 fn operation_input(ctx: &mut GenContext, operation: &mut Operation) {}
53
54 /// Inferred early responses are used to document early returns for
55 /// extractors, guards inside handlers. For example these could represent
56 /// JSON parsing errors, authentication failures.
57 ///
58 /// The function is supposed to return `(status code, response)` pairs,
59 /// if the status code is not specified, the response is assumed to be
60 /// a default response.
61 ///
62 /// It's important for the implementation to be idempotent.
63 ///
64 /// See [`OperationOutput::inferred_responses`] for more details.
65 fn inferred_early_responses(
66 ctx: &mut GenContext,
67 operation: &mut Operation,
68 ) -> Vec<(Option<u16>, Response)> {
69 Vec::new()
70 }
71}
72
73impl OperationInput for () {}
74
75macro_rules! impl_operation_input {
76 ( $($ty:ident),* $(,)? ) => {
77 #[allow(non_snake_case)]
78 impl<$($ty,)*> OperationInput for ($($ty,)*)
79 where
80 $( $ty: OperationInput, )*
81 {
82 fn operation_input(ctx: &mut GenContext, operation: &mut Operation) {
83 $(
84 $ty::operation_input(ctx, operation);
85 )*
86 }
87
88 fn inferred_early_responses(
89 ctx: &mut GenContext,
90 operation: &mut Operation,
91 ) -> Vec<(Option<u16>, Response)> {
92 let mut responses = Vec::new();
93 $(
94 responses.extend($ty::inferred_early_responses(ctx, operation));
95 )*
96 responses
97 }
98 }
99 };
100}
101
102all_the_tuples!(impl_operation_input);
103
104#[doc(hidden)]
105pub trait OperationHandler<I: OperationInput, O: OperationOutput> {}
106
107macro_rules! impl_operation_handler {
108 ( $($ty:ident),* $(,)? ) => {
109 #[allow(non_snake_case)]
110 impl<Ret, F, $($ty,)*> OperationHandler<($($ty,)*), Ret::Output> for F
111 where
112 F: FnOnce($($ty,)*) -> Ret,
113 Ret: std::future::Future,
114 Ret::Output: OperationOutput,
115 $( $ty: OperationInput, )*
116 {}
117 };
118}
119
120impl<Ret, F> OperationHandler<(), Ret::Output> for F
121where
122 F: FnOnce() -> Ret,
123 Ret: std::future::Future,
124 Ret::Output: OperationOutput,
125{
126}
127
128all_the_tuples!(impl_operation_handler);
129
130/// A trait for operation output schema generation.
131///
132/// This can be implemented for types that can
133/// describe their own output schema.
134///
135/// All method implementations are optional.
136///
137/// For simpler cases or wrappers the [`OperationIo`] derive macro
138/// can be used to implement this trait.
139#[allow(unused_variables)]
140pub trait OperationOutput {
141 /// The type that is used in examples.
142 ///
143 /// # Examples
144 ///
145 /// In case of `Json<T>`, this should be `T`,
146 /// whereas for `String` it should be `Self`.
147 type Inner;
148
149 /// Return a response documentation for this type,
150 /// alternatively modify the operation if required.
151 ///
152 /// This method gets mutable access to the
153 /// entire operation, it's the implementer's responsibility
154 /// to detect errors and only modify the operation as much as needed.
155 ///
156 /// Note that this function **can be called multiple
157 /// times for the same operation** and should be idempotent.
158 ///
159 /// There are reusable helpers in [`aide::operation`](crate::operation)
160 /// to help with both boilerplate and error detection.
161 fn operation_response(ctx: &mut GenContext, operation: &mut Operation) -> Option<Response> {
162 None
163 }
164
165 /// Inferred responses are used when the type is
166 /// used as a request handler return type.
167 ///
168 /// The function is supposed to return `(status code, response)` pairs,
169 /// if the status code is not specified, the response is assumed to be
170 /// a default response.
171 ///
172 /// As an example `Result<T, E>` could
173 /// return `(Some(200), T::operation_response(..))` and
174 /// `(None, E::operation_response(..))` to indicate
175 /// a successful response and a default error.
176 ///
177 /// This function can be called after or before `operation_response`,
178 /// it's important for the implementation to be idempotent.
179 fn inferred_responses(
180 ctx: &mut GenContext,
181 operation: &mut Operation,
182 ) -> Vec<(Option<u16>, Response)> {
183 Vec::new()
184 }
185}
186
187/// Location of an operation parameter.
188#[allow(missing_docs)]
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum ParamLocation {
191 Query,
192 Path,
193 Header,
194 Cookie,
195}
196
197/// Generate operation parameters from a JSON schema
198/// where the schema is an object, and each
199/// property is a parameter.
200#[tracing::instrument(skip_all)]
201pub fn parameters_from_schema(
202 ctx: &mut GenContext,
203 schema: Schema,
204 location: ParamLocation,
205) -> Vec<Parameter> {
206 let schema = ctx.resolve_schema(&schema);
207
208 let mut params = Vec::new();
209
210 if let Some(obj) = schema.as_object() {
211 for (name, schema) in obj
212 .get("properties")
213 .and_then(|p| p.as_object())
214 .into_iter()
215 .flatten()
216 {
217 let json_schema: Schema = schema
218 .clone()
219 .try_into()
220 .unwrap_or_else(|err| panic!("Failed to convert schema {schema}: {err:?}"));
221
222 match location {
223 ParamLocation::Query => {
224 params.push(Parameter::Query {
225 parameter_data: ParameterData {
226 name: name.clone(),
227 description: json_schema
228 .get("description")
229 .and_then(|d| d.as_str())
230 .map(String::from),
231 required: obj
232 .get("required")
233 .and_then(|r| r.as_array())
234 .is_some_and(|r| r.contains(&name.as_str().into())),
235 format: crate::openapi::ParameterSchemaOrContent::Schema(
236 openapi::SchemaObject {
237 json_schema,
238 example: None,
239 external_docs: None,
240 },
241 ),
242 extensions: Default::default(),
243 deprecated: None,
244 example: None,
245 examples: IndexMap::default(),
246 explode: None,
247 },
248 allow_reserved: false,
249 style: QueryStyle::Form,
250 allow_empty_value: None,
251 });
252 }
253 ParamLocation::Path => {
254 params.push(Parameter::Path {
255 parameter_data: ParameterData {
256 name: name.clone(),
257 description: json_schema
258 .get("description")
259 .and_then(|d| d.as_str())
260 .map(String::from),
261 required: obj
262 .get("required")
263 .and_then(|r| r.as_array())
264 .is_some_and(|r| r.contains(&name.as_str().into())),
265 format: crate::openapi::ParameterSchemaOrContent::Schema(
266 openapi::SchemaObject {
267 json_schema,
268 example: None,
269 external_docs: None,
270 },
271 ),
272 extensions: Default::default(),
273 deprecated: None,
274 example: None,
275 examples: IndexMap::default(),
276 explode: None,
277 },
278 style: openapi::PathStyle::Simple,
279 });
280 }
281 ParamLocation::Header => {
282 params.push(Parameter::Header {
283 parameter_data: ParameterData {
284 name: name.clone(),
285 description: json_schema
286 .get("description")
287 .and_then(|d| d.as_str())
288 .map(String::from),
289 required: obj
290 .get("required")
291 .and_then(|r| r.as_array())
292 .is_some_and(|r| r.contains(&name.as_str().into())),
293 format: crate::openapi::ParameterSchemaOrContent::Schema(
294 openapi::SchemaObject {
295 json_schema,
296 example: None,
297 external_docs: None,
298 },
299 ),
300 extensions: Default::default(),
301 deprecated: None,
302 example: None,
303 examples: IndexMap::default(),
304 explode: None,
305 },
306 style: openapi::HeaderStyle::Simple,
307 });
308 }
309 ParamLocation::Cookie => {
310 params.push(Parameter::Cookie {
311 parameter_data: ParameterData {
312 name: name.clone(),
313 description: json_schema
314 .get("description")
315 .and_then(|d| d.as_str())
316 .map(String::from),
317 required: obj
318 .get("required")
319 .and_then(|r| r.as_array())
320 .is_some_and(|r| r.contains(&name.as_str().into())),
321 format: crate::openapi::ParameterSchemaOrContent::Schema(
322 openapi::SchemaObject {
323 json_schema,
324 example: None,
325 external_docs: None,
326 },
327 ),
328 extensions: Default::default(),
329 deprecated: None,
330 example: None,
331 examples: IndexMap::default(),
332 explode: None,
333 },
334 style: openapi::CookieStyle::Form,
335 });
336 }
337 }
338 }
339 }
340
341 params
342}
343
344/// Set the body of an operation while
345/// reporting errors.
346pub fn set_body(ctx: &mut GenContext, operation: &mut Operation, body: RequestBody) {
347 if operation.request_body.is_some() {
348 ctx.error(Error::DuplicateRequestBody);
349 }
350 operation.request_body = Some(ReferenceOr::Item(body));
351}
352
353/// Add parameters to an operation while
354/// reporting errors.
355pub fn add_parameters(
356 ctx: &mut GenContext,
357 operation: &mut Operation,
358 params: impl IntoIterator<Item = Parameter>,
359) {
360 for param in params {
361 if operation.parameters.iter().any(|p| match p {
362 ReferenceOr::Reference { .. } => false,
363 ReferenceOr::Item(p) => p.parameter_data_ref().name == param.parameter_data_ref().name,
364 }) {
365 ctx.error(Error::DuplicateParameter(
366 param.parameter_data_ref().name.clone(),
367 ));
368 }
369 operation.parameters.push(ReferenceOr::Item(param));
370 }
371}