Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow for multiple req body content_type #876

Merged
merged 3 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ use self::{
/// where super type declares common code for type aliases.
///
/// In this example we have common `Status` type which accepts one generic type. It is then defined
/// with `#[aliases(...)]` that it is going to be used with [`String`](std::string::String) and [`i32`] values.
/// with `#[aliases(...)]` that it is going to be used with [`String`] and [`i32`] values.
/// The generic argument could also be another [`ToSchema`][to_schema] as well.
/// ```rust
/// # use utoipa::{ToSchema, OpenApi};
Expand Down Expand Up @@ -728,11 +728,12 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
///
/// * `description = "..."` Define the description for the request body object as str.
///
/// * `content_type = "..."` Can be used to override the default behavior of auto resolving the content type
/// from the `content` attribute. If defined the value should be valid content type such as
/// _`application/json`_. By default the content type is _`text/plain`_ for
/// [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and
/// _`application/json`_ for struct and complex enum types.
/// * `content_type = "..."` or `content_type = [...]` Can be used to override the default behavior
/// of auto resolving the content type from the `content` attribute. If defined the value should be valid
/// content type such as _`application/json`_ or a slice of content types within brackets e.g.
/// _`content_type = ["application/json", "text/html"]`_. By default the content type is _`text/plain`_
/// for [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and _`application/json`_
/// for struct and complex enum types.
///
/// * `example = ...` Can be _`json!(...)`_. _`json!(...)`_ should be something that
/// _`serde_json::json!`_ can parse as a _`serde_json::Value`_.
Expand Down Expand Up @@ -766,7 +767,7 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// that free form _`ref`_ is accessible via OpenAPI doc or Swagger UI, users are responsible for making
/// these guarantees.
///
/// * `content_type = "..." | content_type = [...]` Can be used to override the default behavior of auto resolving the content type
/// * `content_type = "..."` or `content_type = [...]` Can be used to override the default behavior of auto resolving the content type
/// from the `body` attribute. If defined the value should be valid content type such as
/// _`application/json`_. By default the content type is _`text/plain`_ for
/// [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and
Expand Down Expand Up @@ -905,7 +906,7 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// The given _`Type`_ can be any Rust type that is JSON parseable. It can be Option, Vec or Map etc.
/// With _`inline(...)`_ the schema will be inlined instead of a referenced which is the default for
/// [`ToSchema`][to_schema] types. Parameter type is placed after `name` with
/// equals sign E.g. _`"id" = String`_
/// equals sign E.g. _`"id" = string`_
///
/// * `in` _**Must be placed after name or parameter_type**_. Define the place of the parameter.
/// This must be one of the variants of [`openapi::path::ParameterIn`][in_enum].
Expand Down Expand Up @@ -1643,7 +1644,7 @@ pub fn openapi(input: TokenStream) -> TokenStream {
///
/// Typically path parameters need to be defined within [`#[utoipa::path(...params(...))]`][path_params] section
/// for the endpoint. But this trait eliminates the need for that when [`struct`][struct]s are used to define parameters.
/// Still [`std::primitive`] and [`String`](std::string::String) path parameters or [`tuple`] style path parameters need to be defined
/// Still [`std::primitive`] and [`String`] path parameters or [`tuple`] style path parameters need to be defined
/// within `params(...)` section if description or other than default configuration need to be given.
///
/// You can use the Rust's own `#[deprecated]` attribute on field to mark it as
Expand Down
43 changes: 43 additions & 0 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,3 +654,46 @@ impl PathTypeTree for TypeTree<'_> {
}
}
}

