Skip to content

Commit

Permalink
Add request body parsing (#9)
Browse files Browse the repository at this point in the history
* Add feature request body parsing
* Add tests for all scenarios
  • Loading branch information
juhaku committed Jan 13, 2022
1 parent f4fce02 commit 3be6dbe
Show file tree
Hide file tree
Showing 6 changed files with 491 additions and 58 deletions.
14 changes: 11 additions & 3 deletions src/openapi/request_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use super::Required;
use super::{Component, Required};

#[non_exhaustive]
#[derive(Serialize, Deserialize, Default)]
Expand Down Expand Up @@ -43,7 +43,15 @@ impl RequestBody {
}

#[derive(Serialize, Deserialize, Default)]
#[non_exhaustive]
pub struct Content {
// TODO implement schema somehow
pub schema: String,
pub schema: Component,
}

impl Content {
pub fn new<I: Into<Component>>(schema: I) -> Self {
Self {
schema: schema.into(),
}
}
}
183 changes: 183 additions & 0 deletions tests/request_body_derive_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use utoipa::OpenApi;

mod common;

macro_rules! test_fn {
( module: $name:ident, body: $body:expr ) => {
#[allow(unused)]
mod $name {

struct Foo {
name: String,
}
#[utoipa::path(
post,
path = "/foo",
request_body = $body,
responses = [
(200, "success", String),
]
)]
fn post_foo() {}
}
};
}

test_fn! {
module: derive_request_body_simple,
body: Foo
}

#[test]
fn derive_path_request_body_simple_success() {
#[derive(OpenApi, Default)]
#[openapi(handler_files = [], handlers = [derive_request_body_simple::post_foo])]
struct ApiDoc;

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

assert_value! {doc=>
"paths./foo.post.requestBody.content.application/json.schema.$ref" = r###""#/components/schemas/Foo""###, "Request body content object type"
"paths./foo.post.requestBody.content.text/plain" = r###"null"###, "Request body content object type not text/plain"
"paths./foo.post.requestBody.required" = r###"null"###, "Request body required"
"paths./foo.post.requestBody.description" = r###"null"###, "Request body description"
}
}

test_fn! {
module: derive_request_body_simple_array,
body: [Foo]
}

#[test]
fn derive_path_request_body_simple_array_success() {
#[derive(OpenApi, Default)]
#[openapi(handler_files = [], handlers = [derive_request_body_simple_array::post_foo])]
struct ApiDoc;

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

assert_value! {doc=>
"paths./foo.post.requestBody.content.application/json.schema.$ref" = r###"null"###, "Request body content object type"
"paths./foo.post.requestBody.content.application/json.schema.items.$ref" = r###""#/components/schemas/Foo""###, "Request body content items object type"
"paths./foo.post.requestBody.content.application/json.schema.type" = r###""array""###, "Request body content items type"
"paths./foo.post.requestBody.content.text/plain" = r###"null"###, "Request body content object type not text/plain"
"paths./foo.post.requestBody.required" = r###"null"###, "Request body required"
"paths./foo.post.requestBody.description" = r###"null"###, "Request body description"
}
}

test_fn! {
module: derive_request_body_primitive_simple,
body: String
}

#[test]
fn derive_request_body_primitive_simple_success() {
#[derive(OpenApi, Default)]
#[openapi(handler_files = [], handlers = [derive_request_body_primitive_simple::post_foo])]
struct ApiDoc;

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

assert_value! {doc=>
"paths./foo.post.requestBody.content.application/json.schema.$ref" = r###"null"###, "Request body content object type not application/json"
"paths./foo.post.requestBody.content.application/json.schema.items.$ref" = r###"null"###, "Request body content items object type"
"paths./foo.post.requestBody.content.application/json.schema.type" = r###"null"###, "Request body content items type"
"paths./foo.post.requestBody.content.text/plain.schema.type" = r###""string""###, "Request body content object type"
"paths./foo.post.requestBody.required" = r###"null"###, "Request body required"
"paths./foo.post.requestBody.description" = r###"null"###, "Request body description"
}
}

test_fn! {
module: derive_request_body_primitive_simple_array,
body: [u64]
}

#[test]
fn derive_request_body_primitive_array_success() {
#[derive(OpenApi, Default)]
#[openapi(handler_files = [], handlers = [derive_request_body_primitive_simple_array::post_foo])]
struct ApiDoc;

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

assert_value! {doc=>
"paths./foo.post.requestBody.content.application/json" = r###"null"###, "Request body content object type not application/json"
"paths./foo.post.requestBody.content.text/plain.schema.type" = r###""array""###, "Request body content object item type"
"paths./foo.post.requestBody.content.text/plain.schema.items.type" = r###""integer""###, "Request body content items object type"
"paths./foo.post.requestBody.content.text/plain.schema.items.format" = r###""int64""###, "Request body content items object format"
"paths./foo.post.requestBody.required" = r###"null"###, "Request body required"
"paths./foo.post.requestBody.description" = r###"null"###, "Request body description"
}
}

test_fn! {
module: derive_request_body_complex,
body: (content = Foo, required, description = "Create new Foo", content_type = "text/xml")
}

#[test]
fn derive_request_body_complex_success() {
#[derive(OpenApi, Default)]
#[openapi(handler_files = [], handlers = [derive_request_body_complex::post_foo])]
struct ApiDoc;

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

assert_value! {doc=>
"paths./foo.post.requestBody.content.application/json" = r###"null"###, "Request body content object type not application/json"
"paths./foo.post.requestBody.content.text/xml.schema.$ref" = r###""#/components/schemas/Foo""###, "Request body content object type"
"paths./foo.post.requestBody.content.text/plain.schema.type" = r###"null"###, "Request body content object item type"
"paths./foo.post.requestBody.content.text/plain.schema.items.type" = r###"null"###, "Request body content items object type"
"paths./foo.post.requestBody.required" = r###"true"###, "Request body required"
"paths./foo.post.requestBody.description" = r###""Create new Foo""###, "Request body description"
}
}

