From 31f92c07cdccd4c55a75f70d89eecc3de9aa899a Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Sat, 27 Feb 2021 15:40:34 +0000 Subject: [PATCH] Custom HTTP Headers --- CHANGELOG.md | 2 +- README.md | 21 ++ derive/src/endpoint.rs | 2 +- src/endpoint.rs | 6 +- src/lib.rs | 44 ++- src/openapi/operation.rs | 7 +- src/openapi/router.rs | 2 +- src/response.rs | 84 ------ src/{result => response}/auth_result.rs | 12 +- src/response/mod.rs | 283 +++++++++++++++++++ src/{result => response}/no_content.rs | 52 +++- src/{result => response}/raw.rs | 22 +- src/{result => response}/redirect.rs | 16 +- src/{result => response}/result.rs | 14 +- src/{result => response}/success.rs | 104 +++---- src/result/mod.rs | 197 ------------- src/routing.rs | 7 +- tests/ui/endpoint/invalid_return_type.stderr | 16 +- 18 files changed, 475 insertions(+), 416 deletions(-) delete mode 100644 src/response.rs rename src/{result => response}/auth_result.rs (85%) create mode 100644 src/response/mod.rs rename src/{result => response}/no_content.rs (66%) rename src/{result => response}/raw.rs (85%) rename src/{result => response}/redirect.rs (89%) rename src/{result => response}/result.rs (85%) rename src/{result => response}/success.rs (52%) delete mode 100644 src/result/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ed987..ec4e9a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - All fields of `Response` are now private - If not enabling the `openapi` feature, `without-openapi` has to be enabled - The endpoint macro attributes (`read`, `create`, ...) no longer take the resource ident and reject all unknown attributes ([!18]) - - The `ResourceResult` trait has been split into `ResourceResult` and `ResourceResultSchema` + - The `ResourceResult` trait has been split into `IntoResponse` and `ResponseSchema` - `HashMap`'s keys are included in the generated OpenAPI spec (they defaulted to `type: string` previously) ### Removed diff --git a/README.md b/README.md index 028feea..76da543 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,27 @@ fn create(body : RawImage) -> Raw> { } ``` +## Custom HTTP Headers + +You can read request headers from the state as you would in any other gotham handler, and specify +custom response headers using [Response::header]. + +```rust +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all] +async fn read_all(state: &mut State) -> NoContent { + let headers: &HeaderMap = state.borrow(); + let accept = &headers[ACCEPT]; + + let mut res = NoContent::default(); + res.header(VARY, "accept".parse().unwrap()); + res +} +``` + ## Features To make life easier for common use-cases, this create offers a few features that might be helpful diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index aac3757..f6f143b 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -487,7 +487,7 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn handle_content = quote!(#handle_content.await); } if is_no_content { - handle_content = quote!(#handle_content; ::gotham_restful::NoContent) + handle_content = quote!(#handle_content; <::gotham_restful::NoContent as ::std::default::Default>::default()) } if let Some(arg) = args.iter().find(|arg| arg.ty.is_database_conn()) { diff --git a/src/endpoint.rs b/src/endpoint.rs index f85fdb2..d8da412 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -1,4 +1,4 @@ -use crate::{RequestBody, ResourceResult}; +use crate::{IntoResponse, RequestBody}; use futures_util::future::BoxFuture; use gotham::{ extractor::{PathExtractor, QueryStringExtractor}, @@ -16,8 +16,8 @@ pub trait Endpoint { fn uri() -> Cow<'static, str>; /// The output type that provides the response. - #[openapi_bound("Output: crate::ResourceResultSchema")] - type Output: ResourceResult + Send; + #[openapi_bound("Output: crate::ResponseSchema")] + type Output: IntoResponse + Send; /// Returns `true` _iff_ the URI contains placeholders. `false` by default. fn has_placeholders() -> bool { diff --git a/src/lib.rs b/src/lib.rs index 1f50701..36674c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,6 +158,37 @@ fn create(body : RawImage) -> Raw> { # } ``` +# Custom HTTP Headers + +You can read request headers from the state as you would in any other gotham handler, and specify +custom response headers using [Response::header]. + +```rust,no_run +# #[macro_use] extern crate gotham_restful_derive; +# use gotham::hyper::header::{ACCEPT, HeaderMap, VARY}; +# use gotham::{router::builder::*, state::State}; +# use gotham_restful::*; +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all] +async fn read_all(state: &mut State) -> NoContent { + let headers: &HeaderMap = state.borrow(); + let accept = &headers[ACCEPT]; +# drop(accept); + + let mut res = NoContent::default(); + res.header(VARY, "accept".parse().unwrap()); + res +} +# fn main() { +# gotham::start("127.0.0.1:8080", build_simple_router(|route| { +# route.resource::("foo"); +# })); +# } +``` + # Features To make life easier for common use-cases, this create offers a few features that might be helpful @@ -475,15 +506,12 @@ pub use endpoint::Endpoint; pub use endpoint::EndpointWithSchema; mod response; -pub use response::Response; - -mod result; -pub use result::{ - AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw, Redirect, - ResourceResult, Success +pub use response::{ + AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponse, IntoResponseError, NoContent, + Raw, Redirect, Response, Success }; #[cfg(feature = "openapi")] -pub use result::{ResourceResultSchema, ResourceResultWithSchema}; +pub use response::{IntoResponseWithSchema, ResponseSchema}; mod routing; pub use routing::{DrawResourceRoutes, DrawResources}; @@ -496,7 +524,7 @@ pub use types::*; /// This trait must be implemented for every resource. It allows you to register the different /// endpoints that can be handled by this resource to be registered with the underlying router. /// -/// It is not recommended to implement this yourself, rather just use `#[derive(Resource)]`. +/// It is not recommended to implement this yourself, just use `#[derive(Resource)]`. #[_private_openapi_trait(ResourceWithSchema)] pub trait Resource { /// Register all methods handled by this resource with the underlying router. diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index 85f5e6c..62d06d5 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -1,5 +1,5 @@ use super::SECURITY_NAME; -use crate::{result::*, EndpointWithSchema, OpenapiSchema, RequestBody}; +use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, OpenapiSchema, RequestBody, ResponseSchema}; use indexmap::IndexMap; use mime::Mime; use openapiv3::{ @@ -184,11 +184,12 @@ impl OperationDescription { #[cfg(test)] mod test { use super::*; + use crate::{NoContent, Raw, ResponseSchema}; #[test] fn no_content_schema_to_content() { let types = NoContent::accepted_types(); - let schema = ::schema(); + let schema = ::schema(); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert!(content.is_empty()); } @@ -196,7 +197,7 @@ mod test { #[test] fn raw_schema_to_content() { let types = Raw::<&str>::accepted_types(); - let schema = as ResourceResultSchema>::schema(); + let schema = as ResponseSchema>::schema(); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert_eq!(content.len(), 1); let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 7097cfd..3ced31b 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -3,7 +3,7 @@ use super::{ handler::{OpenapiHandler, SwaggerUiHandler}, operation::OperationDescription }; -use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceResultSchema, ResourceWithSchema}; +use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema, ResponseSchema}; use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; use once_cell::sync::Lazy; use regex::{Captures, Regex}; diff --git a/src/response.rs b/src/response.rs deleted file mode 100644 index 8f378ca..0000000 --- a/src/response.rs +++ /dev/null @@ -1,84 +0,0 @@ -use gotham::hyper::{ - header::{HeaderMap, HeaderName, HeaderValue}, - Body, StatusCode -}; -use mime::{Mime, APPLICATION_JSON}; - -/// A response, used to create the final gotham response from. -#[derive(Debug)] -pub struct Response { - pub(crate) status: StatusCode, - pub(crate) body: Body, - pub(crate) mime: Option, - pub(crate) headers: HeaderMap -} - -impl Response { - /// Create a new [Response] from raw data. - #[must_use = "Creating a response is pointless if you don't use it"] - pub fn new>(status: StatusCode, body: B, mime: Option) -> Self { - Self { - status, - body: body.into(), - mime, - headers: Default::default() - } - } - - /// Create a [Response] with mime type json from already serialized data. - #[must_use = "Creating a response is pointless if you don't use it"] - pub fn json>(status: StatusCode, body: B) -> Self { - Self { - status, - body: body.into(), - mime: Some(APPLICATION_JSON), - headers: Default::default() - } - } - - /// Create a _204 No Content_ [Response]. - #[must_use = "Creating a response is pointless if you don't use it"] - pub fn no_content() -> Self { - Self { - status: StatusCode::NO_CONTENT, - body: Body::empty(), - mime: None, - headers: Default::default() - } - } - - /// Create an empty _403 Forbidden_ [Response]. - #[must_use = "Creating a response is pointless if you don't use it"] - pub fn forbidden() -> Self { - Self { - status: StatusCode::FORBIDDEN, - body: Body::empty(), - mime: None, - headers: Default::default() - } - } - - /// Return the status code of this [Response]. - pub fn status(&self) -> StatusCode { - self.status - } - - /// Return the mime type of this [Response]. - pub fn mime(&self) -> Option<&Mime> { - self.mime.as_ref() - } - - /// Add an HTTP header to the [Response]. - pub fn header(&mut self, name: HeaderName, value: HeaderValue) { - self.headers.insert(name, value); - } - - #[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()) - } -} diff --git a/src/result/auth_result.rs b/src/response/auth_result.rs similarity index 85% rename from src/result/auth_result.rs rename to src/response/auth_result.rs index 1b1fd7f..d8b3300 100644 --- a/src/result/auth_result.rs +++ b/src/response/auth_result.rs @@ -12,9 +12,9 @@ pub enum AuthError { } /** -This return type can be used to map another [ResourceResult](crate::ResourceResult) that can -only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response -will be issued. +This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse) +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): @@ -86,9 +86,9 @@ where } /** -This return type can be used to map another [ResourceResult](crate::ResourceResult) that can -only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response -will be issued. +This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse) +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): diff --git a/src/response/mod.rs b/src/response/mod.rs new file mode 100644 index 0000000..b2796dc --- /dev/null +++ b/src/response/mod.rs @@ -0,0 +1,283 @@ +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; + +use futures_util::future::{self, BoxFuture, FutureExt}; +use gotham::{ + handler::HandlerError, + hyper::{ + header::{HeaderMap, HeaderName, HeaderValue}, + Body, StatusCode + } +}; +use mime::{Mime, APPLICATION_JSON, STAR_STAR}; +use serde::Serialize; +use std::{ + convert::Infallible, + fmt::{Debug, Display}, + future::Future, + 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 redirect; +pub use redirect::Redirect; + +#[allow(clippy::module_inception)] +mod result; +pub use result::IntoResponseError; + +mod success; +pub use success::Success; + +pub(crate) trait OrAllTypes { + fn or_all_types(self) -> Vec; +} + +impl OrAllTypes for Option> { + fn or_all_types(self) -> Vec { + self.unwrap_or_else(|| vec![STAR_STAR]) + } +} + +/// A response, used to create the final gotham response from. +#[derive(Debug)] +pub struct Response { + pub(crate) status: StatusCode, + pub(crate) body: Body, + pub(crate) mime: Option, + pub(crate) headers: HeaderMap +} + +impl Response { + /// Create a new [Response] from raw data. + #[must_use = "Creating a response is pointless if you don't use it"] + pub fn new>(status: StatusCode, body: B, mime: Option) -> Self { + Self { + status, + body: body.into(), + mime, + headers: Default::default() + } + } + + /// Create a [Response] with mime type json from already serialized data. + #[must_use = "Creating a response is pointless if you don't use it"] + pub fn json>(status: StatusCode, body: B) -> Self { + Self { + status, + body: body.into(), + mime: Some(APPLICATION_JSON), + headers: Default::default() + } + } + + /// Create a _204 No Content_ [Response]. + #[must_use = "Creating a response is pointless if you don't use it"] + pub fn no_content() -> Self { + Self { + status: StatusCode::NO_CONTENT, + body: Body::empty(), + mime: None, + headers: Default::default() + } + } + + /// Create an empty _403 Forbidden_ [Response]. + #[must_use = "Creating a response is pointless if you don't use it"] + pub fn forbidden() -> Self { + Self { + status: StatusCode::FORBIDDEN, + body: Body::empty(), + mime: None, + headers: Default::default() + } + } + + /// Return the status code of this [Response]. + pub fn status(&self) -> StatusCode { + self.status + } + + /// Return the mime type of this [Response]. + pub fn mime(&self) -> Option<&Mime> { + self.mime.as_ref() + } + + /// Add an HTTP header to the [Response]. + pub fn header(&mut self, name: HeaderName, value: HeaderValue) { + self.headers.insert(name, value); + } + + pub(crate) fn with_headers(mut self, headers: HeaderMap) -> Self { + self.headers = headers; + self + } + + #[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()) + } +} + +impl IntoResponse for Response { + type Err = Infallible; + + fn into_response(self) -> BoxFuture<'static, Result> { + future::ok(self).boxed() + } +} + +/// This trait needs to be implemented by every type returned from an endpoint to +/// to provide the response. +pub trait IntoResponse { + type Err: Into + Send + Sync + '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) -> BoxFuture<'static, Result>; + + /// Return a list of supported mime types. + fn accepted_types() -> Option> { + None + } +} + +/// Additional details for [IntoResponse] to be used with an OpenAPI-aware router. +#[cfg(feature = "openapi")] +pub trait ResponseSchema { + fn schema() -> OpenapiSchema; + + fn default_status() -> StatusCode { + StatusCode::OK + } +} + +#[cfg(feature = "openapi")] +mod private { + pub trait Sealed {} +} + +/// A trait provided to convert a resource's result to json, and provide an OpenAPI schema to the +/// router. This trait is implemented for all types that implement [IntoResponse] and +/// [ResponseSchema]. +#[cfg(feature = "openapi")] +pub trait IntoResponseWithSchema: IntoResponse + ResponseSchema + private::Sealed {} + +#[cfg(feature = "openapi")] +impl private::Sealed for R {} + +#[cfg(feature = "openapi")] +impl IntoResponseWithSchema for R {} + +/// 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() + } + } +} + +#[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 +{ + let msg = e.to_string(); + let res = e.into_response_error(); + match &res { + Ok(res) if res.status.is_server_error() => errorlog(msg), + Err(err) => { + errorlog(msg); + errorlog(&err); + }, + _ => {} + }; + future::ready(res).boxed() +} + +impl IntoResponse for Pin + Send>> +where + Res: IntoResponse + 'static +{ + type Err = Res::Err; + + fn into_response(self) -> Pin> + Send>> { + self.then(IntoResponse::into_response).boxed() + } + + fn accepted_types() -> Option> { + Res::accepted_types() + } +} + +#[cfg(feature = "openapi")] +impl ResponseSchema for Pin + Send>> +where + Res: ResponseSchema +{ + fn schema() -> OpenapiSchema { + Res::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode { + Res::default_status() + } +} + +#[cfg(test)] +mod test { + use super::*; + use futures_executor::block_on; + use thiserror::Error; + + #[derive(Debug, Default, Deserialize, Serialize)] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + struct Msg { + msg: String + } + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn result_from_future() { + let nc = NoContent::default(); + let res = block_on(nc.into_response()).unwrap(); + + let fut_nc = async move { NoContent::default() }.boxed(); + let fut_res = block_on(fut_nc.into_response()).unwrap(); + + assert_eq!(res.status, fut_res.status); + assert_eq!(res.mime, fut_res.mime); + assert_eq!(res.full_body().unwrap(), fut_res.full_body().unwrap()); + } +} diff --git a/src/result/no_content.rs b/src/response/no_content.rs similarity index 66% rename from src/result/no_content.rs rename to src/response/no_content.rs index e6072ed..73159c1 100644 --- a/src/result/no_content.rs +++ b/src/response/no_content.rs @@ -1,9 +1,9 @@ -use super::{handle_error, ResourceResult}; +use super::{handle_error, IntoResponse}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, OpenapiType, ResourceResultSchema}; - +use crate::{OpenapiSchema, OpenapiType, ResponseSchema}; use futures_util::{future, future::FutureExt}; +use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName}; #[cfg(feature = "openapi")] use gotham::hyper::StatusCode; use mime::Mime; @@ -31,22 +31,36 @@ fn read_all() { # } ``` */ -#[derive(Clone, Copy, Debug, Default)] -pub struct NoContent; +#[derive(Clone, Debug, Default)] +pub struct NoContent { + headers: HeaderMap +} impl From<()> for NoContent { fn from(_: ()) -> Self { - Self {} + Self::default() } } -impl ResourceResult for NoContent { +impl NoContent { + /// Set a custom HTTP header. If a header with this name was set before, its value is being updated. + pub fn header(&mut self, name: K, value: HeaderValue) { + self.headers.insert(name, value); + } + + /// Allow manipulating HTTP headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } +} + +impl IntoResponse 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() + future::ok(Response::no_content().with_headers(self.headers)).boxed() } fn accepted_types() -> Option> { @@ -55,7 +69,7 @@ impl ResourceResult for NoContent { } #[cfg(feature = "openapi")] -impl ResourceResultSchema for NoContent { +impl ResponseSchema for NoContent { /// Returns the schema of the `()` type. fn schema() -> OpenapiSchema { <()>::schema() @@ -67,7 +81,7 @@ impl ResourceResultSchema for NoContent { } } -impl ResourceResult for Result +impl IntoResponse for Result where E: Display + IntoResponseError { @@ -86,12 +100,12 @@ where } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Result +impl ResponseSchema for Result where E: Display + IntoResponseError { fn schema() -> OpenapiSchema { - ::schema() + ::schema() } #[cfg(feature = "openapi")] @@ -104,7 +118,7 @@ where mod test { use super::*; use futures_executor::block_on; - use gotham::hyper::StatusCode; + use gotham::hyper::{header::ACCESS_CONTROL_ALLOW_ORIGIN, StatusCode}; use thiserror::Error; #[derive(Debug, Default, Error)] @@ -118,6 +132,9 @@ mod test { assert_eq!(res.status, StatusCode::NO_CONTENT); assert_eq!(res.mime, None); assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + + #[cfg(feature = "openapi")] + assert_eq!(NoContent::default_status(), StatusCode::NO_CONTENT); } #[test] @@ -128,4 +145,13 @@ mod test { assert_eq!(res.mime, None); assert_eq!(res.full_body().unwrap(), &[] as &[u8]); } + + #[test] + fn no_content_custom_headers() { + let mut no_content = NoContent::default(); + no_content.header(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); + let res = block_on(no_content.into_response()).expect("didn't expect error response"); + let cors = res.headers.get(ACCESS_CONTROL_ALLOW_ORIGIN); + assert_eq!(cors.map(|value| value.to_str().unwrap()), Some("*")); + } } diff --git a/src/result/raw.rs b/src/response/raw.rs similarity index 85% rename from src/result/raw.rs rename to src/response/raw.rs index 759adbb..6c003dc 100644 --- a/src/result/raw.rs +++ b/src/response/raw.rs @@ -1,7 +1,7 @@ -use super::{handle_error, IntoResponseError, ResourceResult}; +use super::{handle_error, IntoResponse, IntoResponseError}; use crate::{FromBody, RequestBody, ResourceType, Response}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, OpenapiType, ResourceResultSchema}; +use crate::{IntoResponseWithSchema, OpenapiSchema, OpenapiType, ResponseSchema}; use futures_core::future::Future; use futures_util::{future, future::FutureExt}; @@ -99,7 +99,7 @@ impl OpenapiType for Raw { } } -impl> ResourceResult for Raw +impl> IntoResponse for Raw where Self: Send { @@ -111,7 +111,7 @@ where } #[cfg(feature = "openapi")] -impl> ResourceResultSchema for Raw +impl> ResponseSchema for Raw where Self: Send { @@ -120,10 +120,10 @@ where } } -impl ResourceResult for Result, E> +impl IntoResponse for Result, E> where - Raw: ResourceResult, - E: Display + IntoResponseError as ResourceResult>::Err> + Raw: IntoResponse, + E: Display + IntoResponseError as IntoResponse>::Err> { type Err = E::Err; @@ -136,13 +136,13 @@ where } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Result, E> +impl ResponseSchema for Result, E> where - Raw: ResourceResult + ResourceResultSchema, - E: Display + IntoResponseError as ResourceResult>::Err> + Raw: IntoResponseWithSchema, + E: Display + IntoResponseError as IntoResponse>::Err> { fn schema() -> OpenapiSchema { - as ResourceResultSchema>::schema() + as ResponseSchema>::schema() } } diff --git a/src/result/redirect.rs b/src/response/redirect.rs similarity index 89% rename from src/result/redirect.rs rename to src/response/redirect.rs index 0a1933c..8b6e854 100644 --- a/src/result/redirect.rs +++ b/src/response/redirect.rs @@ -1,7 +1,7 @@ -use super::{handle_error, ResourceResult}; +use super::{handle_error, IntoResponse}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{NoContent, OpenapiSchema, ResourceResultSchema}; +use crate::{NoContent, OpenapiSchema, ResponseSchema}; use futures_util::future::{BoxFuture, FutureExt, TryFutureExt}; use gotham::hyper::{ header::{InvalidHeaderValue, LOCATION}, @@ -42,7 +42,7 @@ pub struct Redirect { pub to: String } -impl ResourceResult for Redirect { +impl IntoResponse for Redirect { type Err = InvalidHeaderValue; fn into_response(self) -> BoxFuture<'static, Result> { @@ -56,13 +56,13 @@ impl ResourceResult for Redirect { } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Redirect { +impl ResponseSchema for Redirect { fn default_status() -> StatusCode { StatusCode::SEE_OTHER } fn schema() -> OpenapiSchema { - ::schema() + ::schema() } } @@ -76,7 +76,7 @@ pub enum RedirectError { } #[allow(ambiguous_associated_items)] // an enum variant is not a type. never. -impl ResourceResult for Result +impl IntoResponse for Result where E: Display + IntoResponseError, ::Err: StdError + Sync @@ -92,7 +92,7 @@ where } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Result +impl ResponseSchema for Result where E: Display + IntoResponseError, ::Err: StdError + Sync @@ -102,7 +102,7 @@ where } fn schema() -> OpenapiSchema { - ::schema() + ::schema() } } diff --git a/src/result/result.rs b/src/response/result.rs similarity index 85% rename from src/result/result.rs rename to src/response/result.rs index deaa45b..a28803f 100644 --- a/src/result/result.rs +++ b/src/response/result.rs @@ -1,7 +1,7 @@ -use super::{handle_error, into_response_helper, ResourceResult}; -use crate::{result::ResourceError, Response, ResponseBody}; +use super::{handle_error, IntoResponse, ResourceError}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, ResourceResultSchema}; +use crate::{OpenapiSchema, ResponseSchema}; +use crate::{Response, ResponseBody, Success}; use futures_core::future::Future; use gotham::hyper::StatusCode; @@ -26,7 +26,7 @@ impl IntoResponseError for E { } } -impl ResourceResult for Result +impl IntoResponse for Result where R: ResponseBody, E: Display + IntoResponseError @@ -35,7 +35,7 @@ where fn into_response(self) -> Pin> + Send>> { match self { - Ok(r) => into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&r)?))), + Ok(r) => Success::from(r).into_response(), Err(e) => handle_error(e) } } @@ -46,7 +46,7 @@ where } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Result +impl ResponseSchema for Result where R: ResponseBody, E: Display + IntoResponseError @@ -59,7 +59,7 @@ where #[cfg(test)] mod test { use super::*; - use crate::result::OrAllTypes; + use crate::response::OrAllTypes; use futures_executor::block_on; use thiserror::Error; diff --git a/src/result/success.rs b/src/response/success.rs similarity index 52% rename from src/result/success.rs rename to src/response/success.rs index 3c1bcaa..24d6b3c 100644 --- a/src/result/success.rs +++ b/src/response/success.rs @@ -1,19 +1,17 @@ -use super::{into_response_helper, ResourceResult}; +use super::IntoResponse; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, ResourceResultSchema}; +use crate::{OpenapiSchema, ResponseSchema}; use crate::{Response, ResponseBody}; -use gotham::hyper::StatusCode; -use mime::{Mime, APPLICATION_JSON}; -use std::{ - fmt::Debug, - future::Future, - ops::{Deref, DerefMut}, - pin::Pin +use futures_util::future::{self, FutureExt}; +use gotham::hyper::{ + header::{HeaderMap, HeaderValue, IntoHeaderName}, + StatusCode }; +use mime::{Mime, APPLICATION_JSON}; +use std::{fmt::Debug, future::Future, pin::Pin}; /** -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. +This can be returned from a resource when there is no cause of an error. Usage example: @@ -42,63 +40,40 @@ fn read_all() -> Success { # } ``` */ -#[derive(Debug)] -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 - } +#[derive(Clone, Debug, Default)] +pub struct Success { + value: T, + headers: HeaderMap } impl From for Success { fn from(t: T) -> Self { - Self(t) + Self { + value: t, + headers: HeaderMap::new() + } } } -impl Clone for Success { - fn clone(&self) -> Self { - Self(self.0.clone()) +impl Success { + /// Set a custom HTTP header. If a header with this name was set before, its value is being updated. + pub fn header(&mut self, name: K, value: HeaderValue) { + self.headers.insert(name, value); + } + + /// Allow manipulating HTTP headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers } } -impl Copy for Success {} - -impl Default for Success { - fn default() -> Self { - Self(T::default()) - } -} - -impl ResourceResult for Success -where - Self: Send -{ +impl IntoResponse for Success { 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())?))) + let res = + serde_json::to_string(&self.value).map(|body| Response::json(StatusCode::OK, body).with_headers(self.headers)); + future::ready(res).boxed() } fn accepted_types() -> Option> { @@ -107,10 +82,7 @@ where } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Success -where - Self: Send -{ +impl ResponseSchema for Success { fn schema() -> OpenapiSchema { T::schema() } @@ -119,8 +91,9 @@ where #[cfg(test)] mod test { use super::*; - use crate::result::OrAllTypes; + use crate::response::OrAllTypes; use futures_executor::block_on; + use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN; #[derive(Debug, Default, Serialize)] #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] @@ -135,6 +108,17 @@ mod test { assert_eq!(res.status, StatusCode::OK); assert_eq!(res.mime, Some(APPLICATION_JSON)); assert_eq!(res.full_body().unwrap(), br#"{"msg":""}"#); + #[cfg(feature = "openapi")] + assert_eq!(>::default_status(), StatusCode::OK); + } + + #[test] + fn success_custom_headers() { + let mut success: Success = Msg::default().into(); + success.header(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); + let res = block_on(success.into_response()).expect("didn't expect error response"); + let cors = res.headers.get(ACCESS_CONTROL_ALLOW_ORIGIN); + assert_eq!(cors.map(|value| value.to_str().unwrap()), Some("*")); } #[test] diff --git a/src/result/mod.rs b/src/result/mod.rs deleted file mode 100644 index a119a82..0000000 --- a/src/result/mod.rs +++ /dev/null @@ -1,197 +0,0 @@ -#[cfg(feature = "openapi")] -use crate::OpenapiSchema; -use crate::Response; - -use futures_util::future::{BoxFuture, FutureExt}; -use gotham::handler::HandlerError; -#[cfg(feature = "openapi")] -use gotham::hyper::StatusCode; -use mime::{Mime, STAR_STAR}; -use serde::Serialize; -use std::{ - fmt::{Debug, Display}, - future::Future, - 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 redirect; -pub use redirect::Redirect; - -#[allow(clippy::module_inception)] -mod result; -pub use result::IntoResponseError; - -mod success; -pub use success::Success; - -pub(crate) trait OrAllTypes { - fn or_all_types(self) -> Vec; -} - -impl OrAllTypes for Option> { - fn or_all_types(self) -> Vec { - self.unwrap_or_else(|| vec![STAR_STAR]) - } -} - -/// A trait provided to convert a resource's result to json. -pub trait ResourceResult { - type Err: Into + Send + Sync + '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) -> BoxFuture<'static, Result>; - - /// Return a list of supported mime types. - fn accepted_types() -> Option> { - None - } -} - -/// Additional details for [ResourceResult] to be used with an OpenAPI-aware router. -#[cfg(feature = "openapi")] -pub trait ResourceResultSchema { - fn schema() -> OpenapiSchema; - - fn default_status() -> StatusCode { - StatusCode::OK - } -} - -#[cfg(feature = "openapi")] -mod private { - pub trait Sealed {} -} - -/// A trait provided to convert a resource's result to json, and provide an OpenAPI schema to the -/// router. This trait is implemented for all types that implement [ResourceResult] and -/// [ResourceResultSchema]. -#[cfg(feature = "openapi")] -pub trait ResourceResultWithSchema: ResourceResult + ResourceResultSchema + private::Sealed {} - -#[cfg(feature = "openapi")] -impl private::Sealed for R {} - -#[cfg(feature = "openapi")] -impl ResourceResultWithSchema for R {} - -/// 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(|| { - let msg = e.to_string(); - let res = e.into_response_error(); - match &res { - Ok(res) if res.status.is_server_error() => errorlog(msg), - Err(err) => { - errorlog(msg); - errorlog(&err); - }, - _ => {} - }; - res - }) -} - -impl ResourceResult for Pin + Send>> -where - Res: ResourceResult + 'static -{ - type Err = Res::Err; - - fn into_response(self) -> Pin> + Send>> { - self.then(ResourceResult::into_response).boxed() - } - - fn accepted_types() -> Option> { - Res::accepted_types() - } -} - -#[cfg(feature = "openapi")] -impl ResourceResultSchema for Pin + Send>> -where - Res: ResourceResultSchema -{ - fn schema() -> OpenapiSchema { - Res::schema() - } - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode { - Res::default_status() - } -} - -#[cfg(test)] -mod test { - use super::*; - use futures_executor::block_on; - use thiserror::Error; - - #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] - struct Msg { - msg: String - } - - #[derive(Debug, Default, Error)] - #[error("An Error")] - struct MsgError; - - #[test] - fn result_from_future() { - let nc = NoContent::default(); - let res = block_on(nc.into_response()).unwrap(); - - let fut_nc = async move { NoContent::default() }.boxed(); - let fut_res = block_on(fut_nc.into_response()).unwrap(); - - assert_eq!(res.status, fut_res.status); - assert_eq!(res.mime, fut_res.mime); - assert_eq!(res.full_body().unwrap(), fut_res.full_body().unwrap()); - } -} diff --git a/src/routing.rs b/src/routing.rs index 132eed6..f41dc93 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -3,10 +3,7 @@ use crate::openapi::{ builder::{OpenapiBuilder, OpenapiInfo}, router::OpenapiRouter }; -use crate::{ - result::{ResourceError, ResourceResult}, - Endpoint, FromBody, Resource, Response -}; +use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response}; #[cfg(feature = "cors")] use gotham::router::route::matcher::AccessControlRequestMethodMatcher; @@ -90,7 +87,7 @@ fn response_from(res: Response, state: &State) -> gotham::hyper::Response async fn endpoint_handler(state: &mut State) -> Result, HandlerError> where E: Endpoint, - ::Err: Into + ::Err: Into { trace!("entering endpoint_handler"); let placeholders = E::Placeholders::take_from(state); diff --git a/tests/ui/endpoint/invalid_return_type.stderr b/tests/ui/endpoint/invalid_return_type.stderr index 2879898..69d5f39 100644 --- a/tests/ui/endpoint/invalid_return_type.stderr +++ b/tests/ui/endpoint/invalid_return_type.stderr @@ -1,21 +1,21 @@ -error[E0277]: the trait bound `FooResponse: ResourceResultSchema` is not satisfied +error[E0277]: the trait bound `FooResponse: ResponseSchema` is not satisfied --> $DIR/invalid_return_type.rs:12:18 | 12 | fn endpoint() -> FooResponse { - | ^^^^^^^^^^^ the trait `ResourceResultSchema` is not implemented for `FooResponse` + | ^^^^^^^^^^^ the trait `ResponseSchema` is not implemented for `FooResponse` | ::: $WORKSPACE/src/endpoint.rs | - | #[openapi_bound("Output: crate::ResourceResultSchema")] - | ------------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Output` + | #[openapi_bound("Output: crate::ResponseSchema")] + | ------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Output` -error[E0277]: the trait bound `FooResponse: ResourceResult` is not satisfied +error[E0277]: the trait bound `FooResponse: gotham_restful::IntoResponse` is not satisfied --> $DIR/invalid_return_type.rs:12:18 | 12 | fn endpoint() -> FooResponse { - | ^^^^^^^^^^^ the trait `ResourceResult` is not implemented for `FooResponse` + | ^^^^^^^^^^^ the trait `gotham_restful::IntoResponse` is not implemented for `FooResponse` | ::: $WORKSPACE/src/endpoint.rs | - | type Output: ResourceResult + Send; - | -------------- required by this bound in `gotham_restful::EndpointWithSchema::Output` + | type Output: IntoResponse + Send; + | ------------ required by this bound in `gotham_restful::EndpointWithSchema::Output`