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