utoipa_gen/path/
request_body.rs1use 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#[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}