Skip to main content

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    StatusCode,
10};
11use crate::Error;
12
13#[cfg(feature = "macros")]
14pub use aide_macros::OperationIo;
15
16/// A trait for operation input schema generation.
17///
18/// This must be implemented for all extractors
19/// that appear in documented handlers.
20///
21/// All method implementations are optional.
22///
23/// # Examples
24///
25/// In order to allow an extractor to appear in a handler,
26/// the following is enough:
27///
28/// ```
29/// use aide::OperationInput;
30///
31/// struct MyExtractor;
32///
33/// impl OperationInput for MyExtractor {}
34/// ```
35///
36/// This will enable usage of the extractor in handlers,
37/// but will not add anything to the documentation.
38/// To extend the generated documentation refer to some of the provided
39/// implementations in this crate.
40///
41/// For simpler cases or wrappers the [`OperationIo`] derive macro
42/// can be used to implement this trait.
43#[allow(unused_variables)]
44pub trait OperationInput {
45    /// Modify the operation.
46    ///
47    /// This method gets mutable access to the
48    /// entire operation, it's the implementer's responsibility
49    /// to detect errors and only modify the operation as much as needed.
50    ///
51    /// There are reusable helpers in [`aide::operation`](crate::operation)
52    /// to help with both boilerplate and error detection.
53    fn operation_input(ctx: &mut GenContext, operation: &mut Operation) {}
54
55    /// Inferred early responses are used to document early returns for
56    /// extractors, guards inside handlers. For example these could represent
57    /// JSON parsing errors, authentication failures.
58    ///
59    /// The function is supposed to return `(status code, response)` pairs,
60    /// if the status code is not specified, the response is assumed to be
61    /// a default response.
62    ///
63    /// It's important for the implementation to be idempotent.
64    ///
65    /// See [`OperationOutput::inferred_responses`] for more details.
66    fn inferred_early_responses(
67        ctx: &mut GenContext,
68        operation: &mut Operation,
69    ) -> Vec<(Option<StatusCode>, Response)> {
70        Vec::new()
71    }
72}
73
74impl OperationInput for () {}
75
76macro_rules! impl_operation_input {
77    ( $($ty:ident),* $(,)? ) => {
78        #[allow(non_snake_case)]
79        impl<$($ty,)*> OperationInput for ($($ty,)*)
80        where
81            $( $ty: OperationInput, )*
82        {
83            fn operation_input(ctx: &mut GenContext, operation: &mut Operation) {
84                $(
85                    $ty::operation_input(ctx, operation);
86                )*
87            }
88
89            fn inferred_early_responses(
90                ctx: &mut GenContext,
91                operation: &mut Operation,
92            ) -> Vec<(Option<StatusCode>, Response)> {
93                let mut responses = Vec::new();
94                $(
95                    responses.extend($ty::inferred_early_responses(ctx, operation));
96                )*
97                responses
98            }
99        }
100    };
101}
102
103all_the_tuples!(impl_operation_input);
104
105#[doc(hidden)]
106pub trait OperationHandler<I: OperationInput, O: OperationOutput> {}
107
108macro_rules! impl_operation_handler {
109    ( $($ty:ident),* $(,)? ) => {
110        #[allow(non_snake_case)]
111        impl<Ret, F, $($ty,)*> OperationHandler<($($ty,)*), Ret::Output> for F
112        where
113            F: FnOnce($($ty,)*) -> Ret,
114            Ret: std::future::Future,
115            Ret::Output: OperationOutput,
116            $( $ty: OperationInput, )*
117        {}
118    };
119}
120
121impl<Ret, F> OperationHandler<(), Ret::Output> for F
122where
123    F: FnOnce() -> Ret,
124    Ret: std::future::Future,
125    Ret::Output: OperationOutput,
126{
127}
128
129all_the_tuples!(impl_operation_handler);
130
131/// A trait for operation output schema generation.
132///
133/// This can be implemented for types that can
134/// describe their own output schema.
135///
136/// All method implementations are optional.
137///
138/// For simpler cases or wrappers the [`OperationIo`] derive macro
139/// can be used to implement this trait.
140#[allow(unused_variables)]
141pub trait OperationOutput {
142    /// The type that is used in examples.
143    ///
144    /// # Examples
145    ///
146    /// In case of `Json<T>`, this should be `T`,
147    /// whereas for `String` it should be `Self`.
148    type Inner;
149
150    /// Return a response documentation for this type,
151    /// alternatively modify the operation if required.
152    ///
153    /// This method gets mutable access to the
154    /// entire operation, it's the implementer's responsibility
155    /// to detect errors and only modify the operation as much as needed.
156    ///
157    /// Note that this function **can be called multiple
158    /// times for the same operation** and should be idempotent.
159    ///
160    /// There are reusable helpers in [`aide::operation`](crate::operation)
161    /// to help with both boilerplate and error detection.
162    fn operation_response(ctx: &mut GenContext, operation: &mut Operation) -> Option<Response> {
163        None
164    }
165
166    /// Inferred responses are used when the type is
167    /// used as a request handler return type.
168    ///
169    /// The function is supposed to return `(status code, response)` pairs,
170    /// if the status code is not specified, the response is assumed to be
171    /// a default response.
172    ///
173    /// As an example `Result<T, E>` could
174    /// return `(Some(200), T::operation_response(..))` and
175    /// `(None, E::operation_response(..))` to indicate
176    /// a successful response and a default error.
177    ///
178    /// This function can be called after or before `operation_response`,
179    /// it's important for the implementation to be idempotent.
180    fn inferred_responses(
181        ctx: &mut GenContext,
182        operation: &mut Operation,
183    ) -> Vec<(Option<StatusCode>, Response)> {
184        Vec::new()
185    }
186}
187
188/// Location of an operation parameter.
189#[allow(missing_docs)]
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum ParamLocation {
192    Query,
193    Path,
194    Header,
195    Cookie,
196}
197
198/// Generate operation parameters from a JSON schema
199/// where the schema is an object, and each
200/// property is a parameter.
201#[tracing::instrument(skip_all)]
202pub fn parameters_from_schema(
203    ctx: &mut GenContext,
204    schema: Schema,
205    location: ParamLocation,
206) -> Vec<Parameter> {
207    let schema = ctx.resolve_schema(&schema);
208
209    let mut params = Vec::new();
210
211    if let Some(obj) = schema.as_object() {
212        for (name, schema) in obj
213            .get("properties")
214            .and_then(|p| p.as_object())
215            .into_iter()
216            .flatten()
217        {
218            let json_schema: Schema = schema
219                .clone()
220                .try_into()
221                .unwrap_or_else(|err| panic!("Failed to convert schema {schema}: {err:?}"));
222
223            match location {
224                ParamLocation::Query => {
225                    params.push(Parameter::Query {
226                        parameter_data: ParameterData {
227                            name: name.clone(),
228                            description: json_schema
229                                .get("description")
230                                .and_then(|d| d.as_str())
231                                .map(String::from),
232                            required: obj
233                                .get("required")
234                                .and_then(|r| r.as_array())
235                                .is_some_and(|r| r.contains(&name.as_str().into())),
236                            format: crate::openapi::ParameterSchemaOrContent::Schema(
237                                openapi::SchemaObject {
238                                    json_schema,
239                                    example: None,
240                                    external_docs: None,
241                                },
242                            ),
243                            extensions: Default::default(),
244                            deprecated: None,
245                            example: None,
246                            examples: IndexMap::default(),
247                            explode: None,
248                        },
249                        allow_reserved: false,
250                        style: QueryStyle::Form,
251                        allow_empty_value: None,
252                    });
253                }
254                ParamLocation::Path => {
255                    params.push(Parameter::Path {
256                        parameter_data: ParameterData {
257                            name: name.clone(),
258                            description: json_schema
259                                .get("description")
260                                .and_then(|d| d.as_str())
261                                .map(String::from),
262                            required: obj
263                                .get("required")
264                                .and_then(|r| r.as_array())
265                                .is_some_and(|r| r.contains(&name.as_str().into())),
266                            format: crate::openapi::ParameterSchemaOrContent::Schema(
267                                openapi::SchemaObject {
268                                    json_schema,
269                                    example: None,
270                                    external_docs: None,
271                                },
272                            ),
273                            extensions: Default::default(),
274                            deprecated: None,
275                            example: None,
276                            examples: IndexMap::default(),
277                            explode: None,
278                        },
279                        style: openapi::PathStyle::Simple,
280                    });
281                }
282                ParamLocation::Header => {
283                    params.push(Parameter::Header {
284                        parameter_data: ParameterData {
285                            name: name.clone(),
286                            description: json_schema
287                                .get("description")
288                                .and_then(|d| d.as_str())
289                                .map(String::from),
290                            required: obj
291                                .get("required")
292                                .and_then(|r| r.as_array())
293                                .is_some_and(|r| r.contains(&name.as_str().into())),
294                            format: crate::openapi::ParameterSchemaOrContent::Schema(
295                                openapi::SchemaObject {
296                                    json_schema,
297                                    example: None,
298                                    external_docs: None,
299                                },
300                            ),
301                            extensions: Default::default(),
302                            deprecated: None,
303                            example: None,
304                            examples: IndexMap::default(),
305                            explode: None,
306                        },
307                        style: openapi::HeaderStyle::Simple,
308                    });
309                }
310                ParamLocation::Cookie => {
311                    params.push(Parameter::Cookie {
312                        parameter_data: ParameterData {
313                            name: name.clone(),
314                            description: json_schema
315                                .get("description")
316                                .and_then(|d| d.as_str())
317                                .map(String::from),
318                            required: obj
319                                .get("required")
320                                .and_then(|r| r.as_array())
321                                .is_some_and(|r| r.contains(&name.as_str().into())),
322                            format: crate::openapi::ParameterSchemaOrContent::Schema(
323                                openapi::SchemaObject {
324                                    json_schema,
325                                    example: None,
326                                    external_docs: None,
327                                },
328                            ),
329                            extensions: Default::default(),
330                            deprecated: None,
331                            example: None,
332                            examples: IndexMap::default(),
333                            explode: None,
334                        },
335                        style: openapi::CookieStyle::Form,
336                    });
337                }
338            }
339        }
340    }
341
342    params
343}
344
345/// Set the body of an operation while
346/// reporting errors.
347pub fn set_body(ctx: &mut GenContext, operation: &mut Operation, body: RequestBody) {
348    if operation.request_body.is_some() {
349        ctx.error(Error::DuplicateRequestBody);
350    }
351    operation.request_body = Some(ReferenceOr::Item(body));
352}
353
354/// Add parameters to an operation while
355/// reporting errors.
356pub fn add_parameters(
357    ctx: &mut GenContext,
358    operation: &mut Operation,
359    params: impl IntoIterator<Item = Parameter>,
360) {
361    for param in params {
362        if operation.parameters.iter().any(|p| match p {
363            ReferenceOr::Reference { .. } => false,
364            ReferenceOr::Item(p) => p.parameter_data_ref().name == param.parameter_data_ref().name,
365        }) {
366            ctx.error(Error::DuplicateParameter(
367                param.parameter_data_ref().name.clone(),
368            ));
369        }
370        operation.parameters.push(ReferenceOr::Item(param));
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use crate::generate::GenContext;
377    use crate::openapi::{Operation, Response, StatusCode};
378    use crate::{generate, OperationInput, OperationOutput};
379    use aide_macros::OperationIo;
380    use schemars::JsonSchema;
381
382    fn assert_default_input_impl<T: OperationInput>(ctx: &mut GenContext) {
383        let mut operation = Operation::default();
384
385        T::operation_input(ctx, &mut operation);
386        assert_eq!(operation, Operation::default());
387
388        assert_eq!(T::inferred_early_responses(ctx, &mut operation), Vec::new());
389        assert_eq!(operation, Operation::default());
390    }
391
392    fn assert_default_output_impl<T: OperationOutput<Inner = T>>(ctx: &mut GenContext) {
393        let mut operation = Operation::default();
394
395        assert_eq!(T::operation_response(ctx, &mut operation), None);
396        assert_eq!(operation, Operation::default());
397
398        assert_eq!(T::inferred_responses(ctx, &mut operation), Vec::new());
399        assert_eq!(operation, Operation::default());
400    }
401
402    #[test]
403    fn operation_io() {
404        #[derive(OperationIo)]
405        struct OperationInputOutput;
406        #[derive(OperationIo)]
407        #[aide(input, output)]
408        struct OperationInputOutput2;
409        #[derive(OperationIo)]
410        struct OperationInputOutputGeneric<T>(T);
411        #[derive(OperationIo)]
412        #[aide(input)]
413        struct OperationInput;
414        #[derive(OperationIo)]
415        #[aide(output)]
416        struct OperationOutput;
417
418        generate::in_context(|ctx| {
419            assert_default_input_impl::<OperationInputOutput>(ctx);
420            assert_default_output_impl::<OperationInputOutput>(ctx);
421
422            assert_default_input_impl::<OperationInputOutput2>(ctx);
423            assert_default_output_impl::<OperationInputOutput2>(ctx);
424
425            assert_default_input_impl::<OperationInputOutputGeneric<()>>(ctx);
426            assert_default_output_impl::<OperationInputOutputGeneric<()>>(ctx);
427
428            assert_default_input_impl::<OperationInput>(ctx);
429
430            assert_default_output_impl::<OperationOutput>(ctx);
431        });
432    }
433
434    #[test]
435    fn operation_io_with() {
436        struct ImplsOperationInput;
437        impl OperationInput for ImplsOperationInput {
438            fn operation_input(_ctx: &mut GenContext, operation: &mut Operation) {
439                // Changing a property of the operation so that we know this function was called
440                operation.deprecated = true;
441            }
442
443            fn inferred_early_responses(
444                _ctx: &mut GenContext,
445                _operation: &mut Operation,
446            ) -> Vec<(Option<StatusCode>, Response)> {
447                vec![(Some(StatusCode::Code(400)), Response::default())]
448            }
449        }
450
451        struct ImplsOperationOutput;
452        impl OperationOutput for ImplsOperationOutput {
453            type Inner = ();
454
455            fn operation_response(
456                _ctx: &mut GenContext,
457                _operation: &mut Operation,
458            ) -> Option<Response> {
459                Some(Response::default())
460            }
461
462            fn inferred_responses(
463                _ctx: &mut GenContext,
464                _operation: &mut Operation,
465            ) -> Vec<(Option<StatusCode>, Response)> {
466                vec![(None, Response::default())]
467            }
468        }
469
470        #[derive(OperationIo)]
471        #[aide(
472            input_with = "ImplsOperationInput",
473            output_with = "ImplsOperationOutput"
474        )]
475        struct OperationIoWith;
476
477        generate::in_context(|ctx| {
478            let mut operation = Operation::default();
479
480            OperationIoWith::operation_input(ctx, &mut operation);
481            assert!(operation.deprecated);
482
483            assert_eq!(
484                OperationIoWith::inferred_early_responses(ctx, &mut operation),
485                vec![(Some(StatusCode::Code(400)), Response::default())],
486            );
487
488            assert_eq!(
489                OperationIoWith::operation_response(ctx, &mut operation),
490                Some(Response::default()),
491            );
492            assert_eq!(
493                OperationIoWith::inferred_responses(ctx, &mut operation),
494                vec![(None, Response::default())],
495            );
496            #[allow(clippy::items_after_statements)]
497            fn assert_inner_is_unit<T: OperationOutput<Inner = ()>>() {}
498            assert_inner_is_unit::<OperationIoWith>();
499        });
500    }
501
502    #[test]
503    fn operation_io_json_schema() {
504        // The `input_with`/`output_with` ensures that this test will only compile if
505        // the `json_schema` trait bounds are correct.
506        #[derive(OperationIo)]
507        #[aide(
508            input_with = "OperationInputOutputIfJsonSchema<T, U>",
509            output_with = "OperationInputOutputIfJsonSchema<T, U>",
510            json_schema
511        )]
512        struct OperationInputOutput<T, U>(T, U);
513
514        struct OperationInputOutputIfJsonSchema<T, U>(T, U);
515        impl<T: JsonSchema, U: JsonSchema> OperationInput for OperationInputOutputIfJsonSchema<T, U> {}
516        impl<T: JsonSchema, U: JsonSchema> OperationOutput for OperationInputOutputIfJsonSchema<T, U> {
517            type Inner = Self;
518        }
519
520        fn assert_impls_operation_input_output<T: OperationInput + OperationOutput>() {}
521        assert_impls_operation_input_output::<OperationInputOutput<(), i32>>();
522    }
523}