1use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
2use quote::{quote, quote_spanned, ToTokens};
3use std::borrow::Cow;
4use syn::{
5 parenthesized,
6 parse::{Parse, ParseStream},
7 punctuated::Punctuated,
8 spanned::Spanned,
9 token::Comma,
10 Attribute, Error, ExprPath, LitInt, LitStr, Token, TypePath,
11};
12
13use crate::{
14 component::ComponentSchema, features::attributes::Extensions, parse_utils,
15 path::media_type::Schema, AnyValue, Diagnostics, ToTokensDiagnostics,
16};
17
18use self::{header::Header, link::LinkTuple};
19
20use super::{
21 example::Example,
22 media_type::{DefaultSchema, MediaTypeAttr, ParsedType},
23 parse,
24 status::STATUS_CODES,
25};
26
27pub mod derive;
28mod header;
29pub mod link;
30
31#[cfg_attr(feature = "debug", derive(Debug))]
32pub enum Response<'r> {
33 IntoResponses(Cow<'r, TypePath>),
35 Tuple(ResponseTuple<'r>),
37}
38
39impl Parse for Response<'_> {
40 fn parse(input: ParseStream) -> syn::Result<Self> {
41 if input.fork().parse::<ExprPath>().is_ok() {
42 Ok(Self::IntoResponses(Cow::Owned(input.parse::<TypePath>()?)))
43 } else {
44 let response;
45 parenthesized!(response in input);
46 Ok(Self::Tuple(response.parse()?))
47 }
48 }
49}
50
51impl Response<'_> {
52 pub fn get_component_schemas(
53 &self,
54 ) -> Result<impl Iterator<Item = (bool, ComponentSchema)>, Diagnostics> {
55 match self {
56 Self::Tuple(tuple) => match &tuple.inner {
57 Some(ResponseTupleInner::Value(value)) => {
59 Ok(ResponseComponentSchemaIter::Iter(Box::new(
60 value
61 .content
62 .iter()
63 .map(
64 |media_type| match media_type.schema.get_component_schema() {
65 Ok(component_schema) => {
66 Ok(Some(media_type.schema.is_inline())
67 .zip(component_schema))
68 }
69 Err(error) => Err(error),
70 },
71 )
72 .collect::<Result<Vec<_>, Diagnostics>>()?
73 .into_iter()
74 .flatten(),
75 )))
76 }
77 _ => Ok(ResponseComponentSchemaIter::Empty),
78 },
79 Self::IntoResponses(_) => Ok(ResponseComponentSchemaIter::Empty),
80 }
81 }
82}
83
84pub enum ResponseComponentSchemaIter<'a, T> {
85 Iter(Box<dyn std::iter::Iterator<Item = T> + 'a>),
86 Empty,
87}
88
89impl<'a, T> Iterator for ResponseComponentSchemaIter<'a, T> {
90 type Item = T;
91
92 fn next(&mut self) -> Option<Self::Item> {
93 match self {
94 Self::Iter(iter) => iter.next(),
95 Self::Empty => None,
96 }
97 }
98
99 fn size_hint(&self) -> (usize, Option<usize>) {
100 match self {
101 Self::Iter(iter) => iter.size_hint(),
102 Self::Empty => (0, None),
103 }
104 }
105}
106
107#[derive(Default)]
109#[cfg_attr(feature = "debug", derive(Debug))]
110pub struct ResponseTuple<'r> {
111 status_code: ResponseStatus,
112 inner: Option<ResponseTupleInner<'r>>,
113}
114
115const RESPONSE_INCOMPATIBLE_ATTRIBUTES_MSG: &str =
116 "The `response` attribute may only be used in conjunction with the `status` attribute";
117
118impl<'r> ResponseTuple<'r> {
119 fn set_as_value<F: FnOnce(&mut ResponseValue) -> syn::Result<()>>(
122 &mut self,
123 ident: &Ident,
124 attribute: &str,
125 op: F,
126 ) -> syn::Result<()> {
127 match &mut self.inner {
128 Some(ResponseTupleInner::Value(value)) => {
129 op(value)?;
130 }
131 Some(ResponseTupleInner::Ref(_)) => {
132 return Err(Error::new(ident.span(), format!("Cannot use `{attribute}` in conjunction with `response`. The `response` attribute can only be used in conjunction with `status` attribute.")));
133 }
134 None => {
135 let mut value = ResponseValue {
136 content: vec![MediaTypeAttr::default()],
137 ..Default::default()
138 };
139 op(&mut value)?;
140 self.inner = Some(ResponseTupleInner::Value(value))
141 }
142 };
143
144 Ok(())
145 }
146
147 fn set_ref_type(&mut self, span: Span, ty: ParsedType<'r>) -> syn::Result<()> {
149 match &mut self.inner {
150 None => self.inner = Some(ResponseTupleInner::Ref(ty)),
151 Some(ResponseTupleInner::Ref(r)) => *r = ty,
152 Some(ResponseTupleInner::Value(_)) => {
153 return Err(Error::new(span, RESPONSE_INCOMPATIBLE_ATTRIBUTES_MSG))
154 }
155 }
156 Ok(())
157 }
158}
159
160#[cfg_attr(feature = "debug", derive(Debug))]
161enum ResponseTupleInner<'r> {
162 Value(ResponseValue<'r>),
163 Ref(ParsedType<'r>),
164}
165
166impl Parse for ResponseTuple<'_> {
167 fn parse(input: ParseStream) -> syn::Result<Self> {
168 const EXPECTED_ATTRIBUTES: &str =
169 "status, description, body, content_type, headers, example, examples, response";
170
171 let mut response = ResponseTuple::default();
172
173 while !input.is_empty() {
174 let ident = input.parse::<Ident>().map_err(|error| {
175 Error::new(
176 error.span(),
177 format!(
178 "unexpected attribute, expected any of: {EXPECTED_ATTRIBUTES}, {error}"
179 ),
180 )
181 })?;
182 let name = &*ident.to_string();
183 match name {
184 "status" => {
185 response.status_code =
186 parse_utils::parse_next(input, || input.parse::<ResponseStatus>())?;
187 }
188 "response" => {
189 response.set_ref_type(
190 input.span(),
191 parse_utils::parse_next(input, || input.parse())?,
192 )?;
193 }
194 _ => {
195 response.set_as_value(&ident, name, |value| {
196 value.parse_named_attributes(input, &ident)
197 })?;
198 }
199 }
200
201 if !input.is_empty() {
202 input.parse::<Token![,]>()?;
203 }
204 }
205
206 Ok(response)
207 }
208}
209
210impl<'r> From<ResponseValue<'r>> for ResponseTuple<'r> {
211 fn from(value: ResponseValue<'r>) -> Self {
212 ResponseTuple {
213 inner: Some(ResponseTupleInner::Value(value)),
214 ..Default::default()
215 }
216 }
217}
218
219impl<'r> From<(ResponseStatus, ResponseValue<'r>)> for ResponseTuple<'r> {
220 fn from((status_code, response_value): (ResponseStatus, ResponseValue<'r>)) -> Self {
221 ResponseTuple {
222 inner: Some(ResponseTupleInner::Value(response_value)),
223 status_code,
224 }
225 }
226}
227
228#[derive(Default)]
229#[cfg_attr(feature = "debug", derive(Debug))]
230pub struct ResponseValue<'r> {
231 description: parse_utils::LitStrOrExpr,
232 headers: Vec<Header>,
233 links: Punctuated<LinkTuple, Comma>,
234 content: Vec<MediaTypeAttr<'r>>,
235 is_content_group: bool,
236 extensions: Option<Extensions>,
237}
238
239impl Parse for ResponseValue<'_> {
240 fn parse(input: ParseStream) -> syn::Result<Self> {
241 let mut response_value = ResponseValue::default();
242
243 while !input.is_empty() {
244 let ident = input.parse::<Ident>().map_err(|error| {
245 Error::new(
246 error.span(),
247 format!(
248 "unexpected attribute, expected any of: {expected_attributes}, {error}",
249 expected_attributes = ResponseValue::EXPECTED_ATTRIBUTES
250 ),
251 )
252 })?;
253 response_value.parse_named_attributes(input, &ident)?;
254
255 if !input.is_empty() {
256 input.parse::<Token![,]>()?;
257 }
258 }
259
260 Ok(response_value)
261 }
262}
263
264impl<'r> ResponseValue<'r> {
265 const EXPECTED_ATTRIBUTES: &'static str =
266 "description, body, content_type, headers, example, examples";
267
268 fn parse_named_attributes(&mut self, input: ParseStream, attribute: &Ident) -> syn::Result<()> {
269 let attribute_name = &*attribute.to_string();
270
271 match attribute_name {
272 "description" => {
273 self.description = parse::description(input)?;
274 }
275 "body" => {
276 if self.is_content_group {
277 return Err(Error::new(
278 attribute.span(),
279 "cannot set `body` when content(...) is defined in group form",
280 ));
281 }
282
283 let schema = parse_utils::parse_next(input, || MediaTypeAttr::parse_schema(input))?;
284 if let Some(media_type) = self.content.get_mut(0) {
285 media_type.schema = Schema::Default(schema);
286 }
287 }
288 "content_type" => {
289 if self.is_content_group {
290 return Err(Error::new(
291 attribute.span(),
292 "cannot set `content_type` when content(...) is defined in group form",
293 ));
294 }
295 let content_type = parse_utils::parse_next(input, || {
296 parse_utils::LitStrOrExpr::parse(input)
297 }).map_err(|error| Error::new(error.span(),
298 format!(r#"invalid content_type, must be literal string or expression, e.g. "application/json", {error} "#)
299 ))?;
300
301 if let Some(media_type) = self.content.get_mut(0) {
302 media_type.content_type = Some(content_type);
303 }
304 }
305 "headers" => {
306 self.headers = header::headers(input)?;
307 }
308 "content" => {
309 self.is_content_group = true;
310 fn group_parser<'a>(input: ParseStream) -> syn::Result<MediaTypeAttr<'a>> {
311 let buf;
312 syn::parenthesized!(buf in input);
313 buf.call(MediaTypeAttr::parse)
314 }
315
316 let content =
317 parse_utils::parse_comma_separated_within_parethesis_with(input, group_parser)?
318 .into_iter()
319 .collect::<Vec<_>>();
320
321 self.content = content;
322 }
323 "links" => {
324 self.links = parse_utils::parse_comma_separated_within_parenthesis(input)?;
325 }
326 "extensions" => {
327 self.extensions = Some(input.parse::<Extensions>()?);
328 }
329 _ => {
330 self.content
331 .get_mut(0)
332 .expect(
333 "parse named attributes response value must have one media type by default",
334 )
335 .parse_named_attributes(input, attribute)?;
336 }
337 }
338 Ok(())
339 }
340
341 fn from_schema<S: Into<Schema<'r>>>(schema: S, description: parse_utils::LitStrOrExpr) -> Self {
342 let media_type = MediaTypeAttr {
343 schema: schema.into(),
344 ..Default::default()
345 };
346
347 Self {
348 description,
349 content: vec![media_type],
350 ..Default::default()
351 }
352 }
353
354 fn from_derive_to_response_value<S: Into<Schema<'r>>>(
355 derive_value: DeriveToResponseValue,
356 schema: S,
357 description: parse_utils::LitStrOrExpr,
358 ) -> Self {
359 let media_type = MediaTypeAttr {
360 content_type: derive_value.content_type,
361 schema: schema.into(),
362 example: derive_value.example.map(|(example, _)| example),
363 examples: derive_value
364 .examples
365 .map(|(examples, _)| examples)
366 .unwrap_or_default(),
367 ..MediaTypeAttr::default()
368 };
369
370 Self {
371 description: if derive_value.description.is_empty_litstr()
372 && !description.is_empty_litstr()
373 {
374 description
375 } else {
376 derive_value.description
377 },
378 headers: derive_value.headers,
379 content: vec![media_type],
380 ..Default::default()
381 }
382 }
383
384 fn from_derive_into_responses_value<S: Into<Schema<'r>>>(
385 response_value: DeriveIntoResponsesValue,
386 schema: S,
387 description: parse_utils::LitStrOrExpr,
388 ) -> Self {
389 let media_type = MediaTypeAttr {
390 content_type: response_value.content_type,
391 schema: schema.into(),
392 example: response_value.example.map(|(example, _)| example),
393 examples: response_value
394 .examples
395 .map(|(examples, _)| examples)
396 .unwrap_or_default(),
397 ..MediaTypeAttr::default()
398 };
399
400 ResponseValue {
401 description: if response_value.description.is_empty_litstr()
402 && !description.is_empty_litstr()
403 {
404 description
405 } else {
406 response_value.description
407 },
408 headers: response_value.headers,
409 content: vec![media_type],
410 ..Default::default()
411 }
412 }
413}
414
415impl ToTokensDiagnostics for ResponseTuple<'_> {
416 fn to_tokens(&self, tokens: &mut TokenStream2) -> Result<(), Diagnostics> {
417 match self.inner.as_ref() {
418 Some(ResponseTupleInner::Ref(res)) => {
419 let path = &res.ty;
420 if res.is_inline {
421 tokens.extend(quote_spanned! {path.span()=>
422 <#path as utoipa::ToResponse>::response().1
423 });
424 } else {
425 tokens.extend(quote! {
426 utoipa::openapi::Ref::from_response_name(<#path as utoipa::ToResponse>::response().0)
427 });
428 }
429 }
430 Some(ResponseTupleInner::Value(value)) => {
431 let description = &value.description;
432 tokens.extend(quote! {
433 utoipa::openapi::ResponseBuilder::new().description(#description)
434 });
435
436 for media_type in value.content.iter().filter(|media_type| {
437 !(matches!(media_type.schema, Schema::Default(DefaultSchema::None))
438 && media_type.content_type.is_none())
439 }) {
440 let default_content_type = media_type.schema.get_default_content_type()?;
441
442 let content_type_tokens = media_type
443 .content_type
444 .as_ref()
445 .map(|content_type| content_type.to_token_stream())
446 .unwrap_or_else(|| default_content_type.to_token_stream());
447 let content_tokens = media_type.try_to_token_stream()?;
448
449 tokens.extend(quote! {
450 .content(#content_type_tokens, #content_tokens)
451 });
452 }
453
454 for header in &value.headers {
455 let name = &header.name;
456 let header = crate::as_tokens_or_diagnostics!(header);
457 tokens.extend(quote! {
458 .header(#name, #header)
459 })
460 }
461
462 for LinkTuple(name, link) in &value.links {
463 tokens.extend(quote! {
464 .link(#name, #link)
465 })
466 }
467 if let Some(ref extensions) = value.extensions {
468 tokens.extend(quote! {
469 .extensions(Some(#extensions))
470 });
471 }
472
473 tokens.extend(quote! { .build() });
474 }
475 None => tokens.extend(quote! {
476 utoipa::openapi::ResponseBuilder::new().description("")
477 }),
478 }
479
480 Ok(())
481 }
482}
483
484trait DeriveResponseValue: Parse {
485 fn merge_from(self, other: Self) -> Self;
486
487 fn from_attributes(attributes: &[Attribute]) -> Result<Option<Self>, Diagnostics> {
488 Ok(attributes
489 .iter()
490 .filter(|attribute| attribute.path().get_ident().unwrap() == "response")
491 .map(|attribute| attribute.parse_args::<Self>().map_err(Diagnostics::from))
492 .collect::<Result<Vec<_>, Diagnostics>>()?
493 .into_iter()
494 .reduce(|acc, item| acc.merge_from(item)))
495 }
496}
497
498#[derive(Default)]
499#[cfg_attr(feature = "debug", derive(Debug))]
500struct DeriveToResponseValue {
501 content_type: Option<parse_utils::LitStrOrExpr>,
502 headers: Vec<Header>,
503 description: parse_utils::LitStrOrExpr,
504 example: Option<(AnyValue, Ident)>,
505 examples: Option<(Punctuated<Example, Comma>, Ident)>,
506}
507
508impl DeriveResponseValue for DeriveToResponseValue {
509 fn merge_from(mut self, other: Self) -> Self {
510 if other.content_type.is_some() {
511 self.content_type = other.content_type;
512 }
513 if !other.headers.is_empty() {
514 self.headers = other.headers;
515 }
516 if !other.description.is_empty_litstr() {
517 self.description = other.description;
518 }
519 if other.example.is_some() {
520 self.example = other.example;
521 }
522 if other.examples.is_some() {
523 self.examples = other.examples;
524 }
525
526 self
527 }
528}
529
530impl Parse for DeriveToResponseValue {
531 fn parse(input: ParseStream) -> syn::Result<Self> {
532 let mut response = DeriveToResponseValue::default();
533
534 while !input.is_empty() {
535 let ident = input.parse::<Ident>()?;
536 let attribute_name = &*ident.to_string();
537
538 match attribute_name {
539 "description" => {
540 response.description = parse::description(input)?;
541 }
542 "content_type" => {
543 response.content_type =
544 Some(parse_utils::parse_next_literal_str_or_expr(input)?);
545 }
546 "headers" => {
547 response.headers = header::headers(input)?;
548 }
549 "example" => {
550 response.example = Some((parse::example(input)?, ident));
551 }
552 "examples" => {
553 response.examples = Some((parse::examples(input)?, ident));
554 }
555 _ => {
556 return Err(Error::new(
557 ident.span(),
558 format!("unexpected attribute: {attribute_name}, expected any of: inline, description, content_type, headers, example"),
559 ));
560 }
561 }
562
563 if !input.is_empty() {
564 input.parse::<Comma>()?;
565 }
566 }
567
568 Ok(response)
569 }
570}
571
572#[derive(Default)]
573struct DeriveIntoResponsesValue {
574 status: ResponseStatus,
575 content_type: Option<parse_utils::LitStrOrExpr>,
576 headers: Vec<Header>,
577 description: parse_utils::LitStrOrExpr,
578 example: Option<(AnyValue, Ident)>,
579 examples: Option<(Punctuated<Example, Comma>, Ident)>,
580}
581
582impl DeriveResponseValue for DeriveIntoResponsesValue {
583 fn merge_from(mut self, other: Self) -> Self {
584 self.status = other.status;
585
586 if other.content_type.is_some() {
587 self.content_type = other.content_type;
588 }
589 if !other.headers.is_empty() {
590 self.headers = other.headers;
591 }
592 if !other.description.is_empty_litstr() {
593 self.description = other.description;
594 }
595 if other.example.is_some() {
596 self.example = other.example;
597 }
598 if other.examples.is_some() {
599 self.examples = other.examples;
600 }
601
602 self
603 }
604}
605
606impl Parse for DeriveIntoResponsesValue {
607 fn parse(input: ParseStream) -> syn::Result<Self> {
608 let mut response = DeriveIntoResponsesValue::default();
609 const MISSING_STATUS_ERROR: &str = "missing expected `status` attribute";
610 let first_span = input.span();
611
612 let status_ident = input
613 .parse::<Ident>()
614 .map_err(|error| Error::new(error.span(), MISSING_STATUS_ERROR))?;
615
616 if status_ident == "status" {
617 response.status = parse_utils::parse_next(input, || input.parse::<ResponseStatus>())?;
618 } else {
619 return Err(Error::new(status_ident.span(), MISSING_STATUS_ERROR));
620 }
621
622 if response.status.to_token_stream().is_empty() {
623 return Err(Error::new(first_span, MISSING_STATUS_ERROR));
624 }
625
626 if !input.is_empty() {
627 input.parse::<Token![,]>()?;
628 }
629
630 while !input.is_empty() {
631 let ident = input.parse::<Ident>()?;
632 let attribute_name = &*ident.to_string();
633
634 match attribute_name {
635 "description" => {
636 response.description = parse::description(input)?;
637 }
638 "content_type" => {
639 response.content_type =
640 Some(parse_utils::parse_next_literal_str_or_expr(input)?);
641 }
642 "headers" => {
643 response.headers = header::headers(input)?;
644 }
645 "example" => {
646 response.example = Some((parse::example(input)?, ident));
647 }
648 "examples" => {
649 response.examples = Some((parse::examples(input)?, ident));
650 }
651 _ => {
652 return Err(Error::new(
653 ident.span(),
654 format!("unexpected attribute: {attribute_name}, expected any of: description, content_type, headers, example, examples"),
655 ));
656 }
657 }
658
659 if !input.is_empty() {
660 input.parse::<Token![,]>()?;
661 }
662 }
663
664 Ok(response)
665 }
666}
667
668#[derive(Default)]
669#[cfg_attr(feature = "debug", derive(Debug))]
670struct ResponseStatus(TokenStream2);
671
672impl Parse for ResponseStatus {
673 fn parse(input: ParseStream) -> syn::Result<Self> {
674 fn parse_lit_int(input: ParseStream) -> syn::Result<Cow<'_, str>> {
675 input.parse::<LitInt>()?.base10_parse().map(Cow::Owned)
676 }
677
678 fn parse_lit_str_status_range(input: ParseStream) -> syn::Result<Cow<'_, str>> {
679 const VALID_STATUS_RANGES: [&str; 6] = ["default", "1XX", "2XX", "3XX", "4XX", "5XX"];
680
681 input
682 .parse::<LitStr>()
683 .and_then(|lit_str| {
684 let value = lit_str.value();
685 if !VALID_STATUS_RANGES.contains(&value.as_str()) {
686 Err(Error::new(
687 value.span(),
688 format!(
689 "Invalid status range, expected one of: {}",
690 VALID_STATUS_RANGES.join(", "),
691 ),
692 ))
693 } else {
694 Ok(value)
695 }
696 })
697 .map(Cow::Owned)
698 }
699
700 fn parse_http_status_code(input: ParseStream) -> syn::Result<TokenStream2> {
701 let http_status_path = input.parse::<ExprPath>()?;
702 let last_segment = http_status_path
703 .path
704 .segments
705 .last()
706 .expect("Expected at least one segment in http StatusCode");
707
708 STATUS_CODES
709 .iter()
710 .find_map(|(code, name)| {
711 if last_segment.ident == name {
712 Some(code.to_string().to_token_stream())
713 } else {
714 None
715 }
716 })
717 .ok_or_else(|| {
718 Error::new(
719 last_segment.span(),
720 format!(
721 "No associate item `{}` found for struct `http::StatusCode`",
722 last_segment.ident
723 ),
724 )
725 })
726 }
727
728 let lookahead = input.lookahead1();
729 if lookahead.peek(LitInt) {
730 parse_lit_int(input).map(|status| Self(status.to_token_stream()))
731 } else if lookahead.peek(LitStr) {
732 parse_lit_str_status_range(input).map(|status| Self(status.to_token_stream()))
733 } else if lookahead.peek(syn::Ident) {
734 parse_http_status_code(input).map(Self)
735 } else {
736 Err(lookahead.error())
737 }
738 }
739}
740
741impl ToTokens for ResponseStatus {
742 fn to_tokens(&self, tokens: &mut TokenStream2) {
743 self.0.to_tokens(tokens);
744 }
745}
746
747pub struct Responses<'a>(pub &'a [Response<'a>]);
748
749impl ToTokensDiagnostics for Responses<'_> {
750 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> {
751 tokens.extend(
752 self.0
753 .iter()
754 .map(|response| match response {
755 Response::IntoResponses(path) => {
756 let span = path.span();
757 Ok(quote_spanned! {span =>
758 .responses_from_into_responses::<#path>()
759 })
760 }
761 Response::Tuple(response) => {
762 let code = &response.status_code;
763 let response = crate::as_tokens_or_diagnostics!(response);
764 Ok(quote! { .response(#code, #response) })
765 }
766 })
767 .collect::<Result<Vec<_>, Diagnostics>>()?
768 .into_iter()
769 .fold(
770 quote! { utoipa::openapi::ResponsesBuilder::new() },
771 |mut acc, response| {
772 response.to_tokens(&mut acc);
773
774 acc
775 },
776 ),
777 );
778
779 tokens.extend(quote! { .build() });
780
781 Ok(())
782 }
783}