From 5282dbbe6ca3e02624880f8cb9a88f1dd94c70fe Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 20 Oct 2019 15:42:26 +0200 Subject: [PATCH] add derive for raw request body --- README.md | 18 ++++- gotham_restful/src/lib.rs | 27 ++++++- gotham_restful/src/types.rs | 40 ++++++---- gotham_restful_derive/src/from_body.rs | 69 +++++++++++++++++ gotham_restful_derive/src/lib.rs | 16 ++++ gotham_restful_derive/src/request_body.rs | 91 +++++++++++++++++++++++ 6 files changed, 243 insertions(+), 18 deletions(-) create mode 100644 gotham_restful_derive/src/from_body.rs create mode 100644 gotham_restful_derive/src/request_body.rs diff --git a/README.md b/README.md index ded9534..43c06db 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ gotham_restful = "0.0.1" A basic server with only one resource, handling a simple `GET` request, could look like this: ```rust -# /// Our RESTful Resource. #[derive(Resource)] #[rest_resource(read_all)] @@ -54,6 +53,23 @@ fn main() { } ``` +Uploads and Downloads can also be handled, but you need to specify the mime type manually: + +```rust +#[derive(Resource)] +#[rest_resource(create)] +struct ImageResource; + +#[derive(FromBody, RequestBody)] +#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] +struct RawImage(Vec); + +#[rest_create(ImageResource)] +fn create(_state : &mut State, body : RawImage) -> Raw> { + Raw::new(body.0, mime::APPLICATION_OCTET_STREAM) +} +``` + Look at the [example] for more methods and usage with the `openapi` feature. ## License diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index fe8a1f0..9fc36d2 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -22,7 +22,6 @@ A basic server with only one resource, handling a simple `GET` request, could lo # use gotham::{router::builder::*, state::State}; # use gotham_restful::{DrawResources, Resource, Success}; # use serde::{Deserialize, Serialize}; -# /// Our RESTful Resource. #[derive(Resource)] #[rest_resource(read_all)] @@ -55,6 +54,32 @@ fn main() { } ``` +Uploads and Downloads can also be handled, but you need to specify the mime type manually: + +```rust,no_run +# #[macro_use] extern crate gotham_restful_derive; +# use gotham::{router::builder::*, state::State}; +# use gotham_restful::{DrawResources, Raw, Resource, Success}; +# use serde::{Deserialize, Serialize}; +#[derive(Resource)] +#[rest_resource(create)] +struct ImageResource; + +#[derive(FromBody, RequestBody)] +#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] +struct RawImage(Vec); + +#[rest_create(ImageResource)] +fn create(_state : &mut State, body : RawImage) -> Raw> { + Raw::new(body.0, mime::APPLICATION_OCTET_STREAM) +} +# fn main() { +# gotham::start("127.0.0.1:8080", build_simple_router(|route| { +# route.resource::("image"); +# })); +# } +``` + Look at the [example] for more methods and usage with the `openapi` feature. # License diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index e0198bc..9c9cab6 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -38,34 +38,42 @@ impl ResponseBody for T } -/// A type that can be used inside a request body. Implemented for every type that is -/// deserializable with serde. If the `openapi` feature is used, it must also be of type -/// `OpenapiType`. -pub trait RequestBody : ResourceType + Sized +/// This trait must be implemented by every type that can be used as a request body. It allows +/// to create the type from a hyper body chunk and it's content type. +pub trait FromBody : Sized { type Err : Into; - /// Return all types that are supported as content types - fn supported_types() -> Option> - { - None - } - /// Create the request body from a raw body and the content type. fn from_body(body : Chunk, content_type : Mime) -> Result; } -impl RequestBody for T +impl FromBody for T { type Err = serde_json::Error; - fn supported_types() -> Option> - { - Some(vec![APPLICATION_JSON]) - } - fn from_body(body : Chunk, _content_type : Mime) -> Result { serde_json::from_slice(&body) } } + +/// A type that can be used inside a request body. Implemented for every type that is +/// deserializable with serde. If the `openapi` feature is used, it must also be of type +/// `OpenapiType`. +pub trait RequestBody : ResourceType + FromBody +{ + /// Return all types that are supported as content types. + fn supported_types() -> Option> + { + None + } +} + +impl RequestBody for T +{ + fn supported_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } +} diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs new file mode 100644 index 0000000..79af118 --- /dev/null +++ b/gotham_restful_derive/src/from_body.rs @@ -0,0 +1,69 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{ + Fields, + ItemStruct, + parse_macro_input +}; + +pub fn expand_from_body(tokens : TokenStream) -> TokenStream +{ + let krate = super::krate(); + let input = parse_macro_input!(tokens as ItemStruct); + let ident = input.ident; + let generics = input.generics; + + let (were, body) = match input.fields { + Fields::Named(named) => { + let fields = named.named; + if fields.len() == 0 // basically unit + { + (quote!(), quote!(Self{})) + } + else if fields.len() == 1 + { + let field = fields.first().unwrap(); + let field_ident = field.ident.as_ref().unwrap(); + let field_ty = &field.ty; + (quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self { #field_ident: body.into() })) + } + else + { + panic!("FromBody can only be derived for structs with at most one field") + } + }, + Fields::Unnamed(unnamed) => { + let fields = unnamed.unnamed; + if fields.len() == 0 // basically unit + { + (quote!(), quote!(Self{})) + } + else if fields.len() == 1 + { + let field = fields.first().unwrap(); + let field_ty = &field.ty; + (quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self(body.into()))) + } + else + { + panic!("FromBody can only be derived for structs with at most one field") + } + }, + Fields::Unit => (quote!(), quote!(Self{})) + }; + + let output = quote! { + impl #generics #krate::FromBody for #ident #generics + #were + { + type Err = String; + + fn from_body(body : #krate::Chunk, _content_type : #krate::Mime) -> Result + { + let body : &[u8] = &body; + Ok(#body) + } + } + }; + output.into() +} diff --git a/gotham_restful_derive/src/lib.rs b/gotham_restful_derive/src/lib.rs index 2e15a6e..627dfe4 100644 --- a/gotham_restful_derive/src/lib.rs +++ b/gotham_restful_derive/src/lib.rs @@ -4,8 +4,12 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; +mod from_body; +use from_body::expand_from_body; mod method; use method::{expand_method, Method}; +mod request_body; +use request_body::expand_request_body; mod resource; use resource::expand_resource; #[cfg(feature = "openapi")] @@ -16,6 +20,12 @@ fn krate() -> TokenStream2 quote!(::gotham_restful) } +#[proc_macro_derive(FromBody)] +pub fn derive_from_body(tokens : TokenStream) -> TokenStream +{ + expand_from_body(tokens) +} + #[cfg(feature = "openapi")] #[proc_macro_derive(OpenapiType)] pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream @@ -23,6 +33,12 @@ pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream openapi_type::expand(tokens) } +#[proc_macro_derive(RequestBody, attributes(supported_types))] +pub fn derive_request_body(tokens : TokenStream) -> TokenStream +{ + expand_request_body(tokens) +} + #[proc_macro_derive(Resource, attributes(rest_resource))] pub fn derive_resource(tokens : TokenStream) -> TokenStream { diff --git a/gotham_restful_derive/src/request_body.rs b/gotham_restful_derive/src/request_body.rs new file mode 100644 index 0000000..fdcd017 --- /dev/null +++ b/gotham_restful_derive/src/request_body.rs @@ -0,0 +1,91 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream, Result as SynResult}, + punctuated::Punctuated, + token::Comma, + Generics, + Ident, + ItemStruct, + Path, + parenthesized, + parse_macro_input +}; + +struct MimeList(Punctuated); + +impl Parse for MimeList +{ + fn parse(input: ParseStream) -> SynResult + { + let content; + let _paren = parenthesized!(content in input); + let list : Punctuated = Punctuated::parse_separated_nonempty(&content)?; + Ok(Self(list)) + } +} + +#[cfg(not(feature = "openapi"))] +fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream2 +{ + quote!() +} + +#[cfg(feature = "openapi")] +fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream2 +{ + let krate = super::krate(); + + quote! { + impl #generics #krate::OpenapiType for #ident #generics + { + fn schema() -> #krate::OpenapiSchema + { + use #krate::{export::openapi::*, OpenapiSchema}; + + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), + pattern: None, + enumeration: Vec::new() + }))) + } + } + } +} + +pub fn expand_request_body(tokens : TokenStream) -> TokenStream +{ + let krate = super::krate(); + let input = parse_macro_input!(tokens as ItemStruct); + let ident = input.ident; + let generics = input.generics; + + let types : Vec = input.attrs.into_iter().filter(|attr| + attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string()) // TODO wtf + ).flat_map(|attr| { + let m : MimeList = syn::parse2(attr.tokens).expect("unable to parse attributes"); + m.0.into_iter() + }).collect(); + + let types = match types { + ref types if types.is_empty() => quote!(None), + types => quote!(Some(vec![#(#types),*])) + }; + + let impl_openapi_type = impl_openapi_type(&ident, &generics); + + let output = quote! { + impl #generics #krate::RequestBody for #ident #generics + where #ident #generics : #krate::FromBody + { + fn supported_types() -> Option> + { + #types + } + } + + #impl_openapi_type + }; + output.into() +}