From d754d6044de4e2dd34031ad1d3e8a6bf246a27ec Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Fri, 1 May 2020 14:48:11 +0000 Subject: [PATCH] Allow custom error types through a macro and allow them to be used with Result --- README.md | 2 +- example/src/main.rs | 27 +- gotham_restful/src/lib.rs | 14 +- gotham_restful/src/response.rs | 63 ++ gotham_restful/src/result.rs | 704 -------------------- gotham_restful/src/result/auth_result.rs | 107 +++ gotham_restful/src/result/mod.rs | 224 +++++++ gotham_restful/src/result/no_content.rs | 106 +++ gotham_restful/src/result/raw.rs | 90 +++ gotham_restful/src/result/result.rs | 59 ++ gotham_restful/src/result/success.rs | 136 ++++ gotham_restful/src/routing.rs | 9 +- gotham_restful/src/types.rs | 17 +- gotham_restful_derive/Cargo.toml | 2 + gotham_restful_derive/src/from_body.rs | 2 +- gotham_restful_derive/src/lib.rs | 9 + gotham_restful_derive/src/openapi_type.rs | 24 +- gotham_restful_derive/src/resource_error.rs | 299 +++++++++ gotham_restful_derive/src/util.rs | 22 + 19 files changed, 1165 insertions(+), 751 deletions(-) create mode 100644 gotham_restful/src/response.rs delete mode 100644 gotham_restful/src/result.rs create mode 100644 gotham_restful/src/result/auth_result.rs create mode 100644 gotham_restful/src/result/mod.rs create mode 100644 gotham_restful/src/result/no_content.rs create mode 100644 gotham_restful/src/result/raw.rs create mode 100644 gotham_restful/src/result/result.rs create mode 100644 gotham_restful/src/result/success.rs create mode 100644 gotham_restful_derive/src/resource_error.rs diff --git a/README.md b/README.md index 51700af..1a7e323 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ fn main() { } ``` -Uploads and Downloads can also be handled, but you need to specify the mime type manually: +Uploads and Downloads can also be handled: ```rust #[derive(Resource)] diff --git a/example/src/main.rs b/example/src/main.rs index 3d9a249..f1d822b 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -36,7 +36,7 @@ struct User } #[rest_read_all(Users)] -fn read_all(_state : &mut State) -> Success>> +fn read_all() -> Success>> { vec![Username().fake(), Username().fake()] .into_iter() @@ -46,56 +46,55 @@ fn read_all(_state : &mut State) -> Success>> } #[rest_read(Users)] -fn read(_state : &mut State, id : u64) -> Success +fn read(id : u64) -> Success { let username : String = Username().fake(); User { username: format!("{}{}", username, id) }.into() } #[rest_search(Users)] -fn search(_state : &mut State, query : User) -> Success +fn search(query : User) -> Success { query.into() } #[rest_create(Users)] -fn create(_state : &mut State, body : User) +fn create(body : User) { info!("Created User: {}", body.username); } #[rest_update_all(Users)] -fn update_all(_state : &mut State, body : Vec) +fn update_all(body : Vec) { info!("Changing all Users to {:?}", body.into_iter().map(|u| u.username).collect::>()); } #[rest_update(Users)] -fn update(_state : &mut State, id : u64, body : User) +fn update(id : u64, body : User) { info!("Change User {} to {}", id, body.username); } #[rest_delete_all(Users)] -fn delete_all(_state : &mut State) +fn delete_all() { info!("Delete all Users"); } #[rest_delete(Users)] -fn delete(_state : &mut State, id : u64) +fn delete(id : u64) { info!("Delete User {}", id); } #[rest_read_all(Auth)] -fn auth_read_all(auth : AuthStatus<()>) -> AuthResult> +fn auth_read_all(auth : AuthStatus<()>) -> AuthSuccess { - let str : Success = match auth { - AuthStatus::Authenticated(data) => format!("{:?}", data).into(), - _ => return AuthErr - }; - str.into() + match auth { + AuthStatus::Authenticated(data) => Ok(format!("{:?}", data).into()), + _ => Err(Forbidden) + } } const ADDR : &str = "127.0.0.1:18080"; diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index c62ce0b..1142f29 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -52,7 +52,7 @@ fn main() { } ``` -Uploads and Downloads can also be handled, but you need to specify the mime type manually: +Uploads and Downloads can also be handled: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -131,6 +131,8 @@ pub mod export { pub use futures_util::future::FutureExt; + pub use serde_json; + #[cfg(feature = "database")] pub use gotham_middleware_diesel::Repo; @@ -176,14 +178,20 @@ pub use resource::{ ResourceDelete }; +mod response; +pub use response::Response; + mod result; pub use result::{ + AuthError, + AuthError::Forbidden, + AuthErrorOrOther, AuthResult, - AuthResult::AuthErr, + AuthSuccess, + IntoResponseError, NoContent, Raw, ResourceResult, - Response, Success }; diff --git a/gotham_restful/src/response.rs b/gotham_restful/src/response.rs new file mode 100644 index 0000000..ee4e1f3 --- /dev/null +++ b/gotham_restful/src/response.rs @@ -0,0 +1,63 @@ +use gotham::hyper::{Body, StatusCode}; +use mime::{Mime, APPLICATION_JSON}; + +/// 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 + } + } + + /// Create an empty _403 Forbidden_ `Response`. + pub fn forbidden() -> Self + { + Self { + status: StatusCode::FORBIDDEN, + body: Body::empty(), + mime: None + } + } + + #[cfg(test)] + pub(crate) fn full_body(mut self) -> Result, ::Error> + { + use futures_executor::block_on; + use gotham::hyper::body::to_bytes; + + let bytes : &[u8] = &block_on(to_bytes(&mut self.body))?; + Ok(bytes.to_vec()) + } +} \ No newline at end of file diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs deleted file mode 100644 index 792298f..0000000 --- a/gotham_restful/src/result.rs +++ /dev/null @@ -1,704 +0,0 @@ -use crate::{ResponseBody, StatusCode}; -#[cfg(feature = "openapi")] -use crate::{OpenapiSchema, OpenapiType}; -use futures_core::future::Future; -use futures_util::{future, future::FutureExt}; -use gotham::hyper::Body; -#[cfg(feature = "errorlog")] -use log::error; -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, - fmt::Debug, - pin::Pin -}; - -/// 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 - } - } - - /// Create an empty _403 Forbidden_ `Response`. - pub fn forbidden() -> Self - { - Self { - status: StatusCode::FORBIDDEN, - body: Body::empty(), - mime: None - } - } - - #[cfg(test)] - fn full_body(mut self) -> Result, ::Error> - { - use futures_executor::block_on; - use gotham::hyper::body::to_bytes; - - let bytes : &[u8] = &block_on(to_bytes(&mut self.body))?; - Ok(bytes.to_vec()) - } -} - - -/// A trait provided to convert a resource's result to json. -pub trait ResourceResult -{ - type Err : Error + Send + 'static; - - /// Turn this into a response that can be returned to the browser. This api will likely - /// change in the future. - fn into_response(self) -> Pin> + Send>>; - - /// Return a list of supported mime types. - fn accepted_types() -> Option> - { - None - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema; - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - StatusCode::OK - } -} - -#[cfg(feature = "openapi")] -impl crate::OpenapiType for Res -{ - fn schema() -> OpenapiSchema - { - Self::schema() - } -} - -/// The default json returned on an 500 Internal Server Error. -#[derive(Debug, Serialize)] -pub struct ResourceError -{ - error : bool, - message : String -} - -impl From for ResourceError -{ - fn from(message : T) -> Self - { - Self { - error: true, - message: message.to_string() - } - } -} - -fn into_response_helper(create_response : F) -> Pin> + Send>> -where - Err : Send + 'static, - F : FnOnce() -> Result -{ - let res = create_response(); - async move { res }.boxed() -} - -#[cfg(feature = "errorlog")] -fn errorlog(e : E) -{ - error!("The handler encountered an error: {}", e); -} - -#[cfg(not(feature = "errorlog"))] -fn errorlog(_e : E) {} - -impl ResourceResult for Result -where - Self : Send -{ - type Err = SerdeJsonError; - - fn into_response(self) -> Pin> + Send>> - { - into_response_helper(|| { - Ok(match self { - Ok(r) => Response::json(StatusCode::OK, serde_json::to_string(&r)?), - Err(e) => { - errorlog(&e); - let err : ResourceError = e.into(); - 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 - { - R::schema() - } -} - - -impl ResourceResult for Pin + Send>> -where - Res : ResourceResult + 'static -{ - type Err = Res::Err; - - fn into_response(self) -> Pin> + Send>> - { - self.then(|result| { - result.into_response() - }).boxed() - } - - fn accepted_types() -> Option> - { - Res::accepted_types() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - Res::schema() - } - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - Res::default_status() - } -} - - -/** -This can be returned from a resource when there is no cause of an error. For example: - -``` -# #[macro_use] extern crate gotham_restful_derive; -# mod doc_tests_are_broken { -# use gotham::state::State; -# use gotham_restful::*; -# use serde::{Deserialize, Serialize}; -# -# #[derive(Resource)] -# struct MyResource; -# -#[derive(Deserialize, Serialize)] -# #[derive(OpenapiType)] -struct MyResponse { - message: String -} - -#[rest_read_all(MyResource)] -fn read_all(_state: &mut State) -> Success { - let res = MyResponse { message: "I'm always happy".to_string() }; - res.into() -} -# } -``` -*/ -pub struct Success(T); - -impl From for Success -{ - fn from(t : T) -> Self - { - Self(t) - } -} - -impl Clone for Success -{ - fn clone(&self) -> Self - { - Self(self.0.clone()) - } -} - -impl Debug for Success -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Success({:?})", self.0) - } -} - -impl ResourceResult for Success -where - Self : Send -{ - type Err = SerdeJsonError; - - fn into_response(self) -> Pin> + Send>> - { - into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&self.0)?))) - } - - fn accepted_types() -> Option> - { - Some(vec![APPLICATION_JSON]) - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - T::schema() - } -} - -/** -This return type can be used to map another `ResourceResult` that can only be returned if the -client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can -look something like this (assuming the `auth` feature is enabled): - -``` -# #[macro_use] extern crate gotham_restful_derive; -# mod doc_tests_are_broken { -# use gotham::state::State; -# use gotham_restful::*; -# use serde::Deserialize; -# -# #[derive(Resource)] -# struct MyResource; -# -# #[derive(Clone, Deserialize)] -# struct MyAuthData { exp : u64 } -# -#[rest_read_all(MyResource)] -fn read_all(auth : AuthStatus) -> AuthResult { - let auth_data = match auth { - AuthStatus::Authenticated(data) => data, - _ => return AuthErr - }; - // do something - NoContent::default().into() -} -# } -``` -*/ -pub enum AuthResult -{ - Ok(T), - AuthErr -} - -impl AuthResult -{ - pub fn is_ok(&self) -> bool - { - match self { - Self::Ok(_) => true, - _ => false - } - } - - pub fn unwrap(self) -> T - { - match self { - Self::Ok(data) => data, - _ => panic!() - } - } -} - -impl From for AuthResult -{ - fn from(t : T) -> Self - { - Self::Ok(t) - } -} - -impl Clone for AuthResult -{ - fn clone(&self) -> Self - { - match self { - Self::Ok(t) => Self::Ok(t.clone()), - Self::AuthErr => Self::AuthErr - } - } -} - -impl Debug for AuthResult -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Ok(t) => write!(f, "Ok({:?})", t), - Self::AuthErr => write!(f, "AuthErr") - } - } -} - -impl ResourceResult for AuthResult -{ - type Err = T::Err; - - fn into_response(self) -> Pin> + Send>> - { - match self - { - Self::Ok(res) => res.into_response(), - Self::AuthErr => future::ok(Response::forbidden()).boxed() - } - } - - fn accepted_types() -> Option> - { - T::accepted_types() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - T::schema() - } - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - T::default_status() - } -} - -impl, E : Error> ResourceResult for Result, E> -{ - type Err = T::Err; - - fn into_response(self) -> Pin> + Send>> - { - match self { - Ok(r) => r.into_response(), - Err(e) => { - into_response_helper(|| { - errorlog(&e); - let err : ResourceError = e.into(); - Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) - }) - } - } - } - - fn accepted_types() -> Option> - { - T::accepted_types() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - T::schema() - } - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - T::default_status() - } -} - -/** -This is the return type of a resource that doesn't actually return something. It will result -in a _204 No Content_ answer by default. You don't need to use this type directly if using -the function attributes: - -``` -# #[macro_use] extern crate gotham_restful_derive; -# mod doc_tests_are_broken { -# use gotham::state::State; -# use gotham_restful::*; -# -# #[derive(Resource)] -# struct MyResource; -# -#[rest_read_all(MyResource)] -fn read_all(_state: &mut State) { - // do something -} -# } -``` -*/ -#[derive(Default)] -pub struct NoContent; - -impl From<()> for NoContent -{ - fn from(_ : ()) -> Self - { - Self {} - } -} - -impl ResourceResult for NoContent -{ - type Err = SerdeJsonError; // just for easier handling of `Result` - - /// This will always be a _204 No Content_ together with an empty string. - fn into_response(self) -> Pin> + Send>> - { - future::ok(Response::no_content()).boxed() - } - - fn accepted_types() -> Option> - { - Some(Vec::new()) - } - - /// Returns the schema of the `()` type. - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - <()>::schema() - } - - /// This will always be a _204 No Content_ - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - StatusCode::NO_CONTENT - } -} - -impl ResourceResult for Result -where - Self : Send -{ - type Err = SerdeJsonError; - - fn into_response(self) -> Pin> + Send>> - { - match self { - Ok(nc) => nc.into_response(), - Err(e) => into_response_helper(|| { - let err : ResourceError = e.into(); - Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) - }) - } - } - - fn accepted_types() -> Option> - { - NoContent::accepted_types() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - ::schema() - } - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - NoContent::default_status() - } -} - -pub struct Raw -{ - pub raw : T, - pub mime : Mime -} - -impl Raw -{ - pub fn new(raw : T, mime : Mime) -> Self - { - Self { raw, mime } - } -} - -impl Clone for Raw -{ - fn clone(&self) -> Self - { - Self { - raw: self.raw.clone(), - mime: self.mime.clone() - } - } -} - -impl Debug for Raw -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Raw({:?}, {:?})", self.raw, self.mime) - } -} - -impl> ResourceResult for Raw -where - Self : Send -{ - type Err = SerdeJsonError; // just for easier handling of `Result, E>` - - fn into_response(self) -> Pin> + Send>> - { - future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), - ..Default::default() - }))) - } -} - -impl ResourceResult for Result, E> -where - Self : Send, - Raw : ResourceResult -{ - type Err = SerdeJsonError; - - fn into_response(self) -> Pin> + Send>> - { - match self { - Ok(raw) => raw.into_response(), - Err(e) => into_response_helper(|| { - 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 futures_executor::block_on; - use mime::TEXT_PLAIN; - use thiserror::Error; - - #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(OpenapiType))] - struct Msg - { - msg : String - } - - #[derive(Debug, Default, Error)] - #[error("An Error")] - struct MsgError; - - #[test] - fn resource_result_ok() - { - let ok : Result = Ok(Msg::default()); - let res = block_on(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().unwrap(), r#"{"msg":""}"#.as_bytes()); - } - - #[test] - fn resource_result_err() - { - let err : Result = Err(MsgError::default()); - let res = block_on(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().unwrap(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()); - } - - #[test] - fn success_always_successfull() - { - let success : Success = Msg::default().into(); - let res = block_on(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().unwrap(), r#"{"msg":""}"#.as_bytes()); - } - - #[test] - fn no_content_has_empty_response() - { - let no_content = NoContent::default(); - let res = block_on(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().unwrap(), &[] as &[u8]); - } - - #[test] - fn no_content_result() - { - let no_content : Result = Ok(NoContent::default()); - let res = block_on(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().unwrap(), &[] as &[u8]); - } - - #[test] - fn raw_response() - { - let msg = "Test"; - let raw = Raw::new(msg, TEXT_PLAIN); - let res = block_on(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().unwrap(), msg.as_bytes()); - } -} diff --git a/gotham_restful/src/result/auth_result.rs b/gotham_restful/src/result/auth_result.rs new file mode 100644 index 0000000..86db9d3 --- /dev/null +++ b/gotham_restful/src/result/auth_result.rs @@ -0,0 +1,107 @@ +use gotham_restful_derive::ResourceError; + + +/** +This is an error type that always yields a _403 Forbidden_ response. This type is best used in +combination with [`AuthSuccess`] or [`AuthResult`]. + + [`AuthSuccess`]: type.AuthSuccess.html + [`AuthResult`]: type.AuthResult.html +*/ +#[derive(ResourceError)] +pub enum AuthError +{ + #[status(FORBIDDEN)] + #[display("Forbidden")] + Forbidden +} + +/** +This return type can be used to map another `ResourceResult` that can only be returned if the +client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can +look something like this (assuming the `auth` feature is enabled): + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# use serde::Deserialize; +# +# #[derive(Resource)] +# struct MyResource; +# +# #[derive(Clone, Deserialize)] +# struct MyAuthData { exp : u64 } +# +#[rest_read_all(MyResource)] +fn read_all(auth : AuthStatus) -> AuthSuccess { + let auth_data = match auth { + AuthStatus::Authenticated(data) => data, + _ => return Err(Forbidden) + }; + // do something + Ok(NoContent::default()) +} +# } +``` +*/ +pub type AuthSuccess = Result; + +/** +This is an error type that either yields a _403 Forbidden_ respone if produced from an authentication +error, or delegates to another error type. This type is best used with [`AuthResult`]. + + [`AuthResult`]: type.AuthResult.html +*/ +#[derive(ResourceError)] +pub enum AuthErrorOrOther +{ + #[status(UNAUTHORIZED)] + #[display("Forbidden")] + Forbidden, + #[display("{0}")] + Other(#[from] E) +} + +impl From for AuthErrorOrOther +{ + fn from(err : AuthError) -> Self + { + match err { + AuthError::Forbidden => Self::Forbidden + } + } +} + +/** +This return type can be used to map another `ResourceResult` that can only be returned if the +client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can +look something like this (assuming the `auth` feature is enabled): + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# use serde::Deserialize; +# use std::io; +# +# #[derive(Resource)] +# struct MyResource; +# +# #[derive(Clone, Deserialize)] +# struct MyAuthData { exp : u64 } +# +#[rest_read_all(MyResource)] +fn read_all(auth : AuthStatus) -> AuthResult { + let auth_data = match auth { + AuthStatus::Authenticated(data) => data, + _ => Err(Forbidden)? + }; + // do something + Ok(NoContent::default().into()) +} +# } +*/ +pub type AuthResult = Result>; diff --git a/gotham_restful/src/result/mod.rs b/gotham_restful/src/result/mod.rs new file mode 100644 index 0000000..8399c7c --- /dev/null +++ b/gotham_restful/src/result/mod.rs @@ -0,0 +1,224 @@ +use crate::Response; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use futures_util::future::FutureExt; +use mime::Mime; +use serde::Serialize; +use std::{ + error::Error, + future::Future, + fmt::{Debug, Display}, + pin::Pin +}; + +mod auth_result; +pub use auth_result::{AuthError, AuthErrorOrOther, AuthResult, AuthSuccess}; + +mod no_content; +pub use no_content::NoContent; + +mod raw; +pub use raw::Raw; + +mod result; +pub use result::IntoResponseError; + +mod success; +pub use success::Success; + +/// A trait provided to convert a resource's result to json. +pub trait ResourceResult +{ + type Err : Error + Send + 'static; + + /// Turn this into a response that can be returned to the browser. This api will likely + /// change in the future. + fn into_response(self) -> Pin> + Send>>; + + /// Return a list of supported mime types. + fn accepted_types() -> Option> + { + None + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema; + + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + crate::StatusCode::OK + } +} + +#[cfg(feature = "openapi")] +impl crate::OpenapiType for Res +{ + fn schema() -> OpenapiSchema + { + Self::schema() + } +} + +/// The default json returned on an 500 Internal Server Error. +#[derive(Debug, Serialize)] +pub(crate) struct ResourceError +{ + error : bool, + message : String +} + +impl From for ResourceError +{ + fn from(message : T) -> Self + { + Self { + error: true, + message: message.to_string() + } + } +} + +fn into_response_helper(create_response : F) -> Pin> + Send>> +where + Err : Send + 'static, + F : FnOnce() -> Result +{ + let res = create_response(); + async move { res }.boxed() +} + +#[cfg(feature = "errorlog")] +fn errorlog(e : E) +{ + error!("The handler encountered an error: {}", e); +} + +#[cfg(not(feature = "errorlog"))] +fn errorlog(_e : E) {} + +fn handle_error(e : E) -> Pin> + Send>> +where + E : Display + IntoResponseError +{ + into_response_helper(|| { + errorlog(&e); + e.into_response_error() + }) +} + + +impl ResourceResult for Pin + Send>> +where + Res : ResourceResult + 'static +{ + type Err = Res::Err; + + fn into_response(self) -> Pin> + Send>> + { + self.then(|result| { + result.into_response() + }).boxed() + } + + fn accepted_types() -> Option> + { + Res::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + Res::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + Res::default_status() + } +} + + + +#[cfg(test)] +mod test +{ + use super::*; + use crate::{OpenapiType, StatusCode}; + use futures_executor::block_on; + use mime::{APPLICATION_JSON, TEXT_PLAIN}; + use thiserror::Error; + + #[derive(Debug, Default, Deserialize, Serialize)] + #[cfg_attr(feature = "openapi", derive(OpenapiType))] + struct Msg + { + msg : String + } + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn resource_result_ok() + { + let ok : Result = Ok(Msg::default()); + let res = block_on(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().unwrap(), r#"{"msg":""}"#.as_bytes()); + } + + #[test] + fn resource_result_err() + { + let err : Result = Err(MsgError::default()); + let res = block_on(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().unwrap(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()); + } + + #[test] + fn success_always_successfull() + { + let success : Success = Msg::default().into(); + let res = block_on(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().unwrap(), r#"{"msg":""}"#.as_bytes()); + } + + #[test] + fn no_content_has_empty_response() + { + let no_content = NoContent::default(); + let res = block_on(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().unwrap(), &[] as &[u8]); + } + + #[test] + fn no_content_result() + { + let no_content : Result = Ok(NoContent::default()); + let res = block_on(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().unwrap(), &[] as &[u8]); + } + + #[test] + fn raw_response() + { + let msg = "Test"; + let raw = Raw::new(msg, TEXT_PLAIN); + let res = block_on(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().unwrap(), msg.as_bytes()); + } +} diff --git a/gotham_restful/src/result/no_content.rs b/gotham_restful/src/result/no_content.rs new file mode 100644 index 0000000..c04ea8d --- /dev/null +++ b/gotham_restful/src/result/no_content.rs @@ -0,0 +1,106 @@ +use super::{ResourceResult, handle_error}; +use crate::{IntoResponseError, Response}; +#[cfg(feature = "openapi")] +use crate::{OpenapiSchema, OpenapiType}; +use futures_util::{future, future::FutureExt}; +use mime::Mime; +use std::{ + fmt::Display, + future::Future, + pin::Pin +}; + +/** +This is the return type of a resource that doesn't actually return something. It will result +in a _204 No Content_ answer by default. You don't need to use this type directly if using +the function attributes: + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# +# #[derive(Resource)] +# struct MyResource; +# +#[rest_read_all(MyResource)] +fn read_all(_state: &mut State) { + // do something +} +# } +``` +*/ +#[derive(Clone, Copy, Default)] +pub struct NoContent; + +impl From<()> for NoContent +{ + fn from(_ : ()) -> Self + { + Self {} + } +} + +impl ResourceResult for NoContent +{ + // TODO this shouldn't be a serde_json::Error + type Err = serde_json::Error; // just for easier handling of `Result` + + /// This will always be a _204 No Content_ together with an empty string. + fn into_response(self) -> Pin> + Send>> + { + future::ok(Response::no_content()).boxed() + } + + fn accepted_types() -> Option> + { + Some(Vec::new()) + } + + /// Returns the schema of the `()` type. + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + <()>::schema() + } + + /// This will always be a _204 No Content_ + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + crate::StatusCode::NO_CONTENT + } +} + +impl ResourceResult for Result +where + E : Display + IntoResponseError +{ + type Err = serde_json::Error; + + fn into_response(self) -> Pin> + Send>> + { + match self { + Ok(nc) => nc.into_response(), + Err(e) => handle_error(e) + } + } + + fn accepted_types() -> Option> + { + NoContent::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + ::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + NoContent::default_status() + } +} \ No newline at end of file diff --git a/gotham_restful/src/result/raw.rs b/gotham_restful/src/result/raw.rs new file mode 100644 index 0000000..95fafdf --- /dev/null +++ b/gotham_restful/src/result/raw.rs @@ -0,0 +1,90 @@ +use super::{IntoResponseError, ResourceResult, handle_error}; +use crate::{Response, StatusCode}; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use futures_core::future::Future; +use futures_util::{future, future::FutureExt}; +use gotham::hyper::Body; +use mime::Mime; +#[cfg(feature = "openapi")] +use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; +use serde_json::error::Error as SerdeJsonError; +use std::{ + fmt::{Debug, Display}, + pin::Pin +}; + +pub struct Raw +{ + pub raw : T, + pub mime : Mime +} + +impl Raw +{ + pub fn new(raw : T, mime : Mime) -> Self + { + Self { raw, mime } + } +} + +impl Clone for Raw +{ + fn clone(&self) -> Self + { + Self { + raw: self.raw.clone(), + mime: self.mime.clone() + } + } +} + +impl Debug for Raw +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Raw({:?}, {:?})", self.raw, self.mime) + } +} + +impl> ResourceResult for Raw +where + Self : Send +{ + type Err = SerdeJsonError; // just for easier handling of `Result, E>` + + fn into_response(self) -> Pin> + Send>> + { + future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), + ..Default::default() + }))) + } +} + +impl ResourceResult for Result, E> +where + Raw : ResourceResult, + E : Display + IntoResponseError as ResourceResult>::Err> +{ + type Err = E::Err; + + fn into_response(self) -> Pin> + Send>> + { + match self { + Ok(raw) => raw.into_response(), + Err(e) => handle_error(e) + } + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + as ResourceResult>::schema() + } +} \ No newline at end of file diff --git a/gotham_restful/src/result/result.rs b/gotham_restful/src/result/result.rs new file mode 100644 index 0000000..5857812 --- /dev/null +++ b/gotham_restful/src/result/result.rs @@ -0,0 +1,59 @@ +use super::{ResourceResult, handle_error, into_response_helper}; +use crate::{ + result::ResourceError, + Response, ResponseBody, StatusCode +}; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use futures_core::future::Future; +use mime::{Mime, APPLICATION_JSON}; +use std::{ + error::Error, + fmt::Display, + pin::Pin +}; + +pub trait IntoResponseError +{ + type Err : Error + Send + 'static; + + fn into_response_error(self) -> Result; +} + +impl IntoResponseError for E +{ + type Err = serde_json::Error; + + fn into_response_error(self) -> Result + { + let err : ResourceError = self.into(); + Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) + } +} + +impl ResourceResult for Result +where + R : ResponseBody, + E : Display + IntoResponseError +{ + type Err = E::Err; + + fn into_response(self) -> Pin> + Send>> + { + match self { + Ok(r) => into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&r)?))), + Err(e) => handle_error(e) + } + } + + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + R::schema() + } +} \ No newline at end of file diff --git a/gotham_restful/src/result/success.rs b/gotham_restful/src/result/success.rs new file mode 100644 index 0000000..9931384 --- /dev/null +++ b/gotham_restful/src/result/success.rs @@ -0,0 +1,136 @@ +use super::{ResourceResult, into_response_helper}; +use crate::{Response, ResponseBody}; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use gotham::hyper::StatusCode; +use mime::{Mime, APPLICATION_JSON}; +use std::{ + fmt::Debug, + future::Future, + pin::Pin, + ops::{Deref, DerefMut} +}; + +/** +This can be returned from a resource when there is no cause of an error. It behaves similar to a +smart pointer like box, it that it implements `AsRef`, `Deref` and the likes. + +Usage example: + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# use serde::{Deserialize, Serialize}; +# +# #[derive(Resource)] +# struct MyResource; +# +#[derive(Deserialize, Serialize)] +# #[derive(OpenapiType)] +struct MyResponse { + message: &'static str +} + +#[rest_read_all(MyResource)] +fn read_all(_state: &mut State) -> Success { + let res = MyResponse { message: "I'm always happy" }; + res.into() +} +# } +``` +*/ +pub struct Success(T); + +impl AsMut for Success +{ + fn as_mut(&mut self) -> &mut T + { + &mut self.0 + } +} + +impl AsRef for Success +{ + fn as_ref(&self) -> &T + { + &self.0 + } +} + +impl Deref for Success +{ + type Target = T; + + fn deref(&self) -> &T + { + &self.0 + } +} + +impl DerefMut for Success +{ + fn deref_mut(&mut self) -> &mut T + { + &mut self.0 + } +} + +impl From for Success +{ + fn from(t : T) -> Self + { + Self(t) + } +} + +impl Clone for Success +{ + fn clone(&self) -> Self + { + Self(self.0.clone()) + } +} + +impl Copy for Success +{ +} + +impl Debug for Success +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Success({:?})", self.0) + } +} + +impl Default for Success +{ + fn default() -> Self + { + Self(T::default()) + } +} + +impl ResourceResult for Success +where + Self : Send +{ + type Err = serde_json::Error; + + fn into_response(self) -> Pin> + Send>> + { + into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(self.as_ref())?))) + } + + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + T::schema() + } +} diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index f213248..ffc9e58 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -1,13 +1,16 @@ use crate::{ matcher::{AcceptHeaderMatcher, ContentTypeMatcher}, - openapi::router::OpenapiRouter, resource::*, - result::{ResourceError, ResourceResult, Response}, + result::{ResourceError, ResourceResult}, RequestBody, + Response, StatusCode }; #[cfg(feature = "openapi")] -use crate::openapi::builder::OpenapiBuilder; +use crate::openapi::{ + builder::OpenapiBuilder, + router::OpenapiRouter +}; use futures_util::{future, future::FutureExt}; use gotham::{ diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index e0c769b..504cf94 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -1,11 +1,14 @@ #[cfg(feature = "openapi")] use crate::OpenapiType; -use crate::result::ResourceError; use gotham::hyper::body::Bytes; use mime::{Mime, APPLICATION_JSON}; use serde::{de::DeserializeOwned, Serialize}; -use std::panic::RefUnwindSafe; +use std::{ + error::Error, + panic::RefUnwindSafe +}; +use thiserror::Error; #[cfg(not(feature = "openapi"))] pub trait ResourceType @@ -44,7 +47,7 @@ impl ResponseBody for T /// to create the type from a hyper body chunk and it's content type. pub trait FromBody : Sized { - type Err : Into; + type Err : Error; /// Create the request body from a raw body and the content type. fn from_body(body : Bytes, content_type : Mime) -> Result; @@ -60,6 +63,14 @@ impl FromBody for T } } +/// This error type can be used by `FromBody` implementations when there is no need to return any +/// errors. + +#[derive(Clone, Copy, Debug, Error)] +#[error("No Error")] +pub struct FromBodyNoError; + + /// 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`. diff --git a/gotham_restful_derive/Cargo.toml b/gotham_restful_derive/Cargo.toml index 4166067..1fb4f53 100644 --- a/gotham_restful_derive/Cargo.toml +++ b/gotham_restful_derive/Cargo.toml @@ -18,8 +18,10 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] heck = "0.3.1" +lazy_static = "1.4.0" proc-macro2 = "1.0.10" quote = "1.0.3" +regex = "1.3.7" syn = { version = "1.0.17", features = ["extra-traits", "full"] } [features] diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index dd2069e..a027c55 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -120,7 +120,7 @@ fn expand(tokens : TokenStream) -> Result impl #generics #krate::FromBody for #ident #generics where #where_clause { - type Err = String; + type Err = #krate::FromBodyNoError; fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result { diff --git a/gotham_restful_derive/src/lib.rs b/gotham_restful_derive/src/lib.rs index 4a98a9d..78b8796 100644 --- a/gotham_restful_derive/src/lib.rs +++ b/gotham_restful_derive/src/lib.rs @@ -12,6 +12,8 @@ mod request_body; use request_body::expand_request_body; mod resource; use resource::expand_resource; +mod resource_error; +use resource_error::expand_resource_error; #[cfg(feature = "openapi")] mod openapi_type; @@ -52,6 +54,13 @@ pub fn derive_resource(tokens : TokenStream) -> TokenStream print_tokens(expand_resource(tokens)) } +#[proc_macro_derive(ResourceError, attributes(display, from, status))] +pub fn derive_resource_error(tokens : TokenStream) -> TokenStream +{ + print_tokens(expand_resource_error(tokens)) +} + + #[proc_macro_attribute] pub fn rest_read_all(attr : TokenStream, item : TokenStream) -> TokenStream { diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index 4f39983..319972c 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -1,12 +1,7 @@ -use crate::util::CollectToResult; +use crate::util::{CollectToResult, remove_parens}; use proc_macro::TokenStream; -use proc_macro2::{ - Delimiter, - TokenStream as TokenStream2, - TokenTree -}; +use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use std::{iter, iter::FromIterator}; use syn::{ spanned::Spanned, Attribute, @@ -84,21 +79,6 @@ fn to_bool(lit : &Lit) -> Result } } -fn remove_parens(input : TokenStream2) -> TokenStream2 -{ - let iter = input.into_iter().flat_map(|tt| { - if let TokenTree::Group(group) = &tt - { - if group.delimiter() == Delimiter::Parenthesis - { - return Box::new(group.stream().into_iter()) as Box>; - } - } - Box::new(iter::once(tt)) - }); - TokenStream2::from_iter(iter) -} - fn parse_attributes(input : &[Attribute]) -> Result { let mut parsed = Attrs::default(); diff --git a/gotham_restful_derive/src/resource_error.rs b/gotham_restful_derive/src/resource_error.rs new file mode 100644 index 0000000..4958123 --- /dev/null +++ b/gotham_restful_derive/src/resource_error.rs @@ -0,0 +1,299 @@ +use crate::util::{CollectToResult, remove_parens}; +use lazy_static::lazy_static; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use regex::Regex; +use std::iter; +use syn::{ + parse_macro_input, + spanned::Spanned, + Attribute, + Data, + DeriveInput, + Error, + Fields, + GenericParam, + Ident, + LitStr, + Path, + PathSegment, + Type, + Variant +}; + + +struct ErrorVariantField +{ + attrs : Vec, + ident : Ident, + ty : Type +} + +struct ErrorVariant +{ + ident : Ident, + status : Option, + is_named : bool, + fields : Vec, + from_ty : Option<(usize, Type)>, + display : Option +} + +fn process_variant(variant : Variant) -> Result +{ + let status = match variant.attrs.iter() + .find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string())) + { + Some(attr) => Some(parse_macro_input::parse::(remove_parens(attr.tokens.clone()).into())?), + None => None + }; + + let mut is_named = false; + let mut fields = Vec::new(); + match variant.fields { + Fields::Named(named) => { + is_named = true; + for field in named.named + { + let span = field.span(); + fields.push(ErrorVariantField { + attrs: field.attrs, + ident: field.ident.ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?, + ty: field.ty + }); + } + }, + Fields::Unnamed(unnamed) => { + for (i, field) in unnamed.unnamed.into_iter().enumerate() + { + fields.push(ErrorVariantField { + attrs: field.attrs, + ident: format_ident!("arg{}", i), + ty: field.ty + }) + } + }, + Fields::Unit => {} + } + + let from_ty = fields.iter() + .enumerate() + .find(|(_, field)| field.attrs.iter().any(|attr| attr.path.segments.last().map(|segment| segment.ident.to_string()) == Some("from".to_string()))) + .map(|(i, field)| (i, field.ty.clone())); + + let display = match variant.attrs.iter() + .find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string())) + { + Some(attr) => Some(parse_macro_input::parse::(remove_parens(attr.tokens.clone()).into())?), + None => None + }; + + Ok(ErrorVariant { + ident: variant.ident, + status, + is_named, + fields, + from_ty, + display + }) +} + +fn path_segment(name : &str) -> PathSegment +{ + PathSegment { + ident: format_ident!("{}", name), + arguments: Default::default() + } +} + +lazy_static! { + // TODO this is a really ugly regex that requires at least two characters between captures + static ref DISPLAY_REGEX : Regex = Regex::new(r"(^|[^\{])\{(?P[^\}]+)\}([^\}]|$)").unwrap(); +} + +impl ErrorVariant +{ + fn fields_pat(&self) -> TokenStream2 + { + let mut fields = self.fields.iter().map(|field| &field.ident).peekable(); + if fields.peek().is_none() { + quote!() + } else if self.is_named { + quote!( { #( #fields ),* } ) + } else { + quote!( ( #( #fields ),* ) ) + } + } + + fn to_display_match_arm(&self, formatter_ident : &Ident, enum_ident : &Ident) -> Result + { + let ident = &self.ident; + let display = self.display.as_ref().ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?; + + // lets find all required format parameters + let display_str = display.value(); + let params = DISPLAY_REGEX.captures_iter(&display_str) + .map(|cap| format_ident!("{}{}", if self.is_named { "" } else { "arg" }, cap.name("param").unwrap().as_str())); + + let fields_pat = self.fields_pat(); + Ok(quote! { + #enum_ident::#ident #fields_pat => write!(#formatter_ident, #display #(, #params = #params)*) + }) + } + + fn into_match_arm(self, krate : &TokenStream2, enum_ident : &Ident) -> TokenStream2 + { + let ident = &self.ident; + let fields_pat = self.fields_pat(); + let status = self.status.map(|status| { + // the status might be relative to StatusCode, so let's fix that + if status.leading_colon.is_none() && status.segments.len() < 2 + { + let status_ident = status.segments.first().map(|path| path.clone()).unwrap_or_else(|| path_segment("OK")); + Path { + leading_colon: Some(Default::default()), + segments: vec![path_segment("gotham_restful"), path_segment("gotham"), path_segment("hyper"), path_segment("StatusCode"), status_ident].into_iter().collect() + } + } + else { status } + }); + + // the response will come directly from the from_ty if present + let res = match self.from_ty { + Some((from_index, _)) => { + let from_field = &self.fields[from_index].ident; + quote!(#from_field.into_response_error()) + }, + None => quote!(Ok(#krate::Response { + status: { #status }.into(), + body: #krate::gotham::hyper::Body::empty(), + mime: None + })) + }; + + quote! { + #enum_ident::#ident #fields_pat => #res + } + } + + fn were(&self) -> Option + { + match self.from_ty.as_ref() { + Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )), + None => None + } + } +} + +fn expand(tokens : TokenStream) -> Result +{ + let krate = super::krate(); + let input = parse_macro_input::parse::(tokens)?; + let ident = input.ident; + let generics = input.generics; + + let inum = match input.data { + Data::Enum(inum) => Ok(inum), + Data::Struct(strukt) => Err(strukt.struct_token.span()), + Data::Union(uni) => Err(uni.union_token.span()) + }.map_err(|span| Error::new(span, "#[derive(ResourceError)] only works for enums"))?; + let variants = inum.variants.into_iter() + .map(|variant| process_variant(variant)) + .collect_to_result()?; + + let display_impl = if variants.iter().any(|v| v.display.is_none()) { None } else { + let were = generics.params.iter().filter_map(|param| match param { + GenericParam::Type(ty) => { + let ident = &ty.ident; + Some(quote!(#ident : ::std::fmt::Display)) + }, + _ => None + }); + let formatter_ident = format_ident!("resource_error_display_formatter"); + let match_arms = variants.iter() + .map(|v| v.to_display_match_arm(&formatter_ident, &ident)) + .collect_to_result()?; + Some(quote! { + impl #generics ::std::fmt::Display for #ident #generics + where #( #were ),* + { + fn fmt(&self, #formatter_ident: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result + { + match self { + #( #match_arms ),* + } + } + } + }) + }; + + let mut from_impls : Vec = Vec::new(); + + for var in &variants + { + let var_ident = &var.ident; + let (from_index, from_ty) = match var.from_ty.as_ref() { + Some(f) => f, + None => continue + }; + let from_ident = &var.fields[*from_index].ident; + + let fields_pat = var.fields_pat(); + let fields_where = var.fields.iter().enumerate() + .filter(|(i, _)| i != from_index) + .map(|(_, field)| { + let ty = &field.ty; + quote!( #ty : Default ) + }) + .chain(iter::once(quote!( #from_ty : ::std::error::Error ))); + let fields_let = var.fields.iter().enumerate() + .filter(|(i, _)| i != from_index) + .map(|(_, field)| { + let id = &field.ident; + let ty = &field.ty; + quote!( let #id : #ty = Default::default(); ) + }); + + from_impls.push(quote! { + impl #generics ::std::convert::From<#from_ty> for #ident #generics + where #( #fields_where ),* + { + fn from(#from_ident : #from_ty) -> Self + { + #( #fields_let )* + Self::#var_ident #fields_pat + } + } + }); + } + + let were = variants.iter().filter_map(|variant| variant.were()).collect::>(); + let variants = variants.into_iter().map(|variant| variant.into_match_arm(&krate, &ident)); + + Ok(quote! { + #display_impl + + impl #generics #krate::IntoResponseError for #ident #generics + where #( #were ),* + { + type Err = #krate::export::serde_json::Error; + + fn into_response_error(self) -> Result<#krate::Response, Self::Err> + { + match self { + #( #variants ),* + } + } + } + + #( #from_impls )* + }) +} + +pub fn expand_resource_error(tokens : TokenStream) -> TokenStream +{ + expand(tokens) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/gotham_restful_derive/src/util.rs b/gotham_restful_derive/src/util.rs index 319830a..12a4951 100644 --- a/gotham_restful_derive/src/util.rs +++ b/gotham_restful_derive/src/util.rs @@ -1,3 +1,9 @@ +use proc_macro2::{ + Delimiter, + TokenStream as TokenStream2, + TokenTree +}; +use std::iter; use syn::Error; pub trait CollectToResult @@ -25,3 +31,19 @@ where }) } } + + +pub fn remove_parens(input : TokenStream2) -> TokenStream2 +{ + let iter = input.into_iter().flat_map(|tt| { + if let TokenTree::Group(group) = &tt + { + if group.delimiter() == Delimiter::Parenthesis + { + return Box::new(group.stream().into_iter()) as Box>; + } + } + Box::new(iter::once(tt)) + }); + iter.collect() +}