use std::collections::BTreeMap;
use proc_macro2::{Ident, TokenStream};
use quote::{ToTokens, quote};
use syn::parse::Parse;
use syn::punctuated::Punctuated;
use syn::token::Paren;
use syn::{Error, Token, parenthesized};
use super::encoding::Encoding;
use super::example::Example;
use super::{PathType, PathTypeTree};
use crate::component::ComponentSchema;
use crate::feature::attributes::Inline;
use crate::{AnyValue, Array, DiagResult, Required, TryToTokens, parse_utils};
#[derive(Default, Debug)]
pub(crate) struct RequestBodyAttr<'r> {
pub(crate) content: Option<PathType<'r>>,
pub(crate) content_type: Option<parse_utils::LitStrOrExpr>,
pub(crate) description: Option<parse_utils::LitStrOrExpr>,
pub(crate) example: Option<AnyValue>,
pub(crate) examples: Option<Punctuated<Example, Token![,]>>,
pub(crate) encoding: BTreeMap<String, Encoding>,
}
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, encoding";
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 attr_name = &*ident.to_string();
match attr_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_lit_str_or_expr(&group)?)
}
"description" => {
request_body_attr.description =
Some(parse_utils::parse_next_lit_str_or_expr(&group)?)
}
"example" => {
request_body_attr.example = Some(parse_utils::parse_next(&group, || {
AnyValue::parse_json(&group)
})?)
}
"examples" => {
request_body_attr.examples =
Some(parse_utils::parse_punctuated_within_parenthesis(&group)?)
}
"encoding" => {
let enc_content;
parenthesized!(enc_content in group);
while !enc_content.is_empty() {
let field_name = enc_content.parse::<syn::LitStr>()?.value();
enc_content.parse::<Token![=]>()?;
let encoding = enc_content.parse::<Encoding>()?;
request_body_attr.encoding.insert(field_name, encoding);
if !enc_content.is_empty() {
enc_content.parse::<Token![,]>()?;
}
}
}
_ => 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 TryToTokens for RequestBodyAttr<'_> {
fn try_to_tokens(&self, tokens: &mut TokenStream) -> DiagResult<()> {
let oapi = crate::oapi_crate();
if let Some(body_type) = &self.content {
let media_type_schema = match body_type {
PathType::RefPath(ref_type) => quote! {
<#ref_type as #oapi::oapi::schema::Schema>::to_schema(components)
},
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: "",
compose_context: None,
})?
.to_token_stream()
}
PathType::InlineSchema(schema, _) => schema.to_token_stream(),
};
let mut content = quote! {
#oapi::oapi::Content::new(#media_type_schema)
};
if let Some(ref example) = self.example {
content.extend(quote! {
.example(#example)
})
}
if let Some(ref examples) = self.examples {
let examples = examples
.iter()
.map(|example| {
let name = &example.name;
quote!((#name, #example))
})
.collect::<Array<TokenStream>>();
content.extend(quote!(
.extend_examples(#examples)
))
}
for (field_name, encoding) in &self.encoding {
content.extend(quote!(
.encoding(#field_name, #encoding)
));
}
match body_type {
PathType::RefPath(_) => {
tokens.extend(quote! {
#oapi::oapi::request_body::RequestBody::new()
.add_content("application/json", #content)
});
}
PathType::MediaType(body_type) => {
let type_tree = body_type.as_type_tree()?;
let required: Required = (!type_tree.is_option()).into();
let content_type = if let Some(content_type) = &self.content_type {
content_type.to_token_stream()
} else {
let content_type = type_tree.get_default_content_type();
quote!(#content_type)
};
tokens.extend(quote! {
#oapi::oapi::request_body::RequestBody::new()
.add_content(#content_type, #content)
.required(#required)
});
}
PathType::InlineSchema(..) => {
unreachable!(
"`PathType::InlineSchema` is not implemented for `RequestBodyAttr`"
);
}
}
}
if let Some(description) = &self.description {
tokens.extend(quote! {
.description(#description)
})
}
Ok(())
}
}