use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens};
use syn::parse::ParseStream;
use syn::token::Paren;
use syn::{parse::Parse, Error, Token};
use crate::component::{features::attributes::Extensions, ComponentSchema};
use crate::{
parse_utils,
token_stream::{Diagnostics, ToTokensDiagnostics},
Required,
};
use super::media_type::{MediaTypeAttr, Schema};
use super::parse;
#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct RequestBodyAttr<'r> {
description: Option<parse_utils::LitStrOrExpr>,
content: Vec<MediaTypeAttr<'r>>,
extensions: Option<Extensions>,
}
impl<'r> RequestBodyAttr<'r> {
fn new() -> Self {
Self {
description: Default::default(),
content: vec![MediaTypeAttr::default()],
extensions: Default::default(),
}
}
#[cfg(any(
feature = "actix_extras",
feature = "rocket_extras",
feature = "axum_extras"
))]
pub fn from_schema(schema: Schema<'r>) -> RequestBodyAttr<'r> {
Self {
content: vec![MediaTypeAttr {
schema,
..Default::default()
}],
..Self::new()
}
}
pub fn get_component_schemas(
&self,
) -> Result<impl Iterator<Item = (bool, ComponentSchema)>, Diagnostics> {
Ok(self
.content
.iter()
.map(
|media_type| match media_type.schema.get_component_schema() {
Ok(component_schema) => {
Ok(Some(media_type.schema.is_inline()).zip(component_schema))
}
Err(error) => Err(error),
},
)
.collect::<Result<Vec<_>, Diagnostics>>()?
.into_iter()
.flatten())
}
}
impl Parse for RequestBodyAttr<'_> {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
const EXPECTED_ATTRIBUTE_MESSAGE: &str =
"unexpected attribute, expected any of: content, content_type, description, examples, example, encoding, extensions";
let lookahead = input.lookahead1();
if lookahead.peek(Paren) {
let group;
syn::parenthesized!(group in input);
let mut is_content_group = false;
let mut request_body_attr = RequestBodyAttr::new();
while !group.is_empty() {
let ident = group
.parse::<Ident>()
.map_err(|error| Error::new(error.span(), EXPECTED_ATTRIBUTE_MESSAGE))?;
let attribute_name = &*ident.to_string();
match attribute_name {
"content" => {
if group.peek(Token![=]) {
group.parse::<Token![=]>()?;
let schema = MediaTypeAttr::parse_schema(&group)?;
if let Some(media_type) = request_body_attr.content.get_mut(0) {
media_type.schema = Schema::Default(schema);
}
} else if group.peek(Paren) {
is_content_group = true;
fn group_parser<'a>(
input: ParseStream,
) -> syn::Result<MediaTypeAttr<'a>> {
let buf;
syn::parenthesized!(buf in input);
buf.call(MediaTypeAttr::parse)
}
let media_type =
parse_utils::parse_comma_separated_within_parenthesis_with(
&group,
group_parser,
)?
.into_iter()
.collect::<Vec<_>>();
request_body_attr.content = media_type;
} else {
return Err(Error::new(ident.span(), "unexpected content format, expected either `content = schema` or `content(...)`"));
}
}
"content_type" => {
if is_content_group {
return Err(Error::new(ident.span(), "cannot set `content_type` when content(...) is defined in group form"));
}
let content_type = parse_utils::parse_next(&group, || {
parse_utils::LitStrOrExpr::parse(&group)
}).map_err(|error| Error::new(error.span(),
format!(r#"invalid content_type, must be literal string or expression, e.g. "application/json", {error} "#)
))?;
if let Some(media_type) = request_body_attr.content.get_mut(0) {
media_type.content_type = Some(content_type);
}
}
"description" => {
request_body_attr.description = Some(parse::description(&group)?);
}
"extensions" => {
request_body_attr.extensions = Some(group.parse::<Extensions>()?);
}
_ => {
request_body_attr
.content
.get_mut(0)
.expect("parse request body named attributes must have media type")
.parse_named_attributes(&group, &ident)?;
}
}
if !group.is_empty() {
group.parse::<Token![,]>()?;
}
}
Ok(request_body_attr)
} else if lookahead.peek(Token![=]) {
input.parse::<Token![=]>()?;
let media_type = MediaTypeAttr {
schema: Schema::Default(MediaTypeAttr::parse_schema(input)?),
..MediaTypeAttr::default()
};
Ok(RequestBodyAttr {
content: vec![media_type],
description: None,
extensions: None,
})
} else {
Err(lookahead.error())
}
}
}
impl ToTokensDiagnostics for RequestBodyAttr<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> {
tokens.extend(quote! {
utoipa::openapi::request_body::RequestBodyBuilder::new()
});
let mut any_required = false;
for media_type in self.content.iter() {
let content_type_tokens = match media_type.content_type.as_ref() {
Some(ct) => ct.to_token_stream(),
None => media_type
.schema
.get_default_content_type()?
.to_token_stream(),
};
let content_tokens = media_type.try_to_token_stream()?;
tokens.extend(quote! {
.content(#content_type_tokens, #content_tokens)
});
any_required = any_required
|| media_type
.schema
.get_type_tree()?
.as_ref()
.map(|t| !t.is_option())
.unwrap_or(false);
}
if any_required {
let required: Required = any_required.into();
tokens.extend(quote! {
.required(Some(#required))
})
}
if let Some(ref description) = self.description {
tokens.extend(quote! {
.description(Some(#description))
})
}
if let Some(ref extensions) = self.extensions {
tokens.extend(quote! {
.extensions(Some(#extensions))
});
}
tokens.extend(quote! { .build() });
Ok(())
}
}