utoipa_gen/path/
response.rs

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    /// A type that implements `utoipa::IntoResponses`.
34    IntoResponses(Cow<'r, TypePath>),
35    /// The tuple definition of a response.
36    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                // Only tuple type will have `ComponentSchema`s as of now
58                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/// Parsed representation of response attributes from `#[utoipa::path]` attribute.
108#[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    /// Set as `ResponseValue` the content. This will fail if `response` attribute is already
120    /// defined.
121    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    // Use with the `response` attribute, this will fail if an incompatible attribute has already been set
148    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}