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

Add request body parsing #9

Merged
merged 1 commit into from
Jan 13, 2022
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
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