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::{features::Inline, ComponentSchema, TypeTree},
15    parse_utils, AnyValue, Array, ResultExt,
16};
17
18use super::{example::Example, status::STATUS_CODES, InlineType, PathType, PathTypeTree};
19
20pub mod derive;
21
22#[cfg_attr(feature = "debug", derive(Debug))]
23pub enum Response<'r> {
24    /// A type that implements `utoipa::IntoResponses`.
25    IntoResponses(Cow<'r, TypePath>),
26    /// The tuple definition of a response.
27    Tuple(ResponseTuple<'r>),
28}
29
30impl Parse for Response<'_> {
31    fn parse(input: ParseStream) -> syn::Result<Self> {
32        if input.fork().parse::<ExprPath>().is_ok() {
33            Ok(Self::IntoResponses(Cow::Owned(input.parse::<TypePath>()?)))
34        } else {
35            let response;
36            parenthesized!(response in input);
37            Ok(Self::Tuple(response.parse()?))
38        }
39    }
40}
41
42/// Parsed representation of response attributes from `#[utoipa::path]` attribute.
43#[derive(Default)]
44#[cfg_attr(feature = "debug", derive(Debug))]
45pub struct ResponseTuple<'r> {
46    status_code: ResponseStatus,
47    inner: Option<ResponseTupleInner<'r>>,
48}
49
50const RESPONSE_INCOMPATIBLE_ATTRIBUTES_MSG: &str =
51    "The `response` attribute may only be used in conjunction with the `status` attribute";
52
53impl<'r> ResponseTuple<'r> {
54    // This will error if the `response` attribute has already been set
55    fn as_value(&mut self, span: Span) -> syn::Result<&mut ResponseValue<'r>> {
56        if self.inner.is_none() {
57            self.inner = Some(ResponseTupleInner::Value(ResponseValue::default()));
58        }
59        if let ResponseTupleInner::Value(val) = self.inner.as_mut().unwrap() {
60            Ok(val)
61        } else {
62            Err(Error::new(span, RESPONSE_INCOMPATIBLE_ATTRIBUTES_MSG))
63        }
64    }
65
66    // Use with the `response` attribute, this will fail if an incompatible attribute has already been set
67    fn set_ref_type(&mut self, span: Span, ty: InlineType<'r>) -> syn::Result<()> {
68        match &mut self.inner {
69            None => self.inner = Some(ResponseTupleInner::Ref(ty)),
70            Some(ResponseTupleInner::Ref(r)) => *r = ty,
71            Some(ResponseTupleInner::Value(_)) => {
72                return Err(Error::new(span, RESPONSE_INCOMPATIBLE_ATTRIBUTES_MSG))
73            }
74        }
75        Ok(())
76    }
77}
78
79#[cfg_attr(feature = "debug", derive(Debug))]
80enum ResponseTupleInner<'r> {
81    Value(ResponseValue<'r>),
82    Ref(InlineType<'r>),
83}
84
85impl Parse for ResponseTuple<'_> {
86    fn parse(input: ParseStream) -> syn::Result<Self> {
87        const EXPECTED_ATTRIBUTE_MESSAGE: &str = "unexpected attribute, expected any of: status, description, body, content_type, headers, example, examples, response";
88
89        let mut response = ResponseTuple::default();
90
91        while !input.is_empty() {
92            let ident = input.parse::<Ident>().map_err(|error| {
93                Error::new(
94                    error.span(),
95                    format!("{EXPECTED_ATTRIBUTE_MESSAGE}, {error}"),
96                )
97            })?;
98            let attribute_name = &*ident.to_string();
99
100            match attribute_name {
101                "status" => {
102                    response.status_code =
103                        parse_utils::parse_next(input, || input.parse::<ResponseStatus>())?;
104                }
105                "description" => {
106                    response.as_value(input.span())?.description = parse::description(input)?;
107                }
108                "body" => {
109                    response.as_value(input.span())?.response_type =
110                        Some(parse_utils::parse_next(input, || input.parse())?);
111                }
112                "content_type" => {
113                    response.as_value(input.span())?.content_type =
114                        Some(parse::content_type(input)?);
115                }
116                "headers" => {
117                    response.as_value(input.span())?.headers = parse::headers(input)?;
118                }
119                "example" => {
120                    response.as_value(input.span())?.example = Some(parse::example(input)?);
121                }
122                "examples" => {
123                    response.as_value(input.span())?.examples = Some(parse::examples(input)?);
124                }
125                "content" => {
126                    response.as_value(input.span())?.content =
127                        parse_utils::parse_punctuated_within_parenthesis(input)?;
128                }
129                "response" => {
130                    response.set_ref_type(
131                        input.span(),
132                        parse_utils::parse_next(input, || input.parse())?,
133                    )?;
134                }
135                _ => return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE_MESSAGE)),
136            }
137
138            if !input.is_empty() {
139                input.parse::<Token![,]>()?;
140            }
141        }
142
143        if response.inner.is_none() {
144            response.inner = Some(ResponseTupleInner::Value(ResponseValue::default()))
145        }
146
147        Ok(response)
148    }
149}
150
151impl<'r> From<ResponseValue<'r>> for ResponseTuple<'r> {
152    fn from(value: ResponseValue<'r>) -> Self {
153        ResponseTuple {
154            inner: Some(ResponseTupleInner::Value(value)),
155            ..Default::default()
156        }
157    }
158}
159
160impl<'r> From<(ResponseStatus, ResponseValue<'r>)> for ResponseTuple<'r> {
161    fn from((status_code, response_value): (ResponseStatus, ResponseValue<'r>)) -> Self {
162        ResponseTuple {
163            inner: Some(ResponseTupleInner::Value(response_value)),
164            status_code,
165        }
166    }
167}
168
169pub struct DeriveResponsesAttributes<T> {
170    derive_value: T,
171    description: String,
172}
173
174impl<'r> From<DeriveResponsesAttributes<DeriveIntoResponsesValue>> for ResponseValue<'r> {
175    fn from(value: DeriveResponsesAttributes<DeriveIntoResponsesValue>) -> Self {
176        Self::from_derive_into_responses_value(value.derive_value, value.description)
177    }
178}
179
180impl<'r> From<DeriveResponsesAttributes<Option<DeriveToResponseValue>>> for ResponseValue<'r> {
181    fn from(
182        DeriveResponsesAttributes::<Option<DeriveToResponseValue>> {
183            derive_value,
184            description,
185        }: DeriveResponsesAttributes<Option<DeriveToResponseValue>>,
186    ) -> Self {
187        if let Some(derive_value) = derive_value {
188            ResponseValue::from_derive_to_response_value(derive_value, description)
189        } else {
190            ResponseValue {
191                description,
192                ..Default::default()
193            }
194        }
195    }
196}
197
198#[derive(Default)]
199#[cfg_attr(feature = "debug", derive(Debug))]
200pub struct ResponseValue<'r> {
201    description: String,
202    response_type: Option<PathType<'r>>,
203    content_type: Option<Vec<String>>,
204    headers: Vec<Header>,
205    example: Option<AnyValue>,
206    examples: Option<Punctuated<Example, Comma>>,
207    content: Punctuated<Content<'r>, Comma>,
208}
209
210impl<'r> ResponseValue<'r> {
211    fn from_derive_to_response_value(
212        derive_value: DeriveToResponseValue,
213        description: String,
214    ) -> Self {
215        Self {
216            description: if derive_value.description.is_empty() && !description.is_empty() {
217                description
218            } else {
219                derive_value.description
220            },
221            headers: derive_value.headers,
222            example: derive_value.example.map(|(example, _)| example),
223            examples: derive_value.examples.map(|(examples, _)| examples),
224            content_type: derive_value.content_type,
225            ..Default::default()
226        }
227    }
228
229    fn from_derive_into_responses_value(
230        response_value: DeriveIntoResponsesValue,
231        description: String,
232    ) -> Self {
233        ResponseValue {
234            description: if response_value.description.is_empty() && !description.is_empty() {
235                description
236            } else {
237                response_value.description
238            },
239            headers: response_value.headers,
240            example: response_value.example.map(|(example, _)| example),
241            examples: response_value.examples.map(|(examples, _)| examples),
242            content_type: response_value.content_type,
243            ..Default::default()
244        }
245    }
246
247    fn response_type(mut self, response_type: Option<PathType<'r>>) -> Self {
248        self.response_type = response_type;
249
250        self
251    }
252}
253
254impl ToTokens for ResponseTuple<'_> {
255    fn to_tokens(&self, tokens: &mut TokenStream2) {
256        match self.inner.as_ref().unwrap() {
257            ResponseTupleInner::Ref(res) => {
258                let path = &res.ty;
259                if res.is_inline {
260                    tokens.extend(quote_spanned! {path.span()=>
261                        <#path as utoipa::ToResponse>::response().1
262                    });
263                } else {
264                    tokens.extend(quote! {
265                        utoipa::openapi::Ref::from_response_name(<#path as utoipa::ToResponse>::response().0)
266                    });
267                }
268            }
269            ResponseTupleInner::Value(val) => {
270                let description = &val.description;
271                tokens.extend(quote! {
272                    utoipa::openapi::ResponseBuilder::new().description(#description)
273                });
274
275                let create_content = |path_type: &PathType,
276                                      example: &Option<AnyValue>,
277                                      examples: &Option<Punctuated<Example, Comma>>|
278                 -> TokenStream2 {
279                    let content_schema = match path_type {
280                        PathType::Ref(ref_type) => quote! {
281                            utoipa::openapi::schema::Ref::new(#ref_type)
282                        }
283                        .to_token_stream(),
284                        PathType::MediaType(ref path_type) => {
285                            let type_tree = path_type.as_type_tree();
286
287                            ComponentSchema::new(crate::component::ComponentSchemaProps {
288                                type_tree: &type_tree,
289                                features: Some(vec![Inline::from(path_type.is_inline).into()]),
290                                description: None,
291                                deprecated: None,
292                                object_name: "",
293                            })
294                            .to_token_stream()
295                        }
296                        PathType::InlineSchema(schema, _) => schema.to_token_stream(),
297                    };
298
299                    let mut content =
300                        quote! { utoipa::openapi::ContentBuilder::new().schema(#content_schema) };
301
302                    if let Some(ref example) = example {
303                        content.extend(quote! {
304                            .example(Some(#example))
305                        })
306                    }
307                    if let Some(ref examples) = examples {
308                        let examples = examples
309                            .iter()
310                            .map(|example| {
311                                let name = &example.name;
312                                quote!((#name, #example))
313                            })
314                            .collect::<Array<TokenStream2>>();
315                        content.extend(quote!(
316                            .examples_from_iter(#examples)
317                        ))
318                    }
319
320                    quote! {
321                        #content.build()
322                    }
323                };
324
325                if let Some(response_type) = &val.response_type {
326                    let content = create_content(response_type, &val.example, &val.examples);
327
328                    if let Some(content_types) = val.content_type.as_ref() {
329                        content_types.iter().for_each(|content_type| {
330                            tokens.extend(quote! {
331                                .content(#content_type, #content)
332                            })
333                        })
334                    } else {
335                        match response_type {
336                            PathType::Ref(_) => {
337                                tokens.extend(quote! {
338                                    .content("application/json", #content)
339                                });
340                            }
341                            PathType::MediaType(path_type) => {
342                                let type_tree = path_type.as_type_tree();
343                                let default_type = type_tree.get_default_content_type();
344                                tokens.extend(quote! {
345                                    .content(#default_type, #content)
346                                })
347                            }
348                            PathType::InlineSchema(_, ty) => {
349                                let type_tree = TypeTree::from_type(ty);
350                                let default_type = type_tree.get_default_content_type();
351                                tokens.extend(quote! {
352                                    .content(#default_type, #content)
353                                })
354                            }
355                        }
356                    }
357                }
358
359                val.content
360                    .iter()
361                    .map(|Content(content_type, body, example, examples)| {
362                        let content = create_content(body, example, examples);
363                        (Cow::Borrowed(&**content_type), content)
364                    })
365                    .for_each(|(content_type, content)| {
366                        tokens.extend(quote! { .content(#content_type, #content) })
367                    });
368
369                val.headers.iter().for_each(|header| {
370                    let name = &header.name;
371                    tokens.extend(quote! {
372                        .header(#name, #header)
373                    })
374                });
375
376                tokens.extend(quote! { .build() });
377            }
378        }
379    }
380}
381
382trait DeriveResponseValue: Parse {
383    fn merge_from(self, other: Self) -> Self;
384
385    fn from_attributes(attributes: &[Attribute]) -> Option<Self> {
386        attributes
387            .iter()
388            .filter(|attribute| attribute.path().get_ident().unwrap() == "response")
389            .map(|attribute| attribute.parse_args::<Self>().unwrap_or_abort())
390            .reduce(|acc, item| acc.merge_from(item))
391    }
392}
393
394#[derive(Default)]
395#[cfg_attr(feature = "debug", derive(Debug))]
396struct DeriveToResponseValue {
397    content_type: Option<Vec<String>>,
398    headers: Vec<Header>,
399    description: String,
400    example: Option<(AnyValue, Ident)>,
401    examples: Option<(Punctuated<Example, Comma>, Ident)>,
402}
403
404impl DeriveResponseValue for DeriveToResponseValue {
405    fn merge_from(mut self, other: Self) -> Self {
406        if other.content_type.is_some() {
407            self.content_type = other.content_type;
408        }
409        if !other.headers.is_empty() {
410            self.headers = other.headers;
411        }
412        if !other.description.is_empty() {
413            self.description = other.description;
414        }
415        if other.example.is_some() {
416            self.example = other.example;
417        }
418        if other.examples.is_some() {
419            self.examples = other.examples;
420        }
421
422        self
423    }
424}
425
426impl Parse for DeriveToResponseValue {
427    fn parse(input: ParseStream) -> syn::Result<Self> {
428        let mut response = DeriveToResponseValue::default();
429
430        while !input.is_empty() {
431            let ident = input.parse::<Ident>()?;
432            let attribute_name = &*ident.to_string();
433
434            match attribute_name {
435                "description" => {
436                    response.description = parse::description(input)?;
437                }
438                "content_type" => {
439                    response.content_type = Some(parse::content_type(input)?);
440                }
441                "headers" => {
442                    response.headers = parse::headers(input)?;
443                }
444                "example" => {
445                    response.example = Some((parse::example(input)?, ident));
446                }
447                "examples" => {
448                    response.examples = Some((parse::examples(input)?, ident));
449                }
450                _ => {
451                    return Err(Error::new(
452                        ident.span(),
453                        format!("unexpected attribute: {attribute_name}, expected any of: inline, description, content_type, headers, example"),
454                    ));
455                }
456            }
457
458            if !input.is_empty() {
459                input.parse::<Comma>()?;
460            }
461        }
462
463        Ok(response)
464    }
465}
466
467#[derive(Default)]
468struct DeriveIntoResponsesValue {
469    status: ResponseStatus,
470    content_type: Option<Vec<String>>,
471    headers: Vec<Header>,
472    description: String,
473    example: Option<(AnyValue, Ident)>,
474    examples: Option<(Punctuated<Example, Comma>, Ident)>,
475}
476
477impl DeriveResponseValue for DeriveIntoResponsesValue {
478    fn merge_from(mut self, other: Self) -> Self {
479        self.status = other.status;
480
481        if other.content_type.is_some() {
482            self.content_type = other.content_type;
483        }
484        if !other.headers.is_empty() {
485            self.headers = other.headers;
486        }
487        if !other.description.is_empty() {
488            self.description = other.description;
489        }
490        if other.example.is_some() {
491            self.example = other.example;
492        }
493        if other.examples.is_some() {
494            self.examples = other.examples;
495        }
496
497        self
498    }
499}
500
501impl Parse for DeriveIntoResponsesValue {
502    fn parse(input: ParseStream) -> syn::Result<Self> {
503        let mut response = DeriveIntoResponsesValue::default();
504        const MISSING_STATUS_ERROR: &str = "missing expected `status` attribute";
505        let first_span = input.span();
506
507        let status_ident = input
508            .parse::<Ident>()
509            .map_err(|error| Error::new(error.span(), MISSING_STATUS_ERROR))?;
510
511        if status_ident == "status" {
512            response.status = parse_utils::parse_next(input, || input.parse::<ResponseStatus>())?;
513        } else {
514            return Err(Error::new(status_ident.span(), MISSING_STATUS_ERROR));
515        }
516
517        if response.status.to_token_stream().is_empty() {
518            return Err(Error::new(first_span, MISSING_STATUS_ERROR));
519        }
520
521        if !input.is_empty() {
522            input.parse::<Token![,]>()?;
523        }
524
525        while !input.is_empty() {
526            let ident = input.parse::<Ident>()?;
527            let attribute_name = &*ident.to_string();
528
529            match attribute_name {
530                "description" => {
531                    response.description = parse::description(input)?;
532                }
533                "content_type" => {
534                    response.content_type = Some(parse::content_type(input)?);
535                }
536                "headers" => {
537                    response.headers = parse::headers(input)?;
538                }
539                "example" => {
540                    response.example = Some((parse::example(input)?, ident));
541                }
542                "examples" => {
543                    response.examples = Some((parse::examples(input)?, ident));
544                }
545                _ => {
546                    return Err(Error::new(
547                        ident.span(),
548                        format!("unexpected attribute: {attribute_name}, expected any of: description, content_type, headers, example, examples"),
549                    ));
550                }
551            }
552
553            if !input.is_empty() {
554                input.parse::<Token![,]>()?;
555            }
556        }
557
558        Ok(response)
559    }
560}
561
562#[derive(Default)]
563#[cfg_attr(feature = "debug", derive(Debug))]
564struct ResponseStatus(TokenStream2);
565
566impl Parse for ResponseStatus {
567    fn parse(input: ParseStream) -> syn::Result<Self> {
568        fn parse_lit_int(input: ParseStream) -> syn::Result<Cow<'_, str>> {
569            input.parse::<LitInt>()?.base10_parse().map(Cow::Owned)
570        }
571
572        fn parse_lit_str_status_range(input: ParseStream) -> syn::Result<Cow<'_, str>> {
573            const VALID_STATUS_RANGES: [&str; 6] = ["default", "1XX", "2XX", "3XX", "4XX", "5XX"];
574
575            input
576                .parse::<LitStr>()
577                .and_then(|lit_str| {
578                    let value = lit_str.value();
579                    if !VALID_STATUS_RANGES.contains(&value.as_str()) {
580                        Err(Error::new(
581                            value.span(),
582                            format!(
583                                "Invalid status range, expected one of: {}",
584                                VALID_STATUS_RANGES.join(", "),
585                            ),
586                        ))
587                    } else {
588                        Ok(value)
589                    }
590                })
591                .map(Cow::Owned)
592        }
593
594        fn parse_http_status_code(input: ParseStream) -> syn::Result<TokenStream2> {
595            let http_status_path = input.parse::<ExprPath>()?;
596            let last_segment = http_status_path
597                .path
598                .segments
599                .last()
600                .expect("Expected at least one segment in http StatusCode");
601
602            STATUS_CODES
603                .iter()
604                .find_map(|(code, name)| {
605                    if last_segment.ident == name {
606                        Some(code.to_string().to_token_stream())
607                    } else {
608                        None
609                    }
610                })
611                .ok_or_else(|| {
612                    Error::new(
613                        last_segment.span(),
614                        format!(
615                            "No associate item `{}` found for struct `http::StatusCode`",
616                            last_segment.ident
617                        ),
618                    )
619                })
620        }
621
622        let lookahead = input.lookahead1();
623        if lookahead.peek(LitInt) {
624            parse_lit_int(input).map(|status| Self(status.to_token_stream()))
625        } else if lookahead.peek(LitStr) {
626            parse_lit_str_status_range(input).map(|status| Self(status.to_token_stream()))
627        } else if lookahead.peek(syn::Ident) {
628            parse_http_status_code(input).map(Self)
629        } else {
630            Err(lookahead.error())
631        }
632    }
633}
634
635impl ToTokens for ResponseStatus {
636    fn to_tokens(&self, tokens: &mut TokenStream2) {
637        self.0.to_tokens(tokens);
638    }
639}
640
641// content(
642//   ("application/json" = Response, example = "...", examples(..., ...)),
643//   ("application/json2" = Response2, example = "...", examples("...", "..."))
644// )
645#[cfg_attr(feature = "debug", derive(Debug))]
646struct Content<'c>(
647    String,
648    PathType<'c>,
649    Option<AnyValue>,
650    Option<Punctuated<Example, Comma>>,
651);
652
653impl Parse for Content<'_> {
654    fn parse(input: ParseStream) -> syn::Result<Self> {
655        let content;
656        parenthesized!(content in input);
657
658        let content_type = content.parse::<LitStr>()?;
659        content.parse::<Token![=]>()?;
660        let body = content.parse()?;
661        content.parse::<Option<Comma>>()?;
662        let mut example = None::<AnyValue>;
663        let mut examples = None::<Punctuated<Example, Comma>>;
664
665        while !content.is_empty() {
666            let ident = content.parse::<Ident>()?;
667            let attribute_name = &*ident.to_string();
668            match attribute_name {
669                "example" => {
670                    example = Some(parse_utils::parse_next(&content, || {
671                        AnyValue::parse_json(&content)
672                    })?)
673                }
674                "examples" => {
675                    examples = Some(parse_utils::parse_punctuated_within_parenthesis(&content)?)
676                }
677                _ => {
678                    return Err(Error::new(
679                        ident.span(),
680                        format!(
681                            "unexpected attribute: {ident}, expected one of: example, examples"
682                        ),
683                    ));
684                }
685            }
686
687            if !content.is_empty() {
688                content.parse::<Comma>()?;
689            }
690        }
691
692        Ok(Content(content_type.value(), body, example, examples))
693    }
694}
695
696pub struct Responses<'a>(pub &'a [Response<'a>]);
697
698impl ToTokens for Responses<'_> {
699    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
700        tokens.extend(self.0.iter().fold(
701            quote! { utoipa::openapi::ResponsesBuilder::new() },
702            |mut acc, response| {
703                match response {
704                    Response::IntoResponses(path) => {
705                        let span = path.span();
706                        acc.extend(quote_spanned! {span =>
707                            .responses_from_into_responses::<#path>()
708                        })
709                    }
710                    Response::Tuple(response) => {
711                        let code = &response.status_code;
712                        acc.extend(quote! { .response(#code, #response) });
713                    }
714                }
715
716                acc
717            },
718        ));
719
720        tokens.extend(quote! { .build() });
721    }
722}
723
724/// Parsed representation of response header defined in `#[utoipa::path(..)]` attribute.
725///
726/// Supported configuration format is `("x-my-header-name" = type, description = "optional description of header")`.
727/// The `= type` and the `description = ".."` are optional configurations thus so the same configuration
728/// could be written as follows: `("x-my-header-name")`.
729///
730/// The `type` can be any typical type supported as a header argument such as `String, i32, u64, bool` etc.
731/// and if not provided it will default to `String`.
732///
733/// # Examples
734///
735/// Example of 200 success response which does return nothing back in response body, but returns a
736/// new csrf token in response headers.
737/// ```text
738/// #[utoipa::path(
739///     ...
740///     responses = [
741///         (status = 200, description = "success response",
742///             headers = [
743///                 ("xrfs-token" = String, description = "New csrf token sent back in response header")
744///             ]
745///         ),
746///     ]
747/// )]
748/// ```
749///
750/// Example with default values.
751/// ```text
752/// #[utoipa::path(
753///     ...
754///     responses = [
755///         (status = 200, description = "success response",
756///             headers = [
757///                 ("xrfs-token")
758///             ]
759///         ),
760///     ]
761/// )]
762/// ```
763///
764/// Example with multiple headers with default values.
765/// ```text
766/// #[utoipa::path(
767///     ...
768///     responses = [
769///         (status = 200, description = "success response",
770///             headers = [
771///                 ("xrfs-token"),
772///                 ("another-header"),
773///             ]
774///         ),
775///     ]
776/// )]
777/// ```
778#[derive(Default)]
779#[cfg_attr(feature = "debug", derive(Debug))]
780struct Header {
781    name: String,
782    value_type: Option<InlineType<'static>>,
783    description: Option<String>,
784}
785
786impl Parse for Header {
787    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
788        let mut header = Header {
789            name: input.parse::<LitStr>()?.value(),
790            ..Default::default()
791        };
792
793        if input.peek(Token![=]) {
794            input.parse::<Token![=]>()?;
795
796            header.value_type = Some(input.parse().map_err(|error| {
797                Error::new(
798                    error.span(),
799                    format!("unexpected token, expected type such as String, {error}"),
800                )
801            })?);
802        }
803
804        if !input.is_empty() {
805            input.parse::<Token![,]>()?;
806        }
807
808        if input.peek(syn::Ident) {
809            input
810                .parse::<Ident>()
811                .map_err(|error| {
812                    Error::new(
813                        error.span(),
814                        format!("unexpected attribute, expected: description, {error}"),
815                    )
816                })
817                .and_then(|ident| {
818                    if ident != "description" {
819                        return Err(Error::new(
820                            ident.span(),
821                            "unexpected attribute, expected: description",
822                        ));
823                    }
824                    Ok(ident)
825                })?;
826            input.parse::<Token![=]>()?;
827            header.description = Some(input.parse::<LitStr>()?.value());
828        }
829
830        Ok(header)
831    }
832}
833
834impl ToTokens for Header {
835    fn to_tokens(&self, tokens: &mut TokenStream2) {
836        if let Some(header_type) = &self.value_type {
837            // header property with custom type
838            let type_tree = header_type.as_type_tree();
839
840            let media_type_schema = ComponentSchema::new(crate::component::ComponentSchemaProps {
841                type_tree: &type_tree,
842                features: Some(vec![Inline::from(header_type.is_inline).into()]),
843                description: None,
844                deprecated: None,
845                object_name: "",
846            })
847            .to_token_stream();
848
849            tokens.extend(quote! {
850                utoipa::openapi::HeaderBuilder::new().schema(#media_type_schema)
851            })
852        } else {
853            // default header (string type)
854            tokens.extend(quote! {
855                Into::<utoipa::openapi::HeaderBuilder>::into(utoipa::openapi::Header::default())
856            })
857        };
858
859        if let Some(ref description) = self.description {
860            tokens.extend(quote! {
861                .description(Some(#description))
862            })
863        }
864
865        tokens.extend(quote! { .build() })
866    }
867}
868
869mod parse {
870    use syn::parse::ParseStream;
871    use syn::punctuated::Punctuated;
872    use syn::token::{Bracket, Comma};
873    use syn::{bracketed, parenthesized, LitStr, Result};
874
875    use crate::path::example::Example;
876    use crate::{parse_utils, AnyValue};
877
878    use super::Header;
879
880    #[inline]
881    pub(super) fn description(input: ParseStream) -> Result<String> {
882        parse_utils::parse_next_literal_str(input)
883    }
884
885    #[inline]
886    pub(super) fn content_type(input: ParseStream) -> Result<Vec<String>> {
887        parse_utils::parse_next(input, || {
888            let look_content_type = input.lookahead1();
889            if look_content_type.peek(LitStr) {
890                Ok(vec![input.parse::<LitStr>()?.value()])
891            } else if look_content_type.peek(Bracket) {
892                let content_types;
893                bracketed!(content_types in input);
894                Ok(
895                    Punctuated::<LitStr, Comma>::parse_terminated(&content_types)?
896                        .into_iter()
897                        .map(|lit| lit.value())
898                        .collect(),
899                )
900            } else {
901                Err(look_content_type.error())
902            }
903        })
904    }
905
906    #[inline]
907    pub(super) fn headers(input: ParseStream) -> Result<Vec<Header>> {
908        let headers;
909        parenthesized!(headers in input);
910
911        parse_utils::parse_groups(&headers)
912    }
913
914    #[inline]
915    pub(super) fn example(input: ParseStream) -> Result<AnyValue> {
916        parse_utils::parse_next(input, || AnyValue::parse_lit_str_or_json(input))
917    }
918
919    #[inline]
920    pub(super) fn examples(input: ParseStream) -> Result<Punctuated<Example, Comma>> {
921        parse_utils::parse_punctuated_within_parenthesis(input)
922    }
923}