From 441a42c75e973bfb8130b170c34320f7417ec93c Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Tue, 26 Jan 2021 17:49:11 +0000 Subject: [PATCH] Add a Redirect type that can be returned by endpoints --- Cargo.toml | 1 + src/lib.rs | 2 +- src/result/mod.rs | 3 + src/result/redirect.rs | 143 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/result/redirect.rs diff --git a/Cargo.toml b/Cargo.toml index 9203f31..dff6df4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ openapiv3 = { version = "0.3.2", optional = true } regex = { version = "1.4", optional = true } serde = { version = "1.0.110", features = ["derive"] } serde_json = "1.0.58" +thiserror = "1.0" uuid = { version = "0.8.1", optional = true } [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 780f053..87cb87c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -481,7 +481,7 @@ pub use response::Response; mod result; pub use result::{ - AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw, + AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw, Redirect, ResourceResult, Success }; diff --git a/src/result/mod.rs b/src/result/mod.rs index 526af33..d5895aa 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -20,6 +20,9 @@ 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; diff --git a/src/result/redirect.rs b/src/result/redirect.rs new file mode 100644 index 0000000..5754adc --- /dev/null +++ b/src/result/redirect.rs @@ -0,0 +1,143 @@ +use super::{handle_error, NoContent, ResourceResult}; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use crate::{IntoResponseError, Response}; +use futures_util::future::{BoxFuture, FutureExt, TryFutureExt}; +use gotham::hyper::{ + header::{InvalidHeaderValue, LOCATION}, + Body, StatusCode +}; +use std::{ + error::Error as StdError, + fmt::{Debug, Display} +}; +use thiserror::Error; + +/** +This is the return type of a resource that only returns a redirect. It will result +in a _303 See Other_ answer, meaning the redirect will always result in a GET request +on the target. + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# +# #[derive(Resource)] +# #[resource(read_all)] +# struct MyResource; +# +#[read_all] +fn read_all() -> Redirect { + Redirect { + to: "http://localhost:8080/cool/new/location".to_owned() + } +} +# } +``` +*/ +#[derive(Clone, Debug, Default)] +pub struct Redirect { + pub to: String +} + +impl ResourceResult for Redirect { + type Err = InvalidHeaderValue; + + fn into_response(self) -> BoxFuture<'static, Result> { + async move { + let mut res = Response::new(StatusCode::SEE_OTHER, Body::empty(), None); + res.header(LOCATION, self.to.parse()?); + Ok(res) + } + .boxed() + } + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode { + StatusCode::SEE_OTHER + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema { + ::schema() + } +} + +// private type due to parent mod +#[derive(Debug, Error)] +pub enum RedirectError { + #[error("{0}")] + InvalidLocation(#[from] InvalidHeaderValue), + #[error("{0}")] + Other(#[source] E) +} + +#[allow(ambiguous_associated_items)] // an enum variant is not a type. never. +impl ResourceResult for Result +where + E: Display + IntoResponseError, + ::Err: Sync +{ + type Err = RedirectError<::Err>; + + fn into_response(self) -> BoxFuture<'static, Result> { + match self { + Ok(nc) => nc.into_response().map_err(Into::into).boxed(), + Err(e) => handle_error(e).map_err(|e| RedirectError::Other(e)).boxed() + } + } + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode { + Redirect::default_status() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema { + ::schema() + } +} + +#[cfg(test)] +mod test { + use super::*; + use futures_executor::block_on; + use gotham::hyper::StatusCode; + use thiserror::Error; + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn rediect_has_redirect_response() { + let redir = Redirect { + to: "http://localhost/foo".to_owned() + }; + let res = block_on(redir.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::SEE_OTHER); + assert_eq!(res.mime, None); + assert_eq!( + res.headers.get(LOCATION).map(|hdr| hdr.to_str().unwrap()), + Some("http://localhost/foo") + ); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + } + + #[test] + fn redirect_result() { + let redir: Result = Ok(Redirect { + to: "http://localhost/foo".to_owned() + }); + let res = block_on(redir.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::SEE_OTHER); + assert_eq!(res.mime, None); + assert_eq!( + res.headers.get(LOCATION).map(|hdr| hdr.to_str().unwrap()), + Some("http://localhost/foo") + ); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + } +}