From bb5f58e97d5d9748ea083ed3244b8bef6a80be16 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 19 Oct 2019 20:06:30 +0200 Subject: [PATCH 01/11] support accept header mime types --- gotham_restful/src/result.rs | 41 +++++++++++++++++++------- gotham_restful/src/routing.rs | 55 ++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index 86c10be..6774473 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -1,6 +1,7 @@ use crate::{ResourceType, StatusCode}; #[cfg(feature = "openapi")] use crate::{OpenapiSchema, OpenapiType}; +use mime::{Mime, APPLICATION_JSON}; use serde::Serialize; use serde_json::error::Error as SerdeJsonError; use std::error::Error; @@ -8,7 +9,15 @@ use std::error::Error; /// 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 to_response(&self) -> Result<(StatusCode, String), SerdeJsonError>; + + /// Return a list of supported mime types. + fn accepted_types() -> Option> + { + None + } #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema; @@ -50,7 +59,7 @@ impl From for ResourceError impl ResourceResult for Result { - fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn to_response(&self) -> Result<(StatusCode, String), SerdeJsonError> { Ok(match self { Ok(r) => (StatusCode::OK, serde_json::to_string(r)?), @@ -61,6 +70,11 @@ impl ResourceResult for Result }) } + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema { @@ -105,11 +119,16 @@ impl From for Success impl ResourceResult for Success { - fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn to_response(&self) -> Result<(StatusCode, String), SerdeJsonError> { Ok((StatusCode::OK, serde_json::to_string(&self.0)?)) } + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema { @@ -150,7 +169,7 @@ 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 to_response(&self) -> Result<(StatusCode, String), SerdeJsonError> { Ok((Self::default_status(), "".to_string())) } @@ -172,7 +191,7 @@ impl ResourceResult for NoContent impl ResourceResult for Result { - fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn to_response(&self) -> Result<(StatusCode, String), SerdeJsonError> { Ok(match self { Ok(_) => (Self::default_status(), "".to_string()), @@ -217,7 +236,7 @@ mod test fn resource_result_ok() { let ok : Result = Ok(Msg::default()); - let (status, json) = ok.to_json().expect("didn't expect error response"); + let (status, json) = ok.to_response().expect("didn't expect error response"); assert_eq!(status, StatusCode::OK); assert_eq!(json, r#"{"msg":""}"#); } @@ -226,7 +245,7 @@ mod test fn resource_result_err() { let err : Result = Err(MsgError::default()); - let (status, json) = err.to_json().expect("didn't expect error response"); + let (status, json) = err.to_response().expect("didn't expect error response"); assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(json, format!(r#"{{"error":true,"message":"{}"}}"#, err.unwrap_err())); } @@ -235,7 +254,7 @@ mod test fn success_always_successfull() { let success : Success = Msg::default().into(); - let (status, json) = success.to_json().expect("didn't expect error response"); + let (status, json) = success.to_response().expect("didn't expect error response"); assert_eq!(status, StatusCode::OK); assert_eq!(json, r#"{"msg":""}"#); } @@ -244,7 +263,7 @@ mod test fn no_content_has_empty_json() { let no_content = NoContent::default(); - let (status, json) = no_content.to_json().expect("didn't expect error response"); + let (status, json) = no_content.to_response().expect("didn't expect error response"); assert_eq!(status, StatusCode::NO_CONTENT); assert_eq!(json, ""); } @@ -253,8 +272,8 @@ mod 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"); + let res_def = no_content.to_response().expect("didn't expect error response"); + let res_err = Result::::Ok(no_content).to_response().expect("didn't expect error response"); assert_eq!(res_def, res_err); } } diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 8907541..7e60201 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -16,11 +16,15 @@ use gotham::{ handler::{HandlerFuture, IntoHandlerError}, helpers::http::response::create_response, pipeline::chain::PipelineHandleChain, - router::builder::*, + router::{ + builder::*, + non_match::RouteNonMatch, + route::matcher::{AcceptHeaderRouteMatcher, RouteMatcher} + }, state::{FromState, State} }; use hyper::Body; -use mime::APPLICATION_JSON; +use mime::{Mime, APPLICATION_JSON}; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -109,7 +113,7 @@ where F : FnOnce(&mut State) -> R, R : ResourceResult { - let res = get_result(&mut state).to_json(); + let res = get_result(&mut state).to_response(); match res { Ok((status, body)) => { let res = create_response(&state, status, APPLICATION_JSON, body); @@ -148,7 +152,7 @@ where } }; - let res = get_result(&mut state, body).to_json(); + let res = get_result(&mut state, body).to_response(); match res { Ok((status, body)) => { let res = create_response(&state, status, APPLICATION_JSON, body); @@ -246,6 +250,33 @@ 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) + } + } +} + macro_rules! implDrawResourceRoutes { ($implType:ident) => { @@ -289,7 +320,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 +332,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,7 +345,9 @@ 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)); } @@ -321,7 +358,9 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceCreate { + let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); self.0.post(&self.1) + .extend_route_matcher(matcher) .to(|state| create_handler::(state)); } @@ -331,7 +370,9 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceUpdateAll { + let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); self.0.put(&self.1) + .extend_route_matcher(matcher) .to(|state| update_all_handler::(state)); } @@ -342,7 +383,9 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceUpdate { + let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); self.0.put(&format!("{}/:id", self.1)) + .extend_route_matcher(matcher) .with_path_extractor::>() .to(|state| update_handler::(state)); } @@ -352,7 +395,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 +407,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)); } From 656595711ca2940f81f980d80de69346605532c2 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 19 Oct 2019 20:23:25 +0200 Subject: [PATCH 02/11] response can have a different mime type than json --- gotham_restful/src/result.rs | 93 ++++++++++++++++++++++++----------- gotham_restful/src/routing.rs | 36 ++++++++++---- 2 files changed, 90 insertions(+), 39 deletions(-) diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index 6774473..6158147 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -6,12 +6,40 @@ use serde::Serialize; use serde_json::error::Error as SerdeJsonError; use std::error::Error; +pub struct Response +{ + pub status : StatusCode, + pub body : String, + pub mime : Option +} + +impl Response +{ + pub fn json(status : StatusCode, body : String) -> Self + { + Self { + status, + body, + mime: Some(APPLICATION_JSON) + } + } + + pub fn no_content() -> Self + { + Self { + status: StatusCode::NO_CONTENT, + body: String::new(), + mime: None + } + } +} + /// A trait provided to convert a resource's result to json. pub trait ResourceResult { /// Turn this into a response that can be returned to the browser. This api will likely /// change in the future. - fn to_response(&self) -> Result<(StatusCode, String), SerdeJsonError>; + fn to_response(&self) -> Result; /// Return a list of supported mime types. fn accepted_types() -> Option> @@ -59,13 +87,13 @@ impl From for ResourceError impl ResourceResult for Result { - fn to_response(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn to_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)?) } }) } @@ -119,9 +147,9 @@ impl From for Success impl ResourceResult for Success { - fn to_response(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn to_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> @@ -169,9 +197,9 @@ impl From<()> for NoContent impl ResourceResult for NoContent { /// This will always be a _204 No Content_ together with an empty string. - fn to_response(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn to_response(&self) -> Result { - Ok((Self::default_status(), "".to_string())) + Ok(Response::no_content()) } /// Returns the schema of the `()` type. @@ -191,15 +219,15 @@ impl ResourceResult for NoContent impl ResourceResult for Result { - fn to_response(&self) -> Result<(StatusCode, String), SerdeJsonError> + fn to_response(&self) -> Result { - Ok(match self { - Ok(_) => (Self::default_status(), "".to_string()), + match self { + Ok(nc) => nc.to_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")] @@ -236,44 +264,49 @@ mod test fn resource_result_ok() { let ok : Result = Ok(Msg::default()); - let (status, json) = ok.to_response().expect("didn't expect error response"); - assert_eq!(status, StatusCode::OK); - assert_eq!(json, r#"{"msg":""}"#); + let res = ok.to_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.body, r#"{"msg":""}"#); + assert_eq!(res.mime, Some(APPLICATION_JSON)); } #[test] fn resource_result_err() { let err : Result = Err(MsgError::default()); - let (status, json) = err.to_response().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.to_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(res.body, format!(r#"{{"error":true,"message":"{}"}}"#, err.unwrap_err())); + assert_eq!(res.mime, Some(APPLICATION_JSON)); } #[test] fn success_always_successfull() { let success : Success = Msg::default().into(); - let (status, json) = success.to_response().expect("didn't expect error response"); - assert_eq!(status, StatusCode::OK); - assert_eq!(json, r#"{"msg":""}"#); + let res = success.to_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.body, r#"{"msg":""}"#); + assert_eq!(res.mime, Some(APPLICATION_JSON)); } #[test] - fn no_content_has_empty_json() + fn no_content_has_empty_response() { let no_content = NoContent::default(); - let (status, json) = no_content.to_response().expect("didn't expect error response"); - assert_eq!(status, StatusCode::NO_CONTENT); - assert_eq!(json, ""); + let res = no_content.to_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.body, ""); + assert_eq!(res.mime, None); } #[test] fn no_content_result() { - let no_content = NoContent::default(); - let res_def = no_content.to_response().expect("didn't expect error response"); - let res_err = Result::::Ok(no_content).to_response().expect("didn't expect error response"); - assert_eq!(res_def, res_err); + let no_content : Result = Ok(NoContent::default()); + let res = no_content.to_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.body, ""); + assert_eq!(res.mime, None); } } diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 7e60201..400dfa8 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -1,6 +1,6 @@ use crate::{ resource::*, - result::{ResourceError, ResourceResult}, + result::{ResourceError, ResourceResult, Response}, ResourceType, StatusCode }; @@ -14,7 +14,7 @@ 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::*, @@ -23,7 +23,11 @@ use gotham::{ }, state::{FromState, State} }; -use hyper::Body; +use hyper::{ + header::CONTENT_TYPE, + Body, + Method +}; use mime::{Mime, APPLICATION_JSON}; use serde::de::DeserializeOwned; use std::panic::RefUnwindSafe; @@ -108,6 +112,20 @@ 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.into(); + } + r +} + fn to_handler_future(mut state : State, get_result : F) -> Box where F : FnOnce(&mut State) -> R, @@ -115,9 +133,9 @@ where { let res = get_result(&mut state).to_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()))) } @@ -154,9 +172,9 @@ where let res = get_result(&mut state, body).to_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())) } From 9e9b8869c9acd9d69da82e662c7f1f121e1eb790 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 19 Oct 2019 20:45:31 +0200 Subject: [PATCH 03/11] add raw response --- gotham_restful/src/lib.rs | 2 + gotham_restful/src/result.rs | 78 ++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 25530e4..9fd913c 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -114,7 +114,9 @@ pub use resource::{ mod result; pub use result::{ NoContent, + Raw, ResourceResult, + Response, Success }; diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index 6158147..610b8f3 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -2,6 +2,8 @@ use crate::{ResourceType, StatusCode}; #[cfg(feature = "openapi")] use crate::{OpenapiSchema, OpenapiType}; use mime::{Mime, APPLICATION_JSON}; +#[cfg(feature = "openapi")] +use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; use serde::Serialize; use serde_json::error::Error as SerdeJsonError; use std::error::Error; @@ -15,6 +17,15 @@ pub struct Response impl Response { + pub fn new(status : StatusCode, body : String, mime : Option) -> Self + { + Self { + status, + body, + mime + } + } + pub fn json(status : StatusCode, body : String) -> Self { Self { @@ -243,10 +254,66 @@ 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 to_response(&self) -> Result + { + Ok(Response::new(StatusCode::OK, self.raw.to_string(), Some(self.mime.clone()))) + } + + #[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 to_response(&self) -> Result + { + match self { + Ok(raw) => raw.to_response(), + Err(e) => { + let err : ResourceError = e.into(); + Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) + } + } + } + + #[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)] @@ -309,4 +376,15 @@ mod test assert_eq!(res.body, ""); assert_eq!(res.mime, None); } + + #[test] + fn raw_response() + { + let msg = "Test"; + let raw = Raw::new(msg, TEXT_PLAIN); + let res = raw.to_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.body, msg); + assert_eq!(res.mime, Some(TEXT_PLAIN)); + } } From 3a03dc60faac5c67372be0ebc48d614481fa4ffe Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 20 Oct 2019 00:36:00 +0200 Subject: [PATCH 04/11] use Into for Response --- gotham_restful/src/result.rs | 74 +++++++++++++++++++++-------------- gotham_restful/src/routing.rs | 4 +- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index 610b8f3..6fb889b 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -1,6 +1,7 @@ use crate::{ResourceType, StatusCode}; #[cfg(feature = "openapi")] use crate::{OpenapiSchema, OpenapiType}; +use hyper::Body; use mime::{Mime, APPLICATION_JSON}; #[cfg(feature = "openapi")] use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; @@ -8,41 +9,54 @@ 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 : String, + pub body : Body, pub mime : Option } impl Response { - pub fn new(status : StatusCode, body : String, mime : Option) -> Self + /// Create a new `Response` from raw data. + pub fn new>(status : StatusCode, body : B, mime : Option) -> Self { Self { status, - body, + body: body.into(), mime } } - pub fn json(status : StatusCode, body : String) -> Self + /// Create a `Response` with mime type json from already serialized data. + pub fn json>(status : StatusCode, body : B) -> Self { Self { status, - body, + body: body.into(), mime: Some(APPLICATION_JSON) } } + /// Create a _204 No Content_ `Response`. pub fn no_content() -> Self { Self { status: StatusCode::NO_CONTENT, - body: String::new(), + 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. @@ -50,7 +64,7 @@ pub trait ResourceResult { /// Turn this into a response that can be returned to the browser. This api will likely /// change in the future. - fn to_response(&self) -> Result; + fn into_response(self) -> Result; /// Return a list of supported mime types. fn accepted_types() -> Option> @@ -98,10 +112,10 @@ impl From for ResourceError impl ResourceResult for Result { - fn to_response(&self) -> Result + fn into_response(self) -> Result { Ok(match self { - Ok(r) => Response::json(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(); Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?) @@ -158,7 +172,7 @@ impl From for Success impl ResourceResult for Success { - fn to_response(&self) -> Result + fn into_response(self) -> Result { Ok(Response::json(StatusCode::OK, serde_json::to_string(&self.0)?)) } @@ -208,7 +222,7 @@ impl From<()> for NoContent impl ResourceResult for NoContent { /// This will always be a _204 No Content_ together with an empty string. - fn to_response(&self) -> Result + fn into_response(self) -> Result { Ok(Response::no_content()) } @@ -230,10 +244,10 @@ impl ResourceResult for NoContent impl ResourceResult for Result { - fn to_response(&self) -> Result + fn into_response(self) -> Result { match self { - Ok(nc) => nc.to_response(), + Ok(nc) => nc.into_response(), Err(e) => { let err : ResourceError = e.into(); Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) @@ -268,11 +282,11 @@ impl Raw } } -impl ResourceResult for Raw +impl> ResourceResult for Raw { - fn to_response(&self) -> Result + fn into_response(self) -> Result { - Ok(Response::new(StatusCode::OK, self.raw.to_string(), Some(self.mime.clone()))) + Ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))) } #[cfg(feature = "openapi")] @@ -290,10 +304,10 @@ impl ResourceResult for Result, E> where Raw : ResourceResult { - fn to_response(&self) -> Result + fn into_response(self) -> Result { match self { - Ok(raw) => raw.to_response(), + Ok(raw) => raw.into_response(), Err(e) => { let err : ResourceError = e.into(); Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) @@ -331,50 +345,50 @@ mod test fn resource_result_ok() { let ok : Result = Ok(Msg::default()); - let res = ok.to_response().expect("didn't expect error response"); + let res = ok.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); - assert_eq!(res.body, r#"{"msg":""}"#); 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 res = err.to_response().expect("didn't expect error response"); + let res = err.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(res.body, format!(r#"{{"error":true,"message":"{}"}}"#, err.unwrap_err())); 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 res = success.to_response().expect("didn't expect error response"); + let res = success.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); - assert_eq!(res.body, r#"{"msg":""}"#); assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body(), r#"{"msg":""}"#.as_bytes()); } #[test] fn no_content_has_empty_response() { let no_content = NoContent::default(); - let res = no_content.to_response().expect("didn't expect error response"); + let res = no_content.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::NO_CONTENT); - assert_eq!(res.body, ""); assert_eq!(res.mime, None); + assert_eq!(res.full_body(), &[] as &[u8]); } #[test] fn no_content_result() { let no_content : Result = Ok(NoContent::default()); - let res = no_content.to_response().expect("didn't expect error response"); + let res = no_content.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::NO_CONTENT); - assert_eq!(res.body, ""); assert_eq!(res.mime, None); + assert_eq!(res.full_body(), &[] as &[u8]); } #[test] @@ -382,9 +396,9 @@ mod test { let msg = "Test"; let raw = Raw::new(msg, TEXT_PLAIN); - let res = raw.to_response().expect("didn't expect error response"); + let res = raw.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); - assert_eq!(res.body, msg); 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 400dfa8..437c282 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -131,7 +131,7 @@ where F : FnOnce(&mut State) -> R, R : ResourceResult { - let res = get_result(&mut state).to_response(); + let res = get_result(&mut state).into_response(); match res { Ok(res) => { let r = response_from(res, &state); @@ -170,7 +170,7 @@ where } }; - let res = get_result(&mut state, body).to_response(); + let res = get_result(&mut state, body).into_response(); match res { Ok(res) => { let r = response_from(res, &state); From 57e4f3685279df0b91a0261afbb5b2bf3a942398 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 20 Oct 2019 00:54:27 +0200 Subject: [PATCH 05/11] separate RequestBody and ResponseBody traits from ResourceType --- gotham_restful/src/lib.rs | 2 +- gotham_restful/src/openapi/router.rs | 13 ++++---- gotham_restful/src/resource.rs | 10 +++--- gotham_restful/src/result.rs | 6 ++-- gotham_restful/src/routing.rs | 23 +++++++------- gotham_restful/src/types.rs | 46 +++++++++++++++++++--------- 6 files changed, 60 insertions(+), 40 deletions(-) diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 9fd913c..727eca9 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -126,4 +126,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..ae32444 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; @@ -21,7 +22,7 @@ use log::error; use 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; @@ -280,7 +281,7 @@ fn new_operation(default_status : hyper::StatusCode, schema : ReferenceOr(&mut self) where - Query : ResourceType + QueryStringExtractor + Send + Sync + 'static, + Query : ResourceType + DeserializeOwned + QueryStringExtractor + Send + Sync + 'static, Res : ResourceResult, Handler : ResourceSearch { @@ -383,7 +384,7 @@ macro_rules! implOpenapiRouter { fn create(&mut self) where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceCreate { @@ -400,7 +401,7 @@ macro_rules! implOpenapiRouter { fn update_all(&mut self) where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdateAll { @@ -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 { 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 6fb889b..2cdd719 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -1,4 +1,4 @@ -use crate::{ResourceType, StatusCode}; +use crate::{ResponseBody, StatusCode}; #[cfg(feature = "openapi")] use crate::{OpenapiSchema, OpenapiType}; use hyper::Body; @@ -110,7 +110,7 @@ impl From for ResourceError } } -impl ResourceResult for Result +impl ResourceResult for Result { fn into_response(self) -> Result { @@ -170,7 +170,7 @@ impl From for Success } } -impl ResourceResult for Success +impl ResourceResult for Success { fn into_response(self) -> Result { diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 437c282..22ed134 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -1,6 +1,7 @@ use crate::{ resource::*, result::{ResourceError, ResourceResult, Response}, + RequestBody, ResourceType, StatusCode }; @@ -77,26 +78,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; @@ -121,7 +122,7 @@ fn response_from(res : Response, state : &State) -> hyper::Response } if Method::borrow_from(state) != Method::HEAD { - *r.body_mut() = res.body.into(); + *r.body_mut() = res.body; } r } @@ -217,7 +218,7 @@ where fn create_handler(state : State) -> Box where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceCreate { @@ -226,7 +227,7 @@ where fn update_all_handler(state : State) -> Box where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdateAll { @@ -236,7 +237,7 @@ where fn update_handler(state : State) -> Box where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdate { @@ -372,7 +373,7 @@ macro_rules! implDrawResourceRoutes { fn create(&mut self) where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceCreate { @@ -384,7 +385,7 @@ macro_rules! implDrawResourceRoutes { fn update_all(&mut self) where - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdateAll { @@ -397,7 +398,7 @@ macro_rules! implDrawResourceRoutes { fn update(&mut self) where ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static, - Body : ResourceType, + Body : RequestBody, Res : ResourceResult, Handler : ResourceUpdate { diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index f354e92..9e34add 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -3,28 +3,46 @@ use crate::OpenapiType; 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 +{ +} + + +/// 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 + DeserializeOwned +{ +} + +impl RequestBody for T { } From f737ac43326fca9c21dc16ea59b5dd5c2eceda4a Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 20 Oct 2019 01:59:28 +0200 Subject: [PATCH 06/11] delegate request body parsing to trait --- gotham_restful/src/lib.rs | 5 ++++- gotham_restful/src/routing.rs | 13 +++++++++++-- gotham_restful/src/types.rs | 27 +++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 727eca9..fe8a1f0 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -76,7 +76,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::*; diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 22ed134..d587e38 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -27,6 +27,7 @@ use gotham::{ use hyper::{ header::CONTENT_TYPE, Body, + HeaderMap, Method }; use mime::{Mime, APPLICATION_JSON}; @@ -144,7 +145,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 { @@ -156,8 +157,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(); diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index 9e34add..e0198bc 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -1,6 +1,8 @@ #[cfg(feature = "openapi")] -use crate::OpenapiType; +use crate::{OpenapiType, result::ResourceError}; +use hyper::Chunk; +use mime::{Mime, APPLICATION_JSON}; use serde::{de::DeserializeOwned, Serialize}; #[cfg(not(feature = "openapi"))] @@ -39,10 +41,31 @@ 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 + DeserializeOwned +pub trait RequestBody : ResourceType + 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 { + 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) + } } From 5282dbbe6ca3e02624880f8cb9a88f1dd94c70fe Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 20 Oct 2019 15:42:26 +0200 Subject: [PATCH 07/11] 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() +} From 197aad3c945a06bb239d31c78dcb312d624bcdff Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 20 Oct 2019 15:44:20 +0200 Subject: [PATCH 08/11] clippy --- gotham_restful_derive/src/from_body.rs | 44 ++++++++++---------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index 79af118..633998f 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -16,37 +16,27 @@ pub fn expand_from_body(tokens : TokenStream) -> TokenStream 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") + 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; - 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") + 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{})) From dd9e10a154d2ad250a4a44a47e8ccb4447a590c9 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 20 Oct 2019 15:53:30 +0200 Subject: [PATCH 09/11] match content type --- gotham_restful/src/routing.rs | 51 ++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index d587e38..5a16281 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -20,7 +20,11 @@ use gotham::{ router::{ builder::*, non_match::RouteNonMatch, - route::matcher::{AcceptHeaderRouteMatcher, RouteMatcher} + route::matcher::{ + content_type::ContentTypeHeaderRouteMatcher, + AcceptHeaderRouteMatcher, + RouteMatcher + } }, state::{FromState, State} }; @@ -305,6 +309,33 @@ impl From>> for MaybeMatchAcceptHeader } } +#[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) => { @@ -386,9 +417,11 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceCreate { - let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); + let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); + let content_matcher : MaybeMatchContentTypeHeader = Body::supported_types().into(); self.0.post(&self.1) - .extend_route_matcher(matcher) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) .to(|state| create_handler::(state)); } @@ -398,9 +431,11 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceUpdateAll { - let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); + let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); + let content_matcher : MaybeMatchContentTypeHeader = Body::supported_types().into(); self.0.put(&self.1) - .extend_route_matcher(matcher) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) .to(|state| update_all_handler::(state)); } @@ -411,9 +446,11 @@ macro_rules! implDrawResourceRoutes { Res : ResourceResult, Handler : ResourceUpdate { - let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into(); + 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(matcher) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) .with_path_extractor::>() .to(|state| update_handler::(state)); } From 0863d41084c9a62f47b77d8a87ddac4d16876ea2 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 20 Oct 2019 16:01:57 +0200 Subject: [PATCH 10/11] respect mime types in openapi router --- gotham_restful/src/openapi/router.rs | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index ae32444..3500df5 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -19,7 +19,7 @@ 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 as OARequestBody, Response, Responses, Schema, @@ -172,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 } @@ -266,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 { @@ -283,7 +283,7 @@ fn new_operation(default_status : hyper::StatusCode, schema : ReferenceOr() @@ -360,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::() @@ -376,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::() @@ -393,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::() @@ -410,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::() @@ -428,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::() @@ -443,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::() @@ -459,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::() From eb091be81b0906bd0d403f08166b9870e4f056c7 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 20 Oct 2019 16:19:01 +0200 Subject: [PATCH 11/11] add some openapi tests --- gotham_restful/src/openapi/router.rs | 21 +++++++++++++++++++++ gotham_restful/src/result.rs | 12 +++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 3500df5..48565de 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -476,6 +476,7 @@ implOpenapiRouter!(ScopeBuilder); #[cfg(test)] mod test { + use crate::ResourceResult; use super::*; #[derive(OpenapiType)] @@ -521,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/result.rs b/gotham_restful/src/result.rs index 2cdd719..c5e67b0 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -2,7 +2,7 @@ use crate::{ResponseBody, StatusCode}; #[cfg(feature = "openapi")] use crate::{OpenapiSchema, OpenapiType}; use hyper::Body; -use mime::{Mime, APPLICATION_JSON}; +use mime::{Mime, APPLICATION_JSON, STAR_STAR}; #[cfg(feature = "openapi")] use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; use serde::Serialize; @@ -289,6 +289,11 @@ impl> ResourceResult for Raw 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 { @@ -315,6 +320,11 @@ where } } + fn accepted_types() -> Option> + { + as ResourceResult>::accepted_types() + } + #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema {