utoipa_gen/path/
request_body.rs

1use proc_macro2::{Ident, TokenStream};
2use quote::{quote, ToTokens};
3use syn::parse::ParseStream;
4use syn::token::Paren;
5use syn::{parse::Parse, Error, Token};
6
7use crate::component::{features::attributes::Extensions, ComponentSchema};
8use crate::{parse_utils, Diagnostics, Required, ToTokensDiagnostics};
9
10use super::media_type::{MediaTypeAttr, Schema};
11use super::parse;
12
13/// Parsed information related to request body of path.
14///
15/// Supported configuration options:
16///   * **content** Request body content object type. Can also be array e.g. `content = [String]`.
17///   * **content_type** Defines the actual content mime type of a request body such as `application/json`.
18///     If not provided really rough guess logic is used. Basically all primitive types are treated as `text/plain`
19///     and Object types are expected to be `application/json` by default.
20///   * **description** Additional description for request body content type.
21/// # Examples
22///
23/// Request body in path with all supported info. Where content type is treated as a String and expected
24/// to be xml.
25/// ```text
26/// #[utoipa::path(
27///    request_body(content = String, description = "foobar", content_type = "text/xml"),
28/// )]
29///
30/// It is also possible to provide the request body type simply by providing only the content object type.
31/// ```text
32/// #[utoipa::path(
33///    request_body = Foo,
34/// )]
35/// ```
36///
37/// Or the request body content can also be an array as well by surrounding it with brackets `[..]`.
38/// ```text
39/// #[utoipa::path(
40///    request_body = [Foo],
41/// )]
42/// ```
43///
44/// To define optional request body just wrap the type in `Option<type>`.
45/// ```text
46/// #[utoipa::path(
47///    request_body = Option<[Foo]>,
48/// )]
49/// ```
50///
51/// request_body(
52///     description = "This is request body",
53///     content_type = "content/type",
54///     content = Schema,
55///     example = ...,
56///     examples(..., ...),
57///     encoding(...)
58/// )
59#[derive(Default)]
60#[cfg_attr(feature = "debug", derive(Debug))]
61pub struct RequestBodyAttr<'r> {
62    description: Option<parse_utils::LitStrOrExpr>,
63    content: Vec<MediaTypeAttr<'r>>,
64    extensions: Option<Extensions>,
65}
66
67impl<'r> RequestBodyAttr<'r> {
68    fn new() -> Self {
69        Self {
70            description: Default::default(),
71            content: vec![MediaTypeAttr::default()],
72            extensions: Default::default(),
73        }
74    }
75
76    #[cfg(any(
77        feature = "actix_extras",
78        feature = "rocket_extras",
79        feature = "axum_extras"
80    ))]
81    pub fn from_schema(schema: Schema<'r>) -> RequestBodyAttr<'r> {
82        Self {
83            content: vec![MediaTypeAttr {
84                schema,
85                ..Default::default()
86            }],
87            ..Self::new()
88        }
89    }
90
91    pub fn get_component_schemas(
92        &self,
93    ) -> Result<impl Iterator<Item = (bool, ComponentSchema)>, Diagnostics> {
94        Ok(self
95            .content
96            .iter()
97            .map(
98                |media_type| match media_type.schema.get_component_schema() {
99                    Ok(component_schema) => {
100                        Ok(Some(media_type.schema.is_inline()).zip(component_schema))
101                    }
102                    Err(error) => Err(error),
103                },
104            )
105            .collect::<Result<Vec<_>, Diagnostics>>()?
106            .into_iter()
107            .flatten())
108    }
109}
110
111impl Parse for RequestBodyAttr<'_> {
112    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
113        const EXPECTED_ATTRIBUTE_MESSAGE: &str =
114            "unexpected attribute, expected any of: content, content_type, description, examples, example, encoding, extensions";
115        let lookahead = input.lookahead1();
116
117        if lookahead.peek(Paren) {
118            let group;
119            syn::parenthesized!(group in input);
120
121            let mut is_content_group = false;
122            let mut request_body_attr = RequestBodyAttr::new();
123            while !group.is_empty() {
124                let ident = group
125                    .parse::<Ident>()
126                    .map_err(|error| Error::new(error.span(), EXPECTED_ATTRIBUTE_MESSAGE))?;
127                let attribute_name = &*ident.to_string();
128
129                match attribute_name {
130                    "content" => {
131                        if group.peek(Token![=]) {
132                            group.parse::<Token![=]>()?;
133                            let schema = MediaTypeAttr::parse_schema(&group)?;
134                            if let Some(media_type) = request_body_attr.content.get_mut(0) {
135                                media_type.schema = Schema::Default(schema);
136                            }
137                        } else if group.peek(Paren) {
138                            is_content_group = true;
139                            fn group_parser<'a>(
140                                input: ParseStream,
141                            ) -> syn::Result<MediaTypeAttr<'a>> {
142                                let buf;
143                                syn::parenthesized!(buf in input);
144                                buf.call(MediaTypeAttr::parse)
145                            }
146
147                            let media_type =
148                                parse_utils::parse_comma_separated_within_parethesis_with(
149                                    &group,
150                                    group_parser,
151                                )?
152                                .into_iter()
153                                .collect::<Vec<_>>();
154
155                            request_body_attr.content = media_type;
156                        } else {
157                            return Err(Error::new(ident.span(), "unexpected content format, expected either `content = schema` or `content(...)`"));
158                        }
159                    }
160                    "content_type" => {
161                        if is_content_group {
162                            return Err(Error::new(ident.span(), "cannot set `content_type` when content(...) is defined in group form"));
163                        }
164                        let content_type = parse_utils::parse_next(&group, || {
165                            parse_utils::LitStrOrExpr::parse(&group)
166                        }).map_err(|error| Error::new(error.span(),
167                                format!(r#"invalid content_type, must be literal string or expression, e.g. "application/json", {error} "#)
168                            ))?;
169
170                        if let Some(media_type) = request_body_attr.content.get_mut(0) {
171                            media_type.content_type = Some(content_type);
172                        }
173                    }
174                    "description" => {
175                        request_body_attr.description = Some(parse::description(&group)?);
176                    }
177                    "extensions" => {
178                        request_body_attr.extensions = Some(group.parse::<Extensions>()?);
179                    }
180                    _ => {
181                        request_body_attr
182                            .content
183                            .get_mut(0)
184                            .expect("parse request body named attributes must have media type")
185                            .parse_named_attributes(&group, &ident)?;
186                    }
187                }
188
189                if !group.is_empty() {
190                    group.parse::<Token![,]>()?;
191                }
192            }
193
194            Ok(request_body_attr)
195        } else if lookahead.peek(Token![=]) {
196            input.parse::<Token![=]>()?;
197
198            let media_type = MediaTypeAttr {
199                schema: Schema::Default(MediaTypeAttr::parse_schema(input)?),
200                ..MediaTypeAttr::default()
201            };
202
203            Ok(RequestBodyAttr {
204                content: vec![media_type],
205                description: None,
206                extensions: None,
207            })
208        } else {
209            Err(lookahead.error())
210        }
211    }
212}
213
214impl ToTokensDiagnostics for RequestBodyAttr<'_> {
215    fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> {
216        tokens.extend(quote! {
217            utoipa::openapi::request_body::RequestBodyBuilder::new()
218        });
219
220        let mut any_required = false;
221
222        for media_type in self.content.iter() {
223            let content_type_tokens = match media_type.content_type.as_ref() {
224                Some(ct) => ct.to_token_stream(),
225                None => media_type
226                    .schema
227                    .get_default_content_type()?
228                    .to_token_stream(),
229            };
230
231            let content_tokens = media_type.try_to_token_stream()?;
232
233            tokens.extend(quote! {
234                .content(#content_type_tokens, #content_tokens)
235            });
236
237            any_required = any_required
238                || media_type
239                    .schema
240                    .get_type_tree()?
241                    .as_ref()
242                    .map(|t| !t.is_option())
243                    .unwrap_or(false);
244        }
245
246        if any_required {
247            let required: Required = any_required.into();
248            tokens.extend(quote! {
249                .required(Some(#required))
250            })
251        }
252        if let Some(ref description) = self.description {
253            tokens.extend(quote! {
254                .description(Some(#description))
255            })
256        }
257        if let Some(ref extensions) = self.extensions {
258            tokens.extend(quote! {
259                .extensions(Some(#extensions))
260            });
261        }
262
263        tokens.extend(quote! { .build() });
264
265        Ok(())
266    }
267}