mod parse {
use syn::parse::ParseStream;
use syn::punctuated::Punctuated;
use syn::token::{Bracket, Comma};
use syn::{bracketed, Result};

use crate::path::example::Example;
use crate::{parse_utils, AnyValue};

#[inline]
pub(super) fn description(input: ParseStream) -> Result<parse_utils::Value> {
parse_utils::parse_next_literal_str_or_expr(input)
}

#[inline]
pub(super) fn content_type(input: ParseStream) -> Result<Vec<parse_utils::Value>> {
parse_utils::parse_next(input, || {
let look_content_type = input.lookahead1();
if look_content_type.peek(Bracket) {
let content_types;
bracketed!(content_types in input);
Ok(
Punctuated::<parse_utils::Value, Comma>::parse_terminated(&content_types)?
.into_iter()
.collect(),
)
} else {
Ok(vec![input.parse::<parse_utils::Value>()?])
}
})
}

#[inline]
pub(super) fn example(input: ParseStream) -> Result<AnyValue> {
parse_utils::parse_next(input, || AnyValue::parse_lit_str_or_json(input))
}

#[inline]
pub(super) fn examples(input: ParseStream) -> Result<Punctuated<Example, Comma>> {
parse_utils::parse_punctuated_within_parenthesis(input)
}
}
39 changes: 21 additions & 18 deletions utoipa-gen/src/path/request_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::component::ComponentSchema;
use crate::{impl_to_tokens_diagnostics, parse_utils, AnyValue, Array, Diagnostics, Required};

use super::example::Example;
use super::{PathType, PathTypeTree};
use super::{parse, PathType, PathTypeTree};

