mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-02-23 04:52:28 +00:00
Custom HTTP Headers
This commit is contained in:
parent
28ae4dfdee
commit
31f92c07cd
18 changed files with 475 additions and 416 deletions
|
@ -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
|
- All fields of `Response` are now private
|
||||||
- If not enabling the `openapi` feature, `without-openapi` has to be enabled
|
- 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 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)
|
- `HashMap`'s keys are included in the generated OpenAPI spec (they defaulted to `type: string` previously)
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
21
README.md
21
README.md
|
@ -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
|
## Features
|
||||||
|
|
||||||
To make life easier for common use-cases, this create offers a few features that might be helpful
|
To make life easier for common use-cases, this create offers a few features that might be helpful
|
||||||
|
|
|
@ -487,7 +487,7 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn
|
||||||
handle_content = quote!(#handle_content.await);
|
handle_content = quote!(#handle_content.await);
|
||||||
}
|
}
|
||||||
if is_no_content {
|
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()) {
|
if let Some(arg) = args.iter().find(|arg| arg.ty.is_database_conn()) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{RequestBody, ResourceResult};
|
use crate::{IntoResponse, RequestBody};
|
||||||
use futures_util::future::BoxFuture;
|
use futures_util::future::BoxFuture;
|
||||||
use gotham::{
|
use gotham::{
|
||||||
extractor::{PathExtractor, QueryStringExtractor},
|
extractor::{PathExtractor, QueryStringExtractor},
|
||||||
|
@ -16,8 +16,8 @@ pub trait Endpoint {
|
||||||
fn uri() -> Cow<'static, str>;
|
fn uri() -> Cow<'static, str>;
|
||||||
|
|
||||||
/// The output type that provides the response.
|
/// The output type that provides the response.
|
||||||
#[openapi_bound("Output: crate::ResourceResultSchema")]
|
#[openapi_bound("Output: crate::ResponseSchema")]
|
||||||
type Output: ResourceResult + Send;
|
type Output: IntoResponse + Send;
|
||||||
|
|
||||||
/// Returns `true` _iff_ the URI contains placeholders. `false` by default.
|
/// Returns `true` _iff_ the URI contains placeholders. `false` by default.
|
||||||
fn has_placeholders() -> bool {
|
fn has_placeholders() -> bool {
|
||||||
|
|
44
src/lib.rs
44
src/lib.rs
|
@ -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
|
# Features
|
||||||
|
|
||||||
To make life easier for common use-cases, this create offers a few features that might be helpful
|
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;
|
pub use endpoint::EndpointWithSchema;
|
||||||
|
|
||||||
mod response;
|
mod response;
|
||||||
pub use response::Response;
|
pub use response::{
|
||||||
|
AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponse, IntoResponseError, NoContent,
|
||||||
mod result;
|
Raw, Redirect, Response, Success
|
||||||
pub use result::{
|
|
||||||
AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw, Redirect,
|
|
||||||
ResourceResult, Success
|
|
||||||
};
|
};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub use result::{ResourceResultSchema, ResourceResultWithSchema};
|
pub use response::{IntoResponseWithSchema, ResponseSchema};
|
||||||
|
|
||||||
mod routing;
|
mod routing;
|
||||||
pub use routing::{DrawResourceRoutes, DrawResources};
|
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
|
/// 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.
|
/// 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)]
|
#[_private_openapi_trait(ResourceWithSchema)]
|
||||||
pub trait Resource {
|
pub trait Resource {
|
||||||
/// Register all methods handled by this resource with the underlying router.
|
/// Register all methods handled by this resource with the underlying router.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::SECURITY_NAME;
|
use super::SECURITY_NAME;
|
||||||
use crate::{result::*, EndpointWithSchema, OpenapiSchema, RequestBody};
|
use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, OpenapiSchema, RequestBody, ResponseSchema};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use openapiv3::{
|
use openapiv3::{
|
||||||
|
@ -184,11 +184,12 @@ impl OperationDescription {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::{NoContent, Raw, ResponseSchema};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_content_schema_to_content() {
|
fn no_content_schema_to_content() {
|
||||||
let types = NoContent::accepted_types();
|
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()));
|
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
|
||||||
assert!(content.is_empty());
|
assert!(content.is_empty());
|
||||||
}
|
}
|
||||||
|
@ -196,7 +197,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn raw_schema_to_content() {
|
fn raw_schema_to_content() {
|
||||||
let types = Raw::<&str>::accepted_types();
|
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()));
|
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
|
||||||
assert_eq!(content.len(), 1);
|
assert_eq!(content.len(), 1);
|
||||||
let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap();
|
let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap();
|
||||||
|
|
|
@ -3,7 +3,7 @@ use super::{
|
||||||
handler::{OpenapiHandler, SwaggerUiHandler},
|
handler::{OpenapiHandler, SwaggerUiHandler},
|
||||||
operation::OperationDescription
|
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 gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,9 +12,9 @@ pub enum AuthError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This return type can be used to map another [ResourceResult](crate::ResourceResult) that can
|
This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse)
|
||||||
only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response
|
that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_
|
||||||
will be issued.
|
response will be issued.
|
||||||
|
|
||||||
Use can look something like this (assuming the `auth` feature is enabled):
|
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
|
This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse)
|
||||||
only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response
|
that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_
|
||||||
will be issued.
|
response will be issued.
|
||||||
|
|
||||||
Use can look something like this (assuming the `auth` feature is enabled):
|
Use can look something like this (assuming the `auth` feature is enabled):
|
||||||
|
|
283
src/response/mod.rs
Normal file
283
src/response/mod.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
use super::{handle_error, ResourceResult};
|
use super::{handle_error, IntoResponse};
|
||||||
use crate::{IntoResponseError, Response};
|
use crate::{IntoResponseError, Response};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::{OpenapiSchema, OpenapiType, ResourceResultSchema};
|
use crate::{OpenapiSchema, OpenapiType, ResponseSchema};
|
||||||
|
|
||||||
use futures_util::{future, future::FutureExt};
|
use futures_util::{future, future::FutureExt};
|
||||||
|
use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use gotham::hyper::StatusCode;
|
use gotham::hyper::StatusCode;
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
|
@ -31,22 +31,36 @@ fn read_all() {
|
||||||
# }
|
# }
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct NoContent;
|
pub struct NoContent {
|
||||||
|
headers: HeaderMap
|
||||||
|
}
|
||||||
|
|
||||||
impl From<()> for NoContent {
|
impl From<()> for NoContent {
|
||||||
fn from(_: ()) -> Self {
|
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
|
// TODO this shouldn't be a serde_json::Error
|
||||||
type Err = serde_json::Error; // just for easier handling of `Result<NoContent, E>`
|
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.
|
/// 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>> {
|
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>> {
|
fn accepted_types() -> Option<Vec<Mime>> {
|
||||||
|
@ -55,7 +69,7 @@ impl ResourceResult for NoContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
impl ResourceResultSchema for NoContent {
|
impl ResponseSchema for NoContent {
|
||||||
/// Returns the schema of the `()` type.
|
/// Returns the schema of the `()` type.
|
||||||
fn schema() -> OpenapiSchema {
|
fn schema() -> OpenapiSchema {
|
||||||
<()>::schema()
|
<()>::schema()
|
||||||
|
@ -67,7 +81,7 @@ impl ResourceResultSchema for NoContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> ResourceResult for Result<NoContent, E>
|
impl<E> IntoResponse for Result<NoContent, E>
|
||||||
where
|
where
|
||||||
E: Display + IntoResponseError<Err = serde_json::Error>
|
E: Display + IntoResponseError<Err = serde_json::Error>
|
||||||
{
|
{
|
||||||
|
@ -86,12 +100,12 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
impl<E> ResourceResultSchema for Result<NoContent, E>
|
impl<E> ResponseSchema for Result<NoContent, E>
|
||||||
where
|
where
|
||||||
E: Display + IntoResponseError<Err = serde_json::Error>
|
E: Display + IntoResponseError<Err = serde_json::Error>
|
||||||
{
|
{
|
||||||
fn schema() -> OpenapiSchema {
|
fn schema() -> OpenapiSchema {
|
||||||
<NoContent as ResourceResultSchema>::schema()
|
<NoContent as ResponseSchema>::schema()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
|
@ -104,7 +118,7 @@ where
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use futures_executor::block_on;
|
use futures_executor::block_on;
|
||||||
use gotham::hyper::StatusCode;
|
use gotham::hyper::{header::ACCESS_CONTROL_ALLOW_ORIGIN, StatusCode};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Default, Error)]
|
#[derive(Debug, Default, Error)]
|
||||||
|
@ -118,6 +132,9 @@ mod test {
|
||||||
assert_eq!(res.status, StatusCode::NO_CONTENT);
|
assert_eq!(res.status, StatusCode::NO_CONTENT);
|
||||||
assert_eq!(res.mime, None);
|
assert_eq!(res.mime, None);
|
||||||
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
assert_eq!(NoContent::default_status(), StatusCode::NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -128,4 +145,13 @@ mod test {
|
||||||
assert_eq!(res.mime, None);
|
assert_eq!(res.mime, None);
|
||||||
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
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("*"));
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
use super::{handle_error, IntoResponseError, ResourceResult};
|
use super::{handle_error, IntoResponse, IntoResponseError};
|
||||||
use crate::{FromBody, RequestBody, ResourceType, Response};
|
use crate::{FromBody, RequestBody, ResourceType, Response};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::{OpenapiSchema, OpenapiType, ResourceResultSchema};
|
use crate::{IntoResponseWithSchema, OpenapiSchema, OpenapiType, ResponseSchema};
|
||||||
|
|
||||||
use futures_core::future::Future;
|
use futures_core::future::Future;
|
||||||
use futures_util::{future, future::FutureExt};
|
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
|
where
|
||||||
Self: Send
|
Self: Send
|
||||||
{
|
{
|
||||||
|
@ -111,7 +111,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
impl<T: Into<Body>> ResourceResultSchema for Raw<T>
|
impl<T: Into<Body>> ResponseSchema for Raw<T>
|
||||||
where
|
where
|
||||||
Self: Send
|
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
|
where
|
||||||
Raw<T>: ResourceResult,
|
Raw<T>: IntoResponse,
|
||||||
E: Display + IntoResponseError<Err = <Raw<T> as ResourceResult>::Err>
|
E: Display + IntoResponseError<Err = <Raw<T> as IntoResponse>::Err>
|
||||||
{
|
{
|
||||||
type Err = E::Err;
|
type Err = E::Err;
|
||||||
|
|
||||||
|
@ -136,13 +136,13 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
impl<T, E> ResourceResultSchema for Result<Raw<T>, E>
|
impl<T, E> ResponseSchema for Result<Raw<T>, E>
|
||||||
where
|
where
|
||||||
Raw<T>: ResourceResult + ResourceResultSchema,
|
Raw<T>: IntoResponseWithSchema,
|
||||||
E: Display + IntoResponseError<Err = <Raw<T> as ResourceResult>::Err>
|
E: Display + IntoResponseError<Err = <Raw<T> as IntoResponse>::Err>
|
||||||
{
|
{
|
||||||
fn schema() -> OpenapiSchema {
|
fn schema() -> OpenapiSchema {
|
||||||
<Raw<T> as ResourceResultSchema>::schema()
|
<Raw<T> as ResponseSchema>::schema()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use super::{handle_error, ResourceResult};
|
use super::{handle_error, IntoResponse};
|
||||||
use crate::{IntoResponseError, Response};
|
use crate::{IntoResponseError, Response};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::{NoContent, OpenapiSchema, ResourceResultSchema};
|
use crate::{NoContent, OpenapiSchema, ResponseSchema};
|
||||||
use futures_util::future::{BoxFuture, FutureExt, TryFutureExt};
|
use futures_util::future::{BoxFuture, FutureExt, TryFutureExt};
|
||||||
use gotham::hyper::{
|
use gotham::hyper::{
|
||||||
header::{InvalidHeaderValue, LOCATION},
|
header::{InvalidHeaderValue, LOCATION},
|
||||||
|
@ -42,7 +42,7 @@ pub struct Redirect {
|
||||||
pub to: String
|
pub to: String
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceResult for Redirect {
|
impl IntoResponse for Redirect {
|
||||||
type Err = InvalidHeaderValue;
|
type Err = InvalidHeaderValue;
|
||||||
|
|
||||||
fn into_response(self) -> BoxFuture<'static, Result<Response, Self::Err>> {
|
fn into_response(self) -> BoxFuture<'static, Result<Response, Self::Err>> {
|
||||||
|
@ -56,13 +56,13 @@ impl ResourceResult for Redirect {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
impl ResourceResultSchema for Redirect {
|
impl ResponseSchema for Redirect {
|
||||||
fn default_status() -> StatusCode {
|
fn default_status() -> StatusCode {
|
||||||
StatusCode::SEE_OTHER
|
StatusCode::SEE_OTHER
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schema() -> OpenapiSchema {
|
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.
|
#[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
|
where
|
||||||
E: Display + IntoResponseError,
|
E: Display + IntoResponseError,
|
||||||
<E as IntoResponseError>::Err: StdError + Sync
|
<E as IntoResponseError>::Err: StdError + Sync
|
||||||
|
@ -92,7 +92,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
impl<E> ResourceResultSchema for Result<Redirect, E>
|
impl<E> ResponseSchema for Result<Redirect, E>
|
||||||
where
|
where
|
||||||
E: Display + IntoResponseError,
|
E: Display + IntoResponseError,
|
||||||
<E as IntoResponseError>::Err: StdError + Sync
|
<E as IntoResponseError>::Err: StdError + Sync
|
||||||
|
@ -102,7 +102,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schema() -> OpenapiSchema {
|
fn schema() -> OpenapiSchema {
|
||||||
<Redirect as ResourceResultSchema>::schema()
|
<Redirect as ResponseSchema>::schema()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use super::{handle_error, into_response_helper, ResourceResult};
|
use super::{handle_error, IntoResponse, ResourceError};
|
||||||
use crate::{result::ResourceError, Response, ResponseBody};
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::{OpenapiSchema, ResourceResultSchema};
|
use crate::{OpenapiSchema, ResponseSchema};
|
||||||
|
use crate::{Response, ResponseBody, Success};
|
||||||
|
|
||||||
use futures_core::future::Future;
|
use futures_core::future::Future;
|
||||||
use gotham::hyper::StatusCode;
|
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
|
where
|
||||||
R: ResponseBody,
|
R: ResponseBody,
|
||||||
E: Display + IntoResponseError<Err = serde_json::Error>
|
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>> {
|
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>> {
|
||||||
match self {
|
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)
|
Err(e) => handle_error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
impl<R, E> ResourceResultSchema for Result<R, E>
|
impl<R, E> ResponseSchema for Result<R, E>
|
||||||
where
|
where
|
||||||
R: ResponseBody,
|
R: ResponseBody,
|
||||||
E: Display + IntoResponseError<Err = serde_json::Error>
|
E: Display + IntoResponseError<Err = serde_json::Error>
|
||||||
|
@ -59,7 +59,7 @@ where
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::result::OrAllTypes;
|
use crate::response::OrAllTypes;
|
||||||
use futures_executor::block_on;
|
use futures_executor::block_on;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
use super::{into_response_helper, ResourceResult};
|
use super::IntoResponse;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::{OpenapiSchema, ResourceResultSchema};
|
use crate::{OpenapiSchema, ResponseSchema};
|
||||||
use crate::{Response, ResponseBody};
|
use crate::{Response, ResponseBody};
|
||||||
use gotham::hyper::StatusCode;
|
use futures_util::future::{self, FutureExt};
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
use gotham::hyper::{
|
||||||
use std::{
|
header::{HeaderMap, HeaderValue, IntoHeaderName},
|
||||||
fmt::Debug,
|
StatusCode
|
||||||
future::Future,
|
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
pin::Pin
|
|
||||||
};
|
};
|
||||||
|
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
|
This can be returned from a resource when there is no cause of an error.
|
||||||
smart pointer like box, it that it implements [AsRef], [Deref] and the likes.
|
|
||||||
|
|
||||||
Usage example:
|
Usage example:
|
||||||
|
|
||||||
|
@ -42,63 +40,40 @@ fn read_all() -> Success<MyResponse> {
|
||||||
# }
|
# }
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Success<T>(T);
|
pub struct Success<T> {
|
||||||
|
value: T,
|
||||||
impl<T> AsMut<T> for Success<T> {
|
headers: HeaderMap
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<T> for Success<T> {
|
impl<T> From<T> for Success<T> {
|
||||||
fn from(t: T) -> Self {
|
fn from(t: T) -> Self {
|
||||||
Self(t)
|
Self {
|
||||||
|
value: t,
|
||||||
|
headers: HeaderMap::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone> Clone for Success<T> {
|
impl<T> Success<T> {
|
||||||
fn clone(&self) -> Self {
|
/// Set a custom HTTP header. If a header with this name was set before, its value is being updated.
|
||||||
Self(self.0.clone())
|
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: ResponseBody> IntoResponse 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
|
|
||||||
{
|
|
||||||
type Err = serde_json::Error;
|
type Err = serde_json::Error;
|
||||||
|
|
||||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
|
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>> {
|
fn accepted_types() -> Option<Vec<Mime>> {
|
||||||
|
@ -107,10 +82,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
impl<T: ResponseBody> ResourceResultSchema for Success<T>
|
impl<T: ResponseBody> ResponseSchema for Success<T> {
|
||||||
where
|
|
||||||
Self: Send
|
|
||||||
{
|
|
||||||
fn schema() -> OpenapiSchema {
|
fn schema() -> OpenapiSchema {
|
||||||
T::schema()
|
T::schema()
|
||||||
}
|
}
|
||||||
|
@ -119,8 +91,9 @@ where
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::result::OrAllTypes;
|
use crate::response::OrAllTypes;
|
||||||
use futures_executor::block_on;
|
use futures_executor::block_on;
|
||||||
|
use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize)]
|
#[derive(Debug, Default, Serialize)]
|
||||||
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
||||||
|
@ -135,6 +108,17 @@ mod test {
|
||||||
assert_eq!(res.status, StatusCode::OK);
|
assert_eq!(res.status, StatusCode::OK);
|
||||||
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
||||||
assert_eq!(res.full_body().unwrap(), br#"{"msg":""}"#);
|
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]
|
#[test]
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,10 +3,7 @@ use crate::openapi::{
|
||||||
builder::{OpenapiBuilder, OpenapiInfo},
|
builder::{OpenapiBuilder, OpenapiInfo},
|
||||||
router::OpenapiRouter
|
router::OpenapiRouter
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response};
|
||||||
result::{ResourceError, ResourceResult},
|
|
||||||
Endpoint, FromBody, Resource, Response
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "cors")]
|
#[cfg(feature = "cors")]
|
||||||
use gotham::router::route::matcher::AccessControlRequestMethodMatcher;
|
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>
|
async fn endpoint_handler<E: Endpoint>(state: &mut State) -> Result<gotham::hyper::Response<Body>, HandlerError>
|
||||||
where
|
where
|
||||||
E: Endpoint,
|
E: Endpoint,
|
||||||
<E::Output as ResourceResult>::Err: Into<HandlerError>
|
<E::Output as IntoResponse>::Err: Into<HandlerError>
|
||||||
{
|
{
|
||||||
trace!("entering endpoint_handler");
|
trace!("entering endpoint_handler");
|
||||||
let placeholders = E::Placeholders::take_from(state);
|
let placeholders = E::Placeholders::take_from(state);
|
||||||
|
|
|
@ -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
|
--> $DIR/invalid_return_type.rs:12:18
|
||||||
|
|
|
|
||||||
12 | fn endpoint() -> FooResponse {
|
12 | fn endpoint() -> FooResponse {
|
||||||
| ^^^^^^^^^^^ the trait `ResourceResultSchema` is not implemented for `FooResponse`
|
| ^^^^^^^^^^^ the trait `ResponseSchema` is not implemented for `FooResponse`
|
||||||
|
|
|
|
||||||
::: $WORKSPACE/src/endpoint.rs
|
::: $WORKSPACE/src/endpoint.rs
|
||||||
|
|
|
|
||||||
| #[openapi_bound("Output: crate::ResourceResultSchema")]
|
| #[openapi_bound("Output: crate::ResponseSchema")]
|
||||||
| ------------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Output`
|
| ------------------------------- 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
|
--> $DIR/invalid_return_type.rs:12:18
|
||||||
|
|
|
|
||||||
12 | fn endpoint() -> FooResponse {
|
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
|
::: $WORKSPACE/src/endpoint.rs
|
||||||
|
|
|
|
||||||
| type Output: ResourceResult + Send;
|
| type Output: IntoResponse + Send;
|
||||||
| -------------- required by this bound in `gotham_restful::EndpointWithSchema::Output`
|
| ------------ required by this bound in `gotham_restful::EndpointWithSchema::Output`
|
||||||
|
|
Loading…
Add table
Reference in a new issue