utoipa_gen/path/response/
header.rs

1use proc_macro2::TokenStream;
2use quote::{quote, ToTokens};
3use syn::parse::{Parse, ParseStream};
4use syn::{Error, Generics, Ident, LitStr, Token};
5
6use crate::component::features::attributes::Inline;
7use crate::component::{ComponentSchema, Container, TypeTree};
8use crate::path::media_type::ParsedType;
9use crate::{parse_utils, Diagnostics, ToTokensDiagnostics};
10
11/// Parsed representation of response header defined in `#[utoipa::path(..)]` attribute.
12///
13/// Supported configuration format is `("x-my-header-name" = type, description = "optional description of header")`.
14/// The `= type` and the `description = ".."` are optional configurations thus so the same configuration
15/// could be written as follows: `("x-my-header-name")`.
16///
17/// The `type` can be any typical type supported as a header argument such as `String, i32, u64, bool` etc.
18/// and if not provided it will default to `String`.
19///
20/// # Examples
21///
22/// Example of 200 success response which does return nothing back in response body, but returns a
23/// new csrf token in response headers.
24/// ```text
25/// #[utoipa::path(
26///     ...
27///     responses = [
28///         (status = 200, description = "success response",
29///             headers = [
30///                 ("xrfs-token" = String, description = "New csrf token sent back in response header")
31///             ]
32///         ),
33///     ]
34/// )]
35/// ```
36///
37/// Example with default values.
38/// ```text
39/// #[utoipa::path(
40///     ...
41///     responses = [
42///         (status = 200, description = "success response",
43///             headers = [
44///                 ("xrfs-token")
45///             ]
46///         ),
47///     ]
48/// )]
49/// ```
50///
51/// Example with multiple headers with default values.
52/// ```text
53/// #[utoipa::path(
54///     ...
55///     responses = [
56///         (status = 200, description = "success response",
57///             headers = [
58///                 ("xrfs-token"),
59///                 ("another-header"),
60///             ]
61///         ),
62///     ]
63/// )]
64/// ```
65#[derive(Default)]
66#[cfg_attr(feature = "debug", derive(Debug))]
67pub struct Header {
68    pub name: String,
69    value_type: Option<ParsedType<'static>>,
70    description: Option<String>,
71}
72
73impl Parse for Header {
74    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
75        let mut header = Header {
76            name: input.parse::<LitStr>()?.value(),
77            ..Default::default()
78        };
79
80        if input.peek(Token![=]) {
81            input.parse::<Token![=]>()?;
82
83            header.value_type = Some(input.parse().map_err(|error| {
84                Error::new(
85                    error.span(),
86                    format!("unexpected token, expected type such as String, {error}"),
87                )
88            })?);
89        }
90
91        if !input.is_empty() {
92            input.parse::<Token![,]>()?;
93        }
94
95        if input.peek(syn::Ident) {
96            input
97                .parse::<Ident>()
98                .map_err(|error| {
99                    Error::new(
100                        error.span(),
101                        format!("unexpected attribute, expected: description, {error}"),
102                    )
103                })
104                .and_then(|ident| {
105                    if ident != "description" {
106                        return Err(Error::new(
107                            ident.span(),
108                            "unexpected attribute, expected: description",
109                        ));
110                    }
111                    Ok(ident)
112                })?;
113            input.parse::<Token![=]>()?;
114            header.description = Some(input.parse::<LitStr>()?.value());
115        }
116
117        Ok(header)
118    }
119}
120
121impl ToTokensDiagnostics for Header {
122    fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> {
123        if let Some(header_type) = &self.value_type {
124            // header property with custom type
125            let type_tree = TypeTree::from_type(header_type.ty.as_ref())?;
126
127            let media_type_schema = ComponentSchema::new(crate::component::ComponentSchemaProps {
128                type_tree: &type_tree,
129                features: vec![Inline::from(header_type.is_inline).into()],
130                description: None,
131                container: &Container {
132                    generics: &Generics::default(),
133                },
134            })?
135            .to_token_stream();
136
137            tokens.extend(quote! {
138                utoipa::openapi::HeaderBuilder::new().schema(#media_type_schema)
139            })
140        } else {
141            // default header (string type)
142            tokens.extend(quote! {
143                Into::<utoipa::openapi::HeaderBuilder>::into(utoipa::openapi::Header::default())
144            })
145        };
146
147        if let Some(ref description) = self.description {
148            tokens.extend(quote! {
149                .description(Some(#description))
150            })
151        }
152
153        tokens.extend(quote! { .build() });
154
155        Ok(())
156    }
157}
158
159#[inline]
160pub fn headers(input: ParseStream) -> syn::Result<Vec<Header>> {
161    let headers;
162    syn::parenthesized!(headers in input);
163
164    parse_utils::parse_groups_collect(&headers)
165}