From d030fa539f197a7b6f0d615f2ba98ec2adcdaf0a Mon Sep 17 00:00:00 2001 From: msrd0 Date: Sun, 20 Oct 2019 14:49:53 +0000 Subject: [PATCH] Support File up/download --- README.md | 18 +- gotham_restful/src/lib.rs | 36 +++- gotham_restful/src/openapi/router.rs | 78 +++++--- gotham_restful/src/resource.rs | 10 +- gotham_restful/src/result.rs | 220 ++++++++++++++++++---- gotham_restful/src/routing.rs | 162 +++++++++++++--- gotham_restful/src/types.rs | 79 ++++++-- gotham_restful_derive/src/from_body.rs | 59 ++++++ gotham_restful_derive/src/lib.rs | 16 ++ gotham_restful_derive/src/request_body.rs | 91 +++++++++ 10 files changed, 659 insertions(+), 110 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 25530e4..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 @@ -76,7 +101,10 @@ extern crate self as gotham_restful; #[macro_use] extern crate gotham_derive; #[macro_use] extern crate serde; -pub use hyper::StatusCode; +#[doc(no_inline)] +pub use hyper::{Chunk, StatusCode}; +#[doc(no_inline)] +pub use mime::Mime; pub use gotham_restful_derive::*; @@ -114,7 +142,9 @@ pub use resource::{ mod result; pub use result::{ NoContent, + Raw, ResourceResult, + Response, Success }; @@ -124,4 +154,4 @@ pub use routing::{DrawResources, DrawResourceRoutes}; pub use routing::WithOpenapi; mod types; -pub use types::ResourceType; +pub use types::*; diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index eaae925..48565de 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -4,6 +4,7 @@ use crate::{ routing::*, OpenapiSchema, OpenapiType, + RequestBody, ResourceType }; use futures::future::ok; @@ -18,10 +19,10 @@ use gotham::{ use hyper::Body; use indexmap::IndexMap; use log::error; -use mime::{APPLICATION_JSON, TEXT_PLAIN}; +use mime::{Mime, APPLICATION_JSON, TEXT_PLAIN}; use openapiv3::{ Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, - Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody, Response, Responses, Schema, + Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, Server, StatusCode, Type }; use serde::de::DeserializeOwned; @@ -171,15 +172,18 @@ pub trait GetOpenapi fn get_openapi(&mut self, path : &str); } -fn schema_to_content(schema : ReferenceOr) -> IndexMap +fn schema_to_content(types : Vec, schema : ReferenceOr) -> IndexMap { let mut content : IndexMap = IndexMap::new(); - content.insert(APPLICATION_JSON.to_string(), MediaType { - schema: Some(schema), - example: None, - examples: IndexMap::new(), - encoding: IndexMap::new() - }); + for ty in types + { + content.insert(ty.to_string(), MediaType { + schema: Some(schema.clone()), + example: None, + examples: IndexMap::new(), + encoding: IndexMap::new() + }); + } content } @@ -265,12 +269,9 @@ impl<'a> OperationParams<'a> } } -fn new_operation(default_status : hyper::StatusCode, schema : ReferenceOr, params : OperationParams, body_schema : Option>) -> Operation +fn new_operation(default_status : hyper::StatusCode, accepted_types : Option>, schema : ReferenceOr, params : OperationParams, body_schema : Option>, supported_types : Option>) -> Operation { - let content = match default_status.as_u16() { - 204 => IndexMap::new(), - _ => schema_to_content(schema) - }; + let content = schema_to_content(accepted_types.unwrap_or_default(), schema); let mut responses : IndexMap> = IndexMap::new(); responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response { @@ -280,9 +281,9 @@ fn new_operation(default_status : hyper::StatusCode, schema : ReferenceOr() @@ -359,7 +360,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(Res::default_status(), schema, OperationParams::from_path_params(vec!["id"]), None)); + item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None)); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).read::() @@ -367,7 +368,7 @@ macro_rules! implOpenapiRouter { fn search(&mut self) where - Query : ResourceType + QueryStringExtractor + Send + Sync + 'static, + Query : ResourceType + DeserializeOwned + QueryStringExtractor + Send + Sync + 'static, Res : ResourceResult, Handler : ResourceSearch { @@ -375,7 +376,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/search", &self.1); let mut item = (self.0).1.remove_path(&self.1); - item.get = Some(new_operation(Res::default_status(), schema, OperationParams::from_query_params(Query::schema()), None)); + item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_query_params(Query::schema()), None, None)); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).search::() @@ -383,7 +384,7 @@ macro_rules! implOpenapiRouter { fn create(&mut self) where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceCreate { @@ -392,7 +393,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.post = Some(new_operation(Res::default_status(), schema, OperationParams::default(), Some(body_schema))); + item.post = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Body::supported_types())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).create::() @@ -400,7 +401,7 @@ macro_rules! implOpenapiRouter { fn update_all(&mut self) where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdateAll { @@ -409,7 +410,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(Res::default_status(), schema, OperationParams::default(), Some(body_schema))); + item.put = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Body::supported_types())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).update_all::() @@ -418,7 +419,7 @@ macro_rules! implOpenapiRouter { fn update(&mut self) where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdate { @@ -427,7 +428,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(Res::default_status(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema))); + item.put = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema), Body::supported_types())); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).update::() @@ -442,7 +443,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(new_operation(Res::default_status(), schema, OperationParams::default(), None)); + item.delete = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), None, None)); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).delete_all::() @@ -458,7 +459,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(new_operation(Res::default_status(), schema, OperationParams::from_path_params(vec!["id"]), None)); + item.delete = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None)); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1.to_string()).delete::() @@ -475,6 +476,7 @@ implOpenapiRouter!(ScopeBuilder); #[cfg(test)] mod test { + use crate::ResourceResult; use super::*; #[derive(OpenapiType)] @@ -520,4 +522,24 @@ mod test let json = serde_json::to_string(¶ms).unwrap(); assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}},{{"in":"query","name":"id","required":true,"schema":{{"type":"integer"}},"style":"form"}}]"#, name)); } + + #[test] + fn no_content_schema_to_content() + { + let types = NoContent::accepted_types(); + let schema = ::schema(); + let content = schema_to_content(types.unwrap_or_default(), Item(schema.into_schema())); + assert!(content.is_empty()); + } + + #[test] + fn raw_schema_to_content() + { + let types = Raw::<&str>::accepted_types(); + let schema = as OpenapiType>::schema(); + let content = schema_to_content(types.unwrap_or_default(), Item(schema.into_schema())); + assert_eq!(content.len(), 1); + let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); + assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#); + } } diff --git a/gotham_restful/src/resource.rs b/gotham_restful/src/resource.rs index 8b04159..171f3f9 100644 --- a/gotham_restful/src/resource.rs +++ b/gotham_restful/src/resource.rs @@ -1,4 +1,4 @@ -use crate::{DrawResourceRoutes, ResourceResult, ResourceType}; +use crate::{DrawResourceRoutes, RequestBody, ResourceResult, ResourceType}; use gotham::{ router::response::extender::StaticResponseExtender, state::{State, StateData} @@ -35,25 +35,25 @@ where /// Handle a GET request on the Resource with additional search parameters. pub trait ResourceSearch where - Query : ResourceType + StateData + StaticResponseExtender + Query : ResourceType + DeserializeOwned + StateData + StaticResponseExtender { fn search(state : &mut State, query : Query) -> R; } /// Handle a POST request on the Resource root. -pub trait ResourceCreate +pub trait ResourceCreate { fn create(state : &mut State, body : Body) -> R; } /// Handle a PUT request on the Resource root. -pub trait ResourceUpdateAll +pub trait ResourceUpdateAll { fn update_all(state : &mut State, body : Body) -> R; } /// Handle a PUT request on the Resource with an id. -pub trait ResourceUpdate +pub trait ResourceUpdate where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static { diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index 86c10be..c5e67b0 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -1,14 +1,76 @@ -use crate::{ResourceType, StatusCode}; +use crate::{ResponseBody, StatusCode}; #[cfg(feature = "openapi")] use crate::{OpenapiSchema, OpenapiType}; +use hyper::Body; +use mime::{Mime, APPLICATION_JSON, STAR_STAR}; +#[cfg(feature = "openapi")] +use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; use serde::Serialize; use serde_json::error::Error as SerdeJsonError; use std::error::Error; +/// A response, used to create the final gotham response from. +pub struct Response +{ + pub status : StatusCode, + pub body : Body, + pub mime : Option +} + +impl Response +{ + /// Create a new `Response` from raw data. + pub fn new>(status : StatusCode, body : B, mime : Option) -> Self + { + Self { + status, + body: body.into(), + mime + } + } + + /// Create a `Response` with mime type json from already serialized data. + pub fn json>(status : StatusCode, body : B) -> Self + { + Self { + status, + body: body.into(), + mime: Some(APPLICATION_JSON) + } + } + + /// Create a _204 No Content_ `Response`. + pub fn no_content() -> Self + { + Self { + status: StatusCode::NO_CONTENT, + body: Body::empty(), + mime: None + } + } + + #[cfg(test)] + fn full_body(self) -> Vec + { + use futures::{future::Future, stream::Stream}; + + let bytes : &[u8] = &self.body.concat2().wait().unwrap().into_bytes(); + bytes.to_vec() + } +} + /// A trait provided to convert a resource's result to json. pub trait ResourceResult { - fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>; + /// Turn this into a response that can be returned to the browser. This api will likely + /// change in the future. + fn into_response(self) -> Result; + + /// Return a list of supported mime types. + fn accepted_types() -> Option> + { + None + } #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema; @@ -48,19 +110,24 @@ impl From for ResourceError } } -impl ResourceResult for Result +impl ResourceResult for Result { - fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn into_response(self) -> Result { Ok(match self { - Ok(r) => (StatusCode::OK, serde_json::to_string(r)?), + Ok(r) => Response::json(StatusCode::OK, serde_json::to_string(&r)?), Err(e) => { let err : ResourceError = e.into(); - (StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?) + Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?) } }) } + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema { @@ -103,11 +170,16 @@ impl From for Success } } -impl ResourceResult for Success +impl ResourceResult for Success { - fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn into_response(self) -> Result { - Ok((StatusCode::OK, serde_json::to_string(&self.0)?)) + Ok(Response::json(StatusCode::OK, serde_json::to_string(&self.0)?)) + } + + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) } #[cfg(feature = "openapi")] @@ -150,9 +222,9 @@ impl From<()> for NoContent impl ResourceResult for NoContent { /// This will always be a _204 No Content_ together with an empty string. - fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn into_response(self) -> Result { - Ok((Self::default_status(), "".to_string())) + Ok(Response::no_content()) } /// Returns the schema of the `()` type. @@ -172,15 +244,15 @@ impl ResourceResult for NoContent impl ResourceResult for Result { - fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn into_response(self) -> Result { - Ok(match self { - Ok(_) => (Self::default_status(), "".to_string()), + match self { + Ok(nc) => nc.into_response(), Err(e) => { let err : ResourceError = e.into(); - (StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?) + Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) } - }) + } } #[cfg(feature = "openapi")] @@ -196,10 +268,76 @@ impl ResourceResult for Result } } +pub struct Raw +{ + pub raw : T, + pub mime : Mime +} + +impl Raw +{ + pub fn new(raw : T, mime : Mime) -> Self + { + Self { raw, mime } + } +} + +impl> ResourceResult for Raw +{ + fn into_response(self) -> Result + { + Ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))) + } + + fn accepted_types() -> Option> + { + Some(vec![STAR_STAR]) + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), + pattern: None, + enumeration: Vec::new() + }))) + } +} + +impl ResourceResult for Result, E> +where + Raw : ResourceResult +{ + fn into_response(self) -> Result + { + match self { + Ok(raw) => raw.into_response(), + Err(e) => { + let err : ResourceError = e.into(); + Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) + } + } + } + + fn accepted_types() -> Option> + { + as ResourceResult>::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + as ResourceResult>::schema() + } +} + + #[cfg(test)] mod test { use super::*; + use mime::TEXT_PLAIN; use thiserror::Error; #[derive(Debug, Default, Deserialize, Serialize)] @@ -217,44 +355,60 @@ mod test fn resource_result_ok() { let ok : Result = Ok(Msg::default()); - let (status, json) = ok.to_json().expect("didn't expect error response"); - assert_eq!(status, StatusCode::OK); - assert_eq!(json, r#"{"msg":""}"#); + let res = ok.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body(), r#"{"msg":""}"#.as_bytes()); } #[test] fn resource_result_err() { let err : Result = Err(MsgError::default()); - let (status, json) = err.to_json().expect("didn't expect error response"); - assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(json, format!(r#"{{"error":true,"message":"{}"}}"#, err.unwrap_err())); + let res = err.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()); } #[test] fn success_always_successfull() { let success : Success = Msg::default().into(); - let (status, json) = success.to_json().expect("didn't expect error response"); - assert_eq!(status, StatusCode::OK); - assert_eq!(json, r#"{"msg":""}"#); + let res = success.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body(), r#"{"msg":""}"#.as_bytes()); } #[test] - fn no_content_has_empty_json() + fn no_content_has_empty_response() { let no_content = NoContent::default(); - let (status, json) = no_content.to_json().expect("didn't expect error response"); - assert_eq!(status, StatusCode::NO_CONTENT); - assert_eq!(json, ""); + let res = no_content.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.mime, None); + assert_eq!(res.full_body(), &[] as &[u8]); } #[test] fn no_content_result() { - let no_content = NoContent::default(); - let res_def = no_content.to_json().expect("didn't expect error response"); - let res_err = Result::::Ok(no_content).to_json().expect("didn't expect error response"); - assert_eq!(res_def, res_err); + let no_content : Result = Ok(NoContent::default()); + let res = no_content.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.mime, None); + assert_eq!(res.full_body(), &[] as &[u8]); + } + + #[test] + fn raw_response() + { + let msg = "Test"; + let raw = Raw::new(msg, TEXT_PLAIN); + let res = raw.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(TEXT_PLAIN)); + assert_eq!(res.full_body(), msg.as_bytes()); } } diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 8907541..5a16281 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -1,6 +1,7 @@ use crate::{ resource::*, - result::{ResourceError, ResourceResult}, + result::{ResourceError, ResourceResult, Response}, + RequestBody, ResourceType, StatusCode }; @@ -14,13 +15,26 @@ use futures::{ use gotham::{ extractor::QueryStringExtractor, handler::{HandlerFuture, IntoHandlerError}, - helpers::http::response::create_response, + helpers::http::response::{create_empty_response, create_response}, pipeline::chain::PipelineHandleChain, - router::builder::*, + router::{ + builder::*, + non_match::RouteNonMatch, + route::matcher::{ + content_type::ContentTypeHeaderRouteMatcher, + AcceptHeaderRouteMatcher, + RouteMatcher + } + }, state::{FromState, State} }; -use hyper::Body; -use mime::APPLICATION_JSON; +use hyper::{ + header::CONTENT_TYPE, + Body, + HeaderMap, + Method +}; +use mime::{Mime, APPLICATION_JSON}; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -69,26 +83,26 @@ pub trait DrawResourceRoutes fn search(&mut self) where - Query : ResourceType + QueryStringExtractor + Send + Sync + 'static, + Query : ResourceType + DeserializeOwned + QueryStringExtractor + Send + Sync + 'static, Res : ResourceResult, Handler : ResourceSearch; fn create(&mut self) where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceCreate; fn update_all(&mut self) where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdateAll; fn update(&mut self) where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdate; @@ -104,16 +118,30 @@ pub trait DrawResourceRoutes Handler : ResourceDelete; } +fn response_from(res : Response, state : &State) -> hyper::Response +{ + let mut r = create_empty_response(state, res.status); + if let Some(mime) = res.mime + { + r.headers_mut().insert(CONTENT_TYPE, mime.as_ref().parse().unwrap()); + } + if Method::borrow_from(state) != Method::HEAD + { + *r.body_mut() = res.body; + } + r +} + fn to_handler_future(mut state : State, get_result : F) -> Box where F : FnOnce(&mut State) -> R, R : ResourceResult { - let res = get_result(&mut state).to_json(); + let res = get_result(&mut state).into_response(); match res { - Ok((status, body)) => { - let res = create_response(&state, status, APPLICATION_JSON, body); - Box::new(ok((state, res))) + Ok(res) => { + let r = response_from(res, &state); + Box::new(ok((state, r))) }, Err(e) => Box::new(err((state, e.into_handler_error()))) } @@ -121,7 +149,7 @@ where fn handle_with_body(mut state : State, get_result : F) -> Box where - Body : DeserializeOwned, + Body : RequestBody, F : FnOnce(&mut State, Body) -> R + Send + 'static, R : ResourceResult { @@ -133,8 +161,16 @@ where Ok(body) => body, Err(e) => return err((state, e.into_handler_error())) }; + + let content_type : Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) { + Some(content_type) => content_type.to_str().unwrap().parse().unwrap(), + None => { + let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE); + return ok((state, res)) + } + }; - let body = match serde_json::from_slice(&body) { + let body = match Body::from_body(body, content_type) { Ok(body) => body, Err(e) => return { let error : ResourceError = e.into(); @@ -148,11 +184,11 @@ where } }; - let res = get_result(&mut state, body).to_json(); + let res = get_result(&mut state, body).into_response(); match res { - Ok((status, body)) => { - let res = create_response(&state, status, APPLICATION_JSON, body); - ok((state, res)) + Ok(res) => { + let r = response_from(res, &state); + ok((state, r)) }, Err(e) => err((state, e.into_handler_error())) } @@ -195,7 +231,7 @@ where fn create_handler(state : State) -> Box where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceCreate { @@ -204,7 +240,7 @@ where fn update_all_handler(state : State) -> Box where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdateAll { @@ -214,7 +250,7 @@ where fn update_handler(state : State) -> Box where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdate { @@ -246,6 +282,60 @@ where to_handler_future(state, |state| Handler::delete(state, id)) } +#[derive(Clone)] +struct MaybeMatchAcceptHeader +{ + matcher : Option +} + +impl RouteMatcher for MaybeMatchAcceptHeader +{ + fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> + { + match &self.matcher { + Some(matcher) => matcher.is_match(state), + None => Ok(()) + } + } +} + +impl From>> for MaybeMatchAcceptHeader +{ + fn from(types : Option>) -> Self + { + Self { + matcher: types.map(AcceptHeaderRouteMatcher::new) + } + } +} + +#[derive(Clone)] +struct MaybeMatchContentTypeHeader +{ + matcher : Option +} + +impl RouteMatcher for MaybeMatchContentTypeHeader +{ + fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> + { + match &self.matcher { + Some(matcher) => matcher.is_match(state), + None => Ok(()) + } + } +} + +impl From>> for MaybeMatchContentTypeHeader +{ + fn from(types : Option>) -> Self + { + Self { + matcher: types.map(ContentTypeHeaderRouteMatcher::new) + } + } +} + macro_rules! implDrawResourceRoutes { ($implType:ident) => { @@ -289,7 +379,9 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceReadAll { + let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); self.0.get(&self.1) + .extend_route_matcher(matcher) .to(|state| read_all_handler::(state)); } @@ -299,7 +391,9 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceRead { + let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); self.0.get(&format!("{}/:id", self.1)) + .extend_route_matcher(matcher) .with_path_extractor::>() .to(|state| read_handler::(state)); } @@ -310,39 +404,53 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceSearch { + let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); self.0.get(&format!("{}/search", self.1)) + .extend_route_matcher(matcher) .with_query_string_extractor::() .to(|state| search_handler::(state)); } fn create(&mut self) where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceCreate { + let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); + let content_matcher : MaybeMatchContentTypeHeader = Body::supported_types().into(); self.0.post(&self.1) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) .to(|state| create_handler::(state)); } fn update_all(&mut self) where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdateAll { + let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); + let content_matcher : MaybeMatchContentTypeHeader = Body::supported_types().into(); self.0.put(&self.1) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) .to(|state| update_all_handler::(state)); } fn update(&mut self) where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdate { + let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); + let content_matcher : MaybeMatchContentTypeHeader = Body::supported_types().into(); self.0.put(&format!("{}/:id", self.1)) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) .with_path_extractor::>() .to(|state| update_handler::(state)); } @@ -352,7 +460,9 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceDeleteAll { + let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); self.0.delete(&self.1) + .extend_route_matcher(matcher) .to(|state| delete_all_handler::(state)); } @@ -362,7 +472,9 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceDelete { + let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); self.0.delete(&format!("{}/:id", self.1)) + .extend_route_matcher(matcher) .with_path_extractor::>() .to(|state| delete_handler::(state)); } diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index f354e92..9c9cab6 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -1,30 +1,79 @@ #[cfg(feature = "openapi")] -use crate::OpenapiType; +use crate::{OpenapiType, result::ResourceError}; +use hyper::Chunk; +use mime::{Mime, APPLICATION_JSON}; use serde::{de::DeserializeOwned, Serialize}; -/// A type that can be used inside a request or response body. Implemented for every type -/// that is serializable with serde, however, it is recommended to use the rest_struct! -/// macro to create one. #[cfg(not(feature = "openapi"))] -pub trait ResourceType : DeserializeOwned + Serialize +pub trait ResourceType { } #[cfg(not(feature = "openapi"))] -impl ResourceType for T -{ -} - -/// A type that can be used inside a request or response body. Implemented for every type -/// that is serializable with serde, however, it is recommended to use the rest_struct! -/// macro to create one. -#[cfg(feature = "openapi")] -pub trait ResourceType : OpenapiType + DeserializeOwned + Serialize +impl ResourceType for T { } #[cfg(feature = "openapi")] -impl ResourceType for T +pub trait ResourceType : OpenapiType { } + +#[cfg(feature = "openapi")] +impl ResourceType for T +{ +} + + +/// A type that can be used inside a response body. Implemented for every type that is +/// serializable with serde. If the `openapi` feature is used, it must also be of type +/// `OpenapiType`. +pub trait ResponseBody : ResourceType + Serialize +{ +} + +impl ResponseBody for T +{ +} + + +/// 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; + + /// Create the request body from a raw body and the content type. + fn from_body(body : Chunk, content_type : Mime) -> Result; +} + +impl FromBody for T +{ + type Err = serde_json::Error; + + 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..633998f --- /dev/null +++ b/gotham_restful_derive/src/from_body.rs @@ -0,0 +1,59 @@ +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; + match fields.len() { + 0 => (quote!(), quote!(Self{})), + 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() })) + }, + _ => panic!("FromBody can only be derived for structs with at most one field") + } + }, + Fields::Unnamed(unnamed) => { + let fields = unnamed.unnamed; + match fields.len() { + 0 => (quote!(), quote!(Self{})), + 1 => { + let field = fields.first().unwrap(); + let field_ty = &field.ty; + (quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self(body.into()))) + }, + _ => 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() +}