#[cfg_attr(feature = "debug", derive(Debug))]
pub enum RequestBody<'r> {
Expand Down Expand Up @@ -77,7 +77,7 @@ impl ToTokens for RequestBody<'_> {
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct RequestBodyAttr<'r> {
content: Option<PathType<'r>>,
content_type: Option<parse_utils::Value>,
content_type: Vec<parse_utils::Value>,
description: Option<parse_utils::Value>,
example: Option<AnyValue>,
examples: Option<Punctuated<Example, Comma>>,
Expand Down Expand Up @@ -114,21 +114,16 @@ impl Parse for RequestBodyAttr<'_> {
);
}
"content_type" => {
request_body_attr.content_type =
Some(parse_utils::parse_next_literal_str_or_expr(&group)?)
request_body_attr.content_type = parse::content_type(&group)?;
}
"description" => {
request_body_attr.description =
Some(parse_utils::parse_next_literal_str_or_expr(&group)?)
request_body_attr.description = Some(parse::description(&group)?);
}
"example" => {
request_body_attr.example = Some(parse_utils::parse_next(&group, || {
AnyValue::parse_any(&group)
})?)
request_body_attr.example = Some(parse::example(&group)?);
}
"examples" => {
request_body_attr.examples =
Some(parse_utils::parse_punctuated_within_parenthesis(&group)?)
request_body_attr.examples = Some(parse::examples(&group)?);
}
_ => return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE_MESSAGE)),
}
Expand Down Expand Up @@ -210,18 +205,26 @@ impl RequestBodyAttr<'_> {
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)
}
let content_types = if self.content_type.is_empty() {
let content_type = type_tree.get_default_content_type();
vec![quote!(#content_type)]
} else {
self.content_type
.iter()
.map(|content_type| content_type.to_token_stream())
.collect()
};

tokens.extend(quote! {
utoipa::openapi::request_body::RequestBodyBuilder::new()
.content(#content_type, #content.build())
.required(Some(#required))
});

for content_type in content_types {
tokens.extend(quote! {
.content(#content_type, #content.build())
});
}
}
PathType::InlineSchema(_, _) => {
unreachable!("PathType::InlineSchema is not implemented for RequestBodyAttr");
Expand Down
63 changes: 9 additions & 54 deletions utoipa-gen/src/path/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::{
impl_to_tokens_diagnostics, parse_utils, AnyValue, Array, Diagnostics,
};

use super::{example::Example, status::STATUS_CODES, InlineType, PathType, PathTypeTree};
use super::{example::Example, parse, status::STATUS_CODES, InlineType, PathType, PathTypeTree};

pub mod derive;

Expand Down Expand Up @@ -117,7 +117,7 @@ impl Parse for ResponseTuple<'_> {
Some(parse::content_type(input)?);
}
"headers" => {
response.as_value(input.span())?.headers = parse::headers(input)?;
response.as_value(input.span())?.headers = headers(input)?;
}
"example" => {
response.as_value(input.span())?.example = Some(parse::example(input)?);
Expand Down Expand Up @@ -458,7 +458,7 @@ impl Parse for DeriveToResponseValue {
response.content_type = Some(parse::content_type(input)?);
}
"headers" => {
response.headers = parse::headers(input)?;
response.headers = headers(input)?;
}
"example" => {
response.example = Some((parse::example(input)?, ident));
Expand Down Expand Up @@ -553,7 +553,7 @@ impl Parse for DeriveIntoResponsesValue {
response.content_type = Some(parse::content_type(input)?);
}
"headers" => {
response.headers = parse::headers(input)?;
response.headers = headers(input)?;
}
"example" => {
response.example = Some((parse::example(input)?, ident));
Expand Down Expand Up @@ -895,55 +895,10 @@ impl_to_tokens_diagnostics! {
}
}

mod parse {
use syn::parse::ParseStream;
use syn::punctuated::Punctuated;
use syn::token::{Bracket, Comma};
use syn::{bracketed, parenthesized, Result};
#[inline]
fn headers(input: ParseStream) -> syn::Result<Vec<Header>> {
let headers;
syn::parenthesized!(headers in input);

use crate::path::example::Example;
use crate::{parse_utils, AnyValue};

use super::Header;

#[inline]
pub(super) fn description(input: ParseStream) -> Result<parse_utils::Value> {
parse_utils::parse_next_literal_str_or_expr(input)
}

#[inline]
pub(super) fn content_type(input: ParseStream) -> Result<Vec<parse_utils::Value>> {
parse_utils::parse_next(input, || {
let look_content_type = input.lookahead1();
if look_content_type.peek(Bracket) {
let content_types;
bracketed!(content_types in input);
Ok(
Punctuated::<parse_utils::Value, Comma>::parse_terminated(&content_types)?
.into_iter()
.collect(),
)
} else {
Ok(vec![input.parse::<parse_utils::Value>()?])
}
})
}

#[inline]
pub(super) fn headers(input: ParseStream) -> Result<Vec<Header>> {
let headers;
parenthesized!(headers in input);

parse_utils::parse_groups(&headers)
}

#[inline]
pub(super) fn example(input: ParseStream) -> Result<AnyValue> {
parse_utils::parse_next(input, || AnyValue::parse_lit_str_or_json(input))
}

#[inline]
pub(super) fn examples(input: ParseStream) -> Result<Punctuated<Example, Comma>> {
parse_utils::parse_punctuated_within_parenthesis(input)
}
parse_utils::parse_groups(&headers)
}
36 changes: 36 additions & 0 deletions utoipa-gen/tests/request_body_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,42 @@ fn derive_request_body_complex_success() {
);
}

test_fn! {
module: derive_request_body_complex_multi_content_type,
body: (content = Foo, description = "Create new Foo", content_type = ["text/xml", "application/json"])
}

#[test]
fn derive_request_body_complex_multi_content_type_success() {
#[derive(OpenApi, Default)]
#[openapi(paths(derive_request_body_complex_multi_content_type::post_foo))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();

let request_body: &Value = doc.pointer("/paths/~1foo/post/requestBody").unwrap();

assert_json_eq!(
request_body,
json!({
"content": {
"text/xml": {
"schema": {
"$ref": "#/components/schemas/Foo"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/Foo"
}
}
},
"description": "Create new Foo",
"required": true
})
);
}

test_fn! {
module: derive_request_body_complex_inline,
body: (content = inline(Foo), description = "Create new Foo", content_type = "text/xml")
Expand Down
Loading