use proc_macro2::{Ident, TokenStream as TokenStream2};
use quote::{quote, ToTokens};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parenthesized, parse::Parse, token::Paren, Error, Token};
use crate::component::features::Inline;
use crate::component::ComponentSchema;
use crate::{parse_utils, AnyValue, Array, Required};
use super::example::Example;
use super::{PathType, PathTypeTree};
#[cfg_attr(feature = "debug", derive(Debug))]
pub enum RequestBody<'r> {
Parsed(RequestBodyAttr<'r>),
#[cfg(any(
feature = "actix_extras",
feature = "rocket_extras",
feature = "axum_extras"
))]
Ext(crate::ext::RequestBody<'r>),
}
impl ToTokens for RequestBody<'_> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
Self::Parsed(parsed) => parsed.to_tokens(tokens),
#[cfg(any(
feature = "actix_extras",
feature = "rocket_extras",
feature = "axum_extras"
))]
Self::Ext(ext) => ext.to_tokens(tokens),
}
}
}
#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct RequestBodyAttr<'r> {
content: Option<PathType<'r>>,
content_type: Option<parse_utils::Value>,
description: Option<parse_utils::Value>,
example: Option<AnyValue>,
examples: Option<Punctuated<Example, Comma>>,
}
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";
let lookahead = input.lookahead1();
if lookahead.peek(Paren) {
let group;
parenthesized!(group in input);
let mut request_body_attr = RequestBodyAttr::default();
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" => {
request_body_attr.content = Some(
parse_utils::parse_next(&group, || group.parse()).map_err(|error| {
Error::new(
error.span(),
format!(
"unexpected token, expected type such as String, {error}",
),
)
})?,
);
}
"content_type" => {
request_body_attr.content_type =
Some(parse_utils::parse_next_literal_str_or_expr(&group)?)
}
"description" => {
request_body_attr.description =
Some(parse_utils::parse_next_literal_str_or_expr(&group)?)
}
"example" => {
request_body_attr.example = Some(parse_utils::parse_next(&group, || {
AnyValue::parse_any(&group)
})?)
}
"examples" => {
request_body_attr.examples =
Some(parse_utils::parse_punctuated_within_parenthesis(&group)?)
}
_ => return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE_MESSAGE)),
}
if !group.is_empty() {
group.parse::<Token![,]>()?;
}
}
Ok(request_body_attr)
} else if lookahead.peek(Token![=]) {
input.parse::<Token![=]>()?;
Ok(RequestBodyAttr {
content: Some(input.parse().map_err(|error| {
Error::new(
error.span(),
format!("unexpected token, expected type such as String, {error}"),
)
})?),
..Default::default()
})
} else {
Err(lookahead.error())
}
}
}
impl ToTokens for RequestBodyAttr<'_> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
if let Some(body_type) = &self.content {
let media_type_schema = match body_type {
PathType::Ref(ref_type) => quote! {
utoipa::openapi::schema::Ref::new(#ref_type)
},
PathType::MediaType(body_type) => {
let type_tree = body_type.as_type_tree();
ComponentSchema::new(crate::component::ComponentSchemaProps {
type_tree: &type_tree,
features: Some(vec![Inline::from(body_type.is_inline).into()]),
description: None,
deprecated: None,
object_name: "",
})
.to_token_stream()
}
PathType::InlineSchema(schema, _) => schema.to_token_stream(),
};
let mut content = quote! {
utoipa::openapi::content::ContentBuilder::new()
.schema(#media_type_schema)
};
if let Some(ref example) = self.example {
content.extend(quote! {
.example(Some(#example))
})
}
if let Some(ref examples) = self.examples {
let examples = examples
.iter()
.map(|example| {
let name = &example.name;
quote!((#name, #example))
})
.collect::<Array<TokenStream2>>();
content.extend(quote!(
.examples_from_iter(#examples)
))
}
match body_type {
PathType::Ref(_) => {
tokens.extend(quote! {
utoipa::openapi::request_body::RequestBodyBuilder::new()
.content("application/json", #content.build())
});
}
PathType::MediaType(body_type) => {
let type_tree = body_type.as_type_tree();
let required: Required = (!type_tree.is_option()).into();
let content_type = match &self.content_type {
Some(content_type) => content_type.to_token_stream(),
None => {
let content_type = type_tree.get_default_content_type();
quote!(#content_type)
}
};
tokens.extend(quote! {
utoipa::openapi::request_body::RequestBodyBuilder::new()
.content(#content_type, #content.build())
.required(Some(#required))
});
}
PathType::InlineSchema(_, _) => {
unreachable!("PathType::InlineSchema is not implemented for RequestBodyAttr");
}
}
}
if let Some(ref description) = self.description {
tokens.extend(quote! {
.description(Some(#description))
})
}
tokens.extend(quote! { .build() })
}
}