diff --git a/src/extractor.rs b/src/extractor.rs index 768edfb76..5b3a69a89 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. @@ -521,6 +522,150 @@ impl FromRequest for Option where T: FromRequest, 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; +/// +/// #[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 { +/// A(thing) => Ok(format!("Got something: {:?}", thing)), +/// 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::async(Box::new(a.or_else(|_| b))), + EitherCollisionStrategy::PreferB => AsyncResult::async(Box::new(b.or_else(|_| a))), + EitherCollisionStrategy::FastestSuccessful => AsyncResult::async(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::async(Box::new(b)), + Err(future::Either::B((_berr, a))) => AsyncResult::async(Box::new(a)) + }))), + EitherCollisionStrategy::ErrorA => AsyncResult::async(Box::new(b.then(|r| match r { + Err(berr) => AsyncResult::async(Box::new(a)), + Ok(b) => AsyncResult::async(Box::new(a.then( |r| match r { + Ok(a) => Err(ErrorConflict("Both wings of either extractor completed")), + Err(arr) => Ok(b) + }))) + }))), + EitherCollisionStrategy::ErrorB => AsyncResult::async(Box::new(a.then(|r| match r { + Err(aerr) => AsyncResult::async(Box::new(b)), + Ok(a) => AsyncResult::async(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 @@ -752,6 +897,11 @@ mod tests { hello: String, } + #[derive(Deserialize, Debug, PartialEq)] + struct OtherInfo { + bye: String, + } + #[test] fn test_bytes() { let cfg = PayloadConfig::default(); @@ -841,6 +991,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 3ac0c2ab2..1735ffde6 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),