From 86af02156bbaee37ebd2c0f6be95cc53e785e910 Mon Sep 17 00:00:00 2001 From: Akos Vandra Date: Mon, 10 Dec 2018 17:02:05 +0100 Subject: [PATCH] add impl FromRequest for Either (#618) --- CHANGES.md | 8 +- src/extractor.rs | 195 ++++++++++++++++++++++++++++++++++++++++++++++- src/handler.rs | 2 +- 3 files changed, 202 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6092544e..11e639a8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,14 @@ # Changes +## [0.7.16] - xxxx-xx-xx + +### Added + +* Implement `FromRequest` extractor for `Either` + ## [0.7.15] - 2018-12-05 -## Changed +### Changed * `ClientConnector::resolver` now accepts `Into` instead of `Addr`. It enables user to implement own resolver. diff --git a/src/extractor.rs b/src/extractor.rs index 717e0f6c..6f55487b 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -12,10 +12,11 @@ use serde::de::{self, DeserializeOwned}; use serde_urlencoded; use de::PathDeserializer; -use error::{Error, ErrorBadRequest, ErrorNotFound, UrlencodedError}; +use error::{Error, ErrorBadRequest, ErrorNotFound, UrlencodedError, ErrorConflict}; use handler::{AsyncResult, FromRequest}; use httpmessage::{HttpMessage, MessageBody, UrlEncoded}; use httprequest::HttpRequest; +use Either; #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's path. Information from the path is @@ -634,6 +635,151 @@ where } } +/// Extract either one of two fields from the request. +/// +/// If both or none of the fields can be extracted, the default behaviour is to prefer the first +/// successful, last that failed. The behaviour can be changed by setting the appropriate +/// ```EitherCollisionStrategy```. +/// +/// CAVEAT: Most of the time both extractors will be run. Make sure that the extractors you specify +/// can be run one after another (or in parallel). This will always fail for extractors that modify +/// the request state (such as the `Form` extractors that read in the body stream). +/// So Either, Form> will not work correctly - it will only succeed if it matches the first +/// option, but will always fail to match the second (since the body stream will be at the end, and +/// appear to be empty). +/// +/// ## Example +/// +/// ```rust +/// # extern crate actix_web; +/// extern crate rand; +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; +/// use actix_web::error::ErrorBadRequest; +/// use actix_web::Either; +/// +/// #[derive(Debug, Deserialize)] +/// struct Thing { name: String } +/// +/// #[derive(Debug, Deserialize)] +/// struct OtherThing { id: String } +/// +/// impl FromRequest for Thing { +/// type Config = (); +/// type Result = Result; +/// +/// #[inline] +/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { +/// if rand::random() { +/// Ok(Thing { name: "thingy".into() }) +/// } else { +/// Err(ErrorBadRequest("no luck")) +/// } +/// } +/// } +/// +/// impl FromRequest for OtherThing { +/// type Config = (); +/// type Result = Result; +/// +/// #[inline] +/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { +/// if rand::random() { +/// Ok(OtherThing { id: "otherthingy".into() }) +/// } else { +/// Err(ErrorBadRequest("no luck")) +/// } +/// } +/// } +/// +/// /// extract text data from request +/// fn index(supplied_thing: Either) -> Result { +/// match supplied_thing { +/// Either::A(thing) => Ok(format!("Got something: {:?}", thing)), +/// Either::B(other_thing) => Ok(format!("Got anotherthing: {:?}", other_thing)) +/// } +/// } +/// +/// fn main() { +/// let app = App::new().resource("/users/:first", |r| { +/// r.method(http::Method::POST).with(index) +/// }); +/// } +/// ``` +impl FromRequest for Either where A: FromRequest, B: FromRequest { + type Config = EitherConfig; + type Result = AsyncResult>; + + #[inline] + fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { + let a = A::from_request(&req.clone(), &cfg.a).into().map(|a| Either::A(a)); + let b = B::from_request(req, &cfg.b).into().map(|b| Either::B(b)); + + match &cfg.collision_strategy { + EitherCollisionStrategy::PreferA => AsyncResult::future(Box::new(a.or_else(|_| b))), + EitherCollisionStrategy::PreferB => AsyncResult::future(Box::new(b.or_else(|_| a))), + EitherCollisionStrategy::FastestSuccessful => AsyncResult::future(Box::new(a.select2(b).then( |r| match r { + Ok(future::Either::A((ares, _b))) => AsyncResult::ok(ares), + Ok(future::Either::B((bres, _a))) => AsyncResult::ok(bres), + Err(future::Either::A((_aerr, b))) => AsyncResult::future(Box::new(b)), + Err(future::Either::B((_berr, a))) => AsyncResult::future(Box::new(a)) + }))), + EitherCollisionStrategy::ErrorA => AsyncResult::future(Box::new(b.then(|r| match r { + Err(_berr) => AsyncResult::future(Box::new(a)), + Ok(b) => AsyncResult::future(Box::new(a.then( |r| match r { + Ok(_a) => Err(ErrorConflict("Both wings of either extractor completed")), + Err(_arr) => Ok(b) + }))) + }))), + EitherCollisionStrategy::ErrorB => AsyncResult::future(Box::new(a.then(|r| match r { + Err(_aerr) => AsyncResult::future(Box::new(b)), + Ok(a) => AsyncResult::future(Box::new(b.then( |r| match r { + Ok(_b) => Err(ErrorConflict("Both wings of either extractor completed")), + Err(_berr) => Ok(a) + }))) + }))), + } + } +} + +/// Defines the result if neither or both of the extractors supplied to an Either extractor succeed. +#[derive(Debug)] +pub enum EitherCollisionStrategy { + /// If both are successful, return A, if both fail, return error of B + PreferA, + /// If both are successful, return B, if both fail, return error of A + PreferB, + /// Return result of the faster, error of the slower if both fail + FastestSuccessful, + + /// Return error if both succeed, return error of A if both fail + ErrorA, + /// Return error if both succeed, return error of B if both fail + ErrorB +} + +impl Default for EitherCollisionStrategy { + fn default() -> Self { + EitherCollisionStrategy::FastestSuccessful + } +} + +pub struct EitherConfig where A: FromRequest, B: FromRequest { + a: A::Config, + b: B::Config, + collision_strategy: EitherCollisionStrategy +} + +impl Default for EitherConfig where A: FromRequest, B: FromRequest { + fn default() -> Self { + EitherConfig { + a: A::Config::default(), + b: B::Config::default(), + collision_strategy: EitherCollisionStrategy::default() + } + } +} + /// Optionally extract a field from the request or extract the Error if unsuccessful /// /// If the FromRequest for T fails, inject Err into handler rather than returning an error response @@ -874,6 +1020,11 @@ mod tests { hello: String, } + #[derive(Deserialize, Debug, PartialEq)] + struct OtherInfo { + bye: String, + } + #[test] fn test_bytes() { let cfg = PayloadConfig::default(); @@ -977,6 +1128,48 @@ mod tests { } } + #[test] + fn test_either() { + let req = TestRequest::default().finish(); + let mut cfg: EitherConfig, Query, _> = EitherConfig::default(); + + assert!(Either::, Query>::from_request(&req, &cfg).poll().is_err()); + + let req = TestRequest::default().uri("/index?hello=world").finish(); + + match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { + Async::Ready(r) => assert_eq!(r, Either::A(Query(Info { hello: "world".into() }))), + _ => unreachable!(), + } + + let req = TestRequest::default().uri("/index?bye=world").finish(); + match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { + Async::Ready(r) => assert_eq!(r, Either::B(Query(OtherInfo { bye: "world".into() }))), + _ => unreachable!(), + } + + let req = TestRequest::default().uri("/index?hello=world&bye=world").finish(); + cfg.collision_strategy = EitherCollisionStrategy::PreferA; + + match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { + Async::Ready(r) => assert_eq!(r, Either::A(Query(Info { hello: "world".into() }))), + _ => unreachable!(), + } + + cfg.collision_strategy = EitherCollisionStrategy::PreferB; + + match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { + Async::Ready(r) => assert_eq!(r, Either::B(Query(OtherInfo { bye: "world".into() }))), + _ => unreachable!(), + } + + cfg.collision_strategy = EitherCollisionStrategy::ErrorA; + assert!(Either::, Query>::from_request(&req, &cfg).poll().is_err()); + + cfg.collision_strategy = EitherCollisionStrategy::FastestSuccessful; + assert!(Either::, Query>::from_request(&req, &cfg).poll().is_ok()); + } + #[test] fn test_result() { let req = TestRequest::with_header( diff --git a/src/handler.rs b/src/handler.rs index 6ed93f92..c6880818 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -86,7 +86,7 @@ pub trait FromRequest: Sized { /// # fn is_a_variant() -> bool { true } /// # fn main() {} /// ``` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Either { /// First branch of the type A(A),