1use 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#[allow(unused_variables)]
44pub trait OperationInput {
45 fn operation_input(ctx: &mut GenContext, operation: &mut Operation) {}
54
55 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#[allow(unused_variables)]
141pub trait OperationOutput {
142 type Inner;
149
150 fn operation_response(ctx: &mut GenContext, operation: &mut Operation) -> Option<Response> {
163 None
164 }
165
166 fn inferred_responses(
181 ctx: &mut GenContext,
182 operation: &mut Operation,
183 ) -> Vec<(Option<StatusCode>, Response)> {
184 Vec::new()
185 }
186}
187
188#[allow(missing_docs)]
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum ParamLocation {
192 Query,
193 Path,
194 Header,
195 Cookie,
196}
197
198#[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
345pub 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
354pub 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 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 #[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}