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}