test_fn! {
module: derive_request_body_complex_required_explisit,
body: (content = Foo, required = false, description = "Create new Foo", content_type = "text/xml")
}

#[test]
fn derive_request_body_complex_required_explisit_false_success() {
#[derive(OpenApi, Default)]
#[openapi(handler_files = [], handlers = [derive_request_body_complex_required_explisit::post_foo])]
struct ApiDoc;

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

assert_value! {doc=>
"paths./foo.post.requestBody.content.application/json" = r###"null"###, "Request body content object type not application/json"
"paths./foo.post.requestBody.content.text/xml.schema.$ref" = r###""#/components/schemas/Foo""###, "Request body content object type"
"paths./foo.post.requestBody.content.text/plain.schema.type" = r###"null"###, "Request body content object item type"
"paths./foo.post.requestBody.content.text/plain.schema.items.type" = r###"null"###, "Request body content items object type"
"paths./foo.post.requestBody.required" = r###"false"###, "Request body required"
"paths./foo.post.requestBody.description" = r###""Create new Foo""###, "Request body description"
}
}

test_fn! {
module: derive_request_body_complex_primitive_array,
body: (content = [u32], description = "Create new foo references")
}

#[test]
fn derive_request_body_complex_primitive_array_success() {
#[derive(OpenApi, Default)]
#[openapi(handler_files = [], handlers = [derive_request_body_complex_primitive_array::post_foo])]
struct ApiDoc;

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

assert_value! {doc=>
"paths./foo.post.requestBody.content.application/json" = r###"null"###, "Request body content object type not application/json"
"paths./foo.post.requestBody.content.text/plain.schema.type" = r###""array""###, "Request body content object item type"
"paths./foo.post.requestBody.content.text/plain.schema.items.type" = r###""integer""###, "Request body content items object type"
"paths./foo.post.requestBody.content.text/plain.schema.items.format" = r###""int32""###, "Request body content items object format"
"paths./foo.post.requestBody.required" = r###"null"###, "Request body required"
"paths./foo.post.requestBody.description" = r###""Create new foo references""###, "Request body description"
}
}
3 changes: 2 additions & 1 deletion tests/utoipa_gen_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct Foo {
///
/// Delete foo entity by what
#[utoipa::path(
request_body = (content = Foo, required, description = "foobar", content_type = "text/xml"),
responses = [
(200, "success", String),
(400, "my bad error", u64),
Expand Down Expand Up @@ -44,5 +45,5 @@ fn derive_openapi() {
#[openapi(handler_files = [], handlers = [foo_delete])]
struct ApiDoc;

println!("{:?}", ApiDoc::openapi().to_json())
println!("{}", ApiDoc::openapi().to_pretty_json().unwrap());
}
85 changes: 82 additions & 3 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ use ext::actix::update_parameters_from_arguments;

use ext::{ArgumentResolver, PathOperationResolver, PathOperations, PathResolver};
use proc_macro::TokenStream;
use quote::{format_ident, quote, quote_spanned};
use quote::{format_ident, quote, quote_spanned, ToTokens};

use proc_macro2::{Ident, TokenStream as TokenStream2};
use syn::{
bracketed, parse::Parse, punctuated::Punctuated, Attribute, DeriveInput, ExprPath, LitStr,
Token,
bracketed,
parse::{Parse, ParseStream},
punctuated::Punctuated,
token::Bracket,
Attribute, DeriveInput, ExprPath, LitStr, Token,
};

mod attribute;
Expand All @@ -22,6 +25,7 @@ mod component_type;
mod ext;
mod info;
mod path;
mod request_body;

use proc_macro_error::*;

Expand Down Expand Up @@ -329,3 +333,78 @@ fn impl_paths<I: IntoIterator<Item = ExprPath>>(
},
)
}

enum Deprecated {
True,
False,
}

impl From<bool> for Deprecated {
fn from(bool: bool) -> Self {
if bool {
Self::True
} else {
Self::False
}
}
}

impl ToTokens for Deprecated {
fn to_tokens(&self, tokens: &mut TokenStream2) {
tokens.extend(match self {
Self::False => quote! { utoipa::openapi::Deprecated::False },
Self::True => quote! { utoipa::openapi::Deprecated::True },
})
}
}

enum Required {
True,
False,
}

impl From<bool> for Required {
fn from(bool: bool) -> Self {
if bool {
Self::True
} else {
Self::False
}
}
}

impl ToTokens for Required {
fn to_tokens(&self, tokens: &mut TokenStream2) {
tokens.extend(match self {
Self::False => quote! { utoipa::openapi::Required::False },
Self::True => quote! { utoipa::openapi::Required::True },
})
}
}

/// Media type is wrapper around type and information is type an array
#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
struct MediaType {
ty: Option<Ident>,
is_array: bool,
}

impl Parse for MediaType {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut is_array = false;
let ty = if input.peek(Bracket) {
is_array = true;
let group;
bracketed!(group in input);
group.parse::<Ident>().unwrap()
} else {
input.parse::<Ident>().unwrap()
};

Ok(MediaType {
ty: Some(ty),
is_array,
})
}
}
Loading

0 comments on commit 3be6dbe

Please sign in to comment.