1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-02-22 12:42:28 +00:00

Custom HTTP Headers

This commit is contained in:
msrd0 2021-02-27 15:40:34 +00:00
parent 28ae4dfdee
commit 31f92c07cd
18 changed files with 475 additions and 416 deletions

View file

@ -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

View file

@ -148,6 +148,27 @@ fn create(body : RawImage) -> Raw<Vec<u8>> {
}
```
## 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

View file

@ -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()) {

View file

@ -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 {

View file

@ -158,6 +158,37 @@ fn create(body : RawImage) -> Raw<Vec<u8>> {
# }
```
# 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::<FooResource>("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.

View file

@ -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 = <NoContent as ResourceResultSchema>::schema();
let schema = <NoContent as ResponseSchema>::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 = <Raw<&str> as ResourceResultSchema>::schema();
let schema = <Raw<&str> 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();

View file

@ -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};

View file

@ -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<Mime>,
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<B: Into<Body>>(status: StatusCode, body: B, mime: Option<Mime>) -> 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<B: Into<Body>>(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<Vec<u8>, <Body as gotham::hyper::body::HttpBody>::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())
}
}

View file

@ -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):

283
src/response/mod.rs Normal file
View file

@ -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<Mime>;
}
impl OrAllTypes for Option<Vec<Mime>> {
fn or_all_types(self) -> Vec<Mime> {
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<Mime>,
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<B: Into<Body>>(status: StatusCode, body: B, mime: Option<Mime>) -> 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<B: Into<Body>>(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<Vec<u8>, <Body as gotham::hyper::body::HttpBody>::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<Response, Self::Err>> {
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<HandlerError> + 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<Response, Self::Err>>;
/// Return a list of supported mime types.
fn accepted_types() -> Option<Vec<Mime>> {
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<R: IntoResponse + ResponseSchema> private::Sealed for R {}
#[cfg(feature = "openapi")]
impl<R: IntoResponse + ResponseSchema> 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<T: ToString> From<T> for ResourceError {
fn from(message: T) -> Self {
Self {
error: true,
message: message.to_string()
}
}
}
#[cfg(feature = "errorlog")]
fn errorlog<E: Display>(e: E) {
error!("The handler encountered an error: {}", e);
}
#[cfg(not(feature = "errorlog"))]
fn errorlog<E>(_e: E) {}
fn handle_error<E>(e: E) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + 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<Res> IntoResponse for Pin<Box<dyn Future<Output = Res> + Send>>
where
Res: IntoResponse + 'static
{
type Err = Res::Err;
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
self.then(IntoResponse::into_response).boxed()
}
fn accepted_types() -> Option<Vec<Mime>> {
Res::accepted_types()
}
}
#[cfg(feature = "openapi")]
impl<Res> ResponseSchema for Pin<Box<dyn Future<Output = Res> + 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());
}
}

View file

@ -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<K: IntoHeaderName>(&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<NoContent, E>`
/// This will always be a _204 No Content_ together with an empty string.
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
future::ok(Response::no_content()).boxed()
future::ok(Response::no_content().with_headers(self.headers)).boxed()
}
fn accepted_types() -> Option<Vec<Mime>> {
@ -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<E> ResourceResult for Result<NoContent, E>
impl<E> IntoResponse for Result<NoContent, E>
where
E: Display + IntoResponseError<Err = serde_json::Error>
{
@ -86,12 +100,12 @@ where
}
#[cfg(feature = "openapi")]
impl<E> ResourceResultSchema for Result<NoContent, E>
impl<E> ResponseSchema for Result<NoContent, E>
where
E: Display + IntoResponseError<Err = serde_json::Error>
{
fn schema() -> OpenapiSchema {
<NoContent as ResourceResultSchema>::schema()
<NoContent as ResponseSchema>::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("*"));
}
}

View file

@ -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<T> OpenapiType for Raw<T> {
}
}
impl<T: Into<Body>> ResourceResult for Raw<T>
impl<T: Into<Body>> IntoResponse for Raw<T>
where
Self: Send
{
@ -111,7 +111,7 @@ where
}
#[cfg(feature = "openapi")]
impl<T: Into<Body>> ResourceResultSchema for Raw<T>
impl<T: Into<Body>> ResponseSchema for Raw<T>
where
Self: Send
{
@ -120,10 +120,10 @@ where
}
}
impl<T, E> ResourceResult for Result<Raw<T>, E>
impl<T, E> IntoResponse for Result<Raw<T>, E>
where
Raw<T>: ResourceResult,
E: Display + IntoResponseError<Err = <Raw<T> as ResourceResult>::Err>
Raw<T>: IntoResponse,
E: Display + IntoResponseError<Err = <Raw<T> as IntoResponse>::Err>
{
type Err = E::Err;
@ -136,13 +136,13 @@ where
}
#[cfg(feature = "openapi")]
impl<T, E> ResourceResultSchema for Result<Raw<T>, E>
impl<T, E> ResponseSchema for Result<Raw<T>, E>
where
Raw<T>: ResourceResult + ResourceResultSchema,
E: Display + IntoResponseError<Err = <Raw<T> as ResourceResult>::Err>
Raw<T>: IntoResponseWithSchema,
E: Display + IntoResponseError<Err = <Raw<T> as IntoResponse>::Err>
{
fn schema() -> OpenapiSchema {
<Raw<T> as ResourceResultSchema>::schema()
<Raw<T> as ResponseSchema>::schema()
}
}

View file

@ -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<Response, Self::Err>> {
@ -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 {
<NoContent as ResourceResultSchema>::schema()
<NoContent as ResponseSchema>::schema()
}
}
@ -76,7 +76,7 @@ pub enum RedirectError<E: StdError + 'static> {
}
#[allow(ambiguous_associated_items)] // an enum variant is not a type. never.
impl<E> ResourceResult for Result<Redirect, E>
impl<E> IntoResponse for Result<Redirect, E>
where
E: Display + IntoResponseError,
<E as IntoResponseError>::Err: StdError + Sync
@ -92,7 +92,7 @@ where
}
#[cfg(feature = "openapi")]
impl<E> ResourceResultSchema for Result<Redirect, E>
impl<E> ResponseSchema for Result<Redirect, E>
where
E: Display + IntoResponseError,
<E as IntoResponseError>::Err: StdError + Sync
@ -102,7 +102,7 @@ where
}
fn schema() -> OpenapiSchema {
<Redirect as ResourceResultSchema>::schema()
<Redirect as ResponseSchema>::schema()
}
}

View file

@ -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<E: Error> IntoResponseError for E {
}
}
impl<R, E> ResourceResult for Result<R, E>
impl<R, E> IntoResponse for Result<R, E>
where
R: ResponseBody,
E: Display + IntoResponseError<Err = serde_json::Error>
@ -35,7 +35,7 @@ where
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + 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<R, E> ResourceResultSchema for Result<R, E>
impl<R, E> ResponseSchema for Result<R, E>
where
R: ResponseBody,
E: Display + IntoResponseError<Err = serde_json::Error>
@ -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;

View file

@ -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<MyResponse> {
# }
```
*/
#[derive(Debug)]
pub struct Success<T>(T);
impl<T> AsMut<T> for Success<T> {
fn as_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T> AsRef<T> for Success<T> {
fn as_ref(&self) -> &T {
&self.0
}
}
impl<T> Deref for Success<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> DerefMut for Success<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
#[derive(Clone, Debug, Default)]
pub struct Success<T> {
value: T,
headers: HeaderMap
}
impl<T> From<T> for Success<T> {
fn from(t: T) -> Self {
Self(t)
Self {
value: t,
headers: HeaderMap::new()
}
}
}
impl<T: Clone> Clone for Success<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
impl<T> Success<T> {
/// Set a custom HTTP header. If a header with this name was set before, its value is being updated.
pub fn header<K: IntoHeaderName>(&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<T: Copy> Copy for Success<T> {}
impl<T: Default> Default for Success<T> {
fn default() -> Self {
Self(T::default())
}
}
impl<T: ResponseBody> ResourceResult for Success<T>
where
Self: Send
{
impl<T: ResponseBody> IntoResponse for Success<T> {
type Err = serde_json::Error;
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + 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<Vec<Mime>> {
@ -107,10 +82,7 @@ where
}
#[cfg(feature = "openapi")]
impl<T: ResponseBody> ResourceResultSchema for Success<T>
where
Self: Send
{
impl<T: ResponseBody> ResponseSchema for Success<T> {
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!(<Success<Msg>>::default_status(), StatusCode::OK);
}
#[test]
fn success_custom_headers() {
let mut success: Success<Msg> = 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]

View file

@ -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<Mime>;
}
impl OrAllTypes for Option<Vec<Mime>> {
fn or_all_types(self) -> Vec<Mime> {
self.unwrap_or_else(|| vec![STAR_STAR])
}
}
/// A trait provided to convert a resource's result to json.
pub trait ResourceResult {
type Err: Into<HandlerError> + 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<Response, Self::Err>>;
/// Return a list of supported mime types.
fn accepted_types() -> Option<Vec<Mime>> {
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<R: ResourceResult + ResourceResultSchema> private::Sealed for R {}
#[cfg(feature = "openapi")]
impl<R: ResourceResult + ResourceResultSchema> 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<T: ToString> From<T> for ResourceError {
fn from(message: T) -> Self {
Self {
error: true,
message: message.to_string()
}
}
}
fn into_response_helper<Err, F>(create_response: F) -> Pin<Box<dyn Future<Output = Result<Response, Err>> + Send>>
where
Err: Send + 'static,
F: FnOnce() -> Result<Response, Err>
{
let res = create_response();
async move { res }.boxed()
}
#[cfg(feature = "errorlog")]
fn errorlog<E: Display>(e: E) {
error!("The handler encountered an error: {}", e);
}
#[cfg(not(feature = "errorlog"))]
fn errorlog<E>(_e: E) {}
fn handle_error<E>(e: E) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + 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<Res> ResourceResult for Pin<Box<dyn Future<Output = Res> + Send>>
where
Res: ResourceResult + 'static
{
type Err = Res::Err;
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
self.then(ResourceResult::into_response).boxed()
}
fn accepted_types() -> Option<Vec<Mime>> {
Res::accepted_types()
}
}
#[cfg(feature = "openapi")]
impl<Res> ResourceResultSchema for Pin<Box<dyn Future<Output = Res> + 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());
}
}

View file

@ -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<Body>
async fn endpoint_handler<E: Endpoint>(state: &mut State) -> Result<gotham::hyper::Response<Body>, HandlerError>
where
E: Endpoint,
<E::Output as ResourceResult>::Err: Into<HandlerError>
<E::Output as IntoResponse>::Err: Into<HandlerError>
{
trace!("entering endpoint_handler");
let placeholders = E::Placeholders::take_from(state);

View file

@ -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`