utoipa_gen/path/
request_body.rs

1use proc_macro2::{Ident, TokenStream as TokenStream2};
2use quote::{quote, ToTokens};
3use syn::punctuated::Punctuated;
4use syn::token::Comma;
5use syn::{parenthesized, parse::Parse, token::Paren, Error, Token};
6
7use crate::component::features::Inline;
8use crate::component::ComponentSchema;
9use crate::{parse_utils, AnyValue, Array, Required};
10
11use super::example::Example;
12use super::{PathType, PathTypeTree};
13
14#[cfg_attr(feature = "debug", derive(Debug))]
15pub enum RequestBody<'r> {
16    Parsed(RequestBodyAttr<'r>),
17    #[cfg(any(
18        feature = "actix_extras",
19        feature = "rocket_extras",
20        feature = "axum_extras"
21    ))]
22    Ext(crate::ext::RequestBody<'r>),
23}
24
25impl ToTokens for RequestBody<'_> {
26    fn to_tokens(&self, tokens: &mut TokenStream2) {
27        match self {
28            Self::Parsed(parsed) => parsed.to_tokens(tokens),
29            #[cfg(any(
30                feature = "actix_extras",
31                feature = "rocket_extras",
32                feature = "axum_extras"
33            ))]
34            Self::Ext(ext) => ext.to_tokens(tokens),
35        }
36    }
37}
38
39/// Parsed information related to request body of path.
40///
41/// Supported configuration options:
42///   * **content** Request body content object type. Can also be array e.g. `content = [String]`.
43///   * **content_type** Defines the actual content mime type of a request body such as `application/json`.
44///     If not provided really rough guess logic is used. Basically all primitive types are treated as `text/plain`
45///     and Object types are expected to be `application/json` by default.
46///   * **description** Additional description for request body content type.
47/// # Examples
48///
49/// Request body in path with all supported info. Where content type is treated as a String and expected
50/// to be xml.
51/// ```text
52/// #[utoipa::path(
53///    request_body = (content = String, description = "foobar", content_type = "text/xml"),
54/// )]
55///
56/// It is also possible to provide the request body type simply by providing only the content object type.
57/// ```text
58/// #[utoipa::path(
59///    request_body = Foo,
60/// )]
61/// ```
62///
63/// Or the request body content can also be an array as well by surrounding it with brackets `[..]`.
64/// ```text
65/// #[utoipa::path(
66///    request_body = [Foo],
67/// )]
68/// ```
69///
70/// To define optional request body just wrap the type in `Option<type>`.
71/// ```text
72/// #[utoipa::path(
73///    request_body = Option<[Foo]>,
74/// )]
75/// ```
76#[derive(Default)]
77#[cfg_attr(feature = "debug", derive(Debug))]
78pub struct RequestBodyAttr<'r> {
79    content: Option<PathType<'r>>,
80    content_type: Option<String>,
81    description: Option<String>,
82    example: Option<AnyValue>,
83    examples: Option<Punctuated<Example, Comma>>,
84}
85
86impl Parse for RequestBodyAttr<'_> {
87    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
88        const EXPECTED_ATTRIBUTE_MESSAGE: &str =
89            "unexpected attribute, expected any of: content, content_type, description, examples";
90        let lookahead = input.lookahead1();
91
92        if lookahead.peek(Paren) {
93            let group;
94            parenthesized!(group in input);
95
96            let mut request_body_attr = RequestBodyAttr::default();
97            while !group.is_empty() {
98                let ident = group
99                    .parse::<Ident>()
100                    .map_err(|error| Error::new(error.span(), EXPECTED_ATTRIBUTE_MESSAGE))?;
101                let attribute_name = &*ident.to_string();
102
103                match attribute_name {
104                    "content" => {
105                        request_body_attr.content = Some(
106                            parse_utils::parse_next(&group, || group.parse()).map_err(|error| {
107                                Error::new(
108                                    error.span(),
109                                    format!(
110                                        "unexpected token, expected type such as String, {error}",
111                                    ),
112                                )
113                            })?,
114                        );
115                    }
116                    "content_type" => {
117                        request_body_attr.content_type =
118                            Some(parse_utils::parse_next_literal_str(&group)?)
119                    }
120                    "description" => {
121                        request_body_attr.description =
122                            Some(parse_utils::parse_next_literal_str(&group)?)
123                    }
124                    "example" => {
125                        request_body_attr.example = Some(parse_utils::parse_next(&group, || {
126                            AnyValue::parse_json(&group)
127                        })?)
128                    }
129                    "examples" => {
130                        request_body_attr.examples =
131                            Some(parse_utils::parse_punctuated_within_parenthesis(&group)?)
132                    }
133                    _ => return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE_MESSAGE)),
134                }
135
136                if !group.is_empty() {
137                    group.parse::<Token![,]>()?;
138                }
139            }
140
141            Ok(request_body_attr)
142        } else if lookahead.peek(Token![=]) {
143            input.parse::<Token![=]>()?;
144
145            Ok(RequestBodyAttr {
146                content: Some(input.parse().map_err(|error| {
147                    Error::new(
148                        error.span(),
149                        format!("unexpected token, expected type such as String, {error}"),
150                    )
151                })?),
152                ..Default::default()
153            })
154        } else {
155            Err(lookahead.error())
156        }
157    }
158}
159
160impl ToTokens for RequestBodyAttr<'_> {
161    fn to_tokens(&self, tokens: &mut TokenStream2) {
162        if let Some(body_type) = &self.content {
163            let media_type_schema = match body_type {
164                PathType::Ref(ref_type) => quote! {
165                    utoipa::openapi::schema::Ref::new(#ref_type)
166                },
167                PathType::MediaType(body_type) => {
168                    let type_tree = body_type.as_type_tree();
169                    ComponentSchema::new(crate::component::ComponentSchemaProps {
170                        type_tree: &type_tree,
171                        features: Some(vec![Inline::from(body_type.is_inline).into()]),
172                        description: None,
173                        deprecated: None,
174                        object_name: "",
175                    })
176                    .to_token_stream()
177                }
178                PathType::InlineSchema(schema, _) => schema.to_token_stream(),
179            };
180            let mut content = quote! {
181                utoipa::openapi::content::ContentBuilder::new()
182                    .schema(#media_type_schema)
183            };
184
185            if let Some(ref example) = self.example {
186                content.extend(quote! {
187                    .example(Some(#example))
188                })
189            }
190            if let Some(ref examples) = self.examples {
191                let examples = examples
192                    .iter()
193                    .map(|example| {
194                        let name = &example.name;
195                        quote!((#name, #example))
196                    })
197                    .collect::<Array<TokenStream2>>();
198                content.extend(quote!(
199                    .examples_from_iter(#examples)
200                ))
201            }
202
203            match body_type {
204                PathType::Ref(_) => {
205                    tokens.extend(quote! {
206                        utoipa::openapi::request_body::RequestBodyBuilder::new()
207                            .content("application/json", #content.build())
208                    });
209                }
210                PathType::MediaType(body_type) => {
211                    let type_tree = body_type.as_type_tree();
212                    let required: Required = (!type_tree.is_option()).into();
213                    let content_type = self
214                        .content_type
215                        .as_deref()
216                        .unwrap_or_else(|| type_tree.get_default_content_type());
217                    tokens.extend(quote! {
218                        utoipa::openapi::request_body::RequestBodyBuilder::new()
219                            .content(#content_type, #content.build())
220                            .required(Some(#required))
221                    });
222                }
223                PathType::InlineSchema(_, _) => {
224                    unreachable!("PathType::InlineSchema is not implemented for RequestBodyAttr");
225                }
226            }
227        }
228
229        if let Some(ref description) = self.description {
230            tokens.extend(quote! {
231                .description(Some(#description))
232            })
233        }
234
235        tokens.extend(quote! { .build() })
236    }
237}