From e22f6f9d2bbce9623d798717bad807c53a36175f Mon Sep 17 00:00:00 2001 From: David Pedersen <david.pdrsn@gmail.com> Date: Wed, 17 Aug 2022 10:57:19 +0200 Subject: [PATCH] Add `Either{2..8}` to axum-extra (#1263) * Add `Either{2..8}` * Apply suggestions from code review Co-authored-by: Jonas Platte <jplatte+git@posteo.de> * Either2 => Either * Update axum-extra/src/either.rs Co-authored-by: Jonas Platte <jplatte+git@posteo.de> Co-authored-by: Jonas Platte <jplatte+git@posteo.de> --- axum-extra/CHANGELOG.md | 3 + axum-extra/src/either.rs | 233 +++++++++++++++++++++++++++++++++++ axum-extra/src/handler/or.rs | 6 +- axum-extra/src/lib.rs | 47 +------ 4 files changed, 240 insertions(+), 49 deletions(-) create mode 100755 axum-extra/src/either.rs diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index 0e435aa8..dda3e2eb 100644 --- a/axum-extra/CHANGELOG.md +++ b/axum-extra/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning]. - **added:** Support chaining handlers with `HandlerCallWithExtractors::or` ([#1170]) - **change:** axum-extra's MSRV is now 1.60 ([#1239]) - **added:** Add Protocol Buffer extractor and response ([#1239]) +- **added:** Add `Either*` types for combining extractors and responses into a + single type ([#1263]) - **added:** `WithRejection` extractor for customizing other extractors' rejections ([#1262]) - **added:** Add sync constructors to `CookieJar`, `PrivateCookieJar`, and `SignedCookieJar` so they're easier to use in custom middleware @@ -28,6 +30,7 @@ and this project adheres to [Semantic Versioning]. [#1214]: https://github.com/tokio-rs/axum/pull/1214 [#1239]: https://github.com/tokio-rs/axum/pull/1239 [#1262]: https://github.com/tokio-rs/axum/pull/1262 +[#1263]: https://github.com/tokio-rs/axum/pull/1263 # 0.3.5 (27. June, 2022) diff --git a/axum-extra/src/either.rs b/axum-extra/src/either.rs new file mode 100755 index 00000000..d342fc19 --- /dev/null +++ b/axum-extra/src/either.rs @@ -0,0 +1,233 @@ +//! `Either*` types for combining extractors or responses into a single type. +//! +//! # As an extractor +//! +//! ``` +//! use axum_extra::either::Either3; +//! use axum::{body::Bytes, Json}; +//! +//! async fn handler( +//! body: Either3<Json<serde_json::Value>, String, Bytes>, +//! ) { +//! match body { +//! Either3::E1(json) => { /* ... */ } +//! Either3::E2(string) => { /* ... */ } +//! Either3::E3(bytes) => { /* ... */ } +//! } +//! } +//! # +//! # let _: axum::routing::MethodRouter = axum::routing::get(handler); +//! ``` +//! +//! Note that if all the inner extractors reject the request, the rejection from the last +//! extractor will be returned. For the example above that would be [`BytesRejection`]. +//! +//! # As a response +//! +//! ``` +//! use axum_extra::either::Either3; +//! use axum::{Json, http::StatusCode, response::IntoResponse}; +//! use serde_json::{Value, json}; +//! +//! async fn handler() -> Either3<Json<Value>, &'static str, StatusCode> { +//! if something() { +//! Either3::E1(Json(json!({ "data": "..." }))) +//! } else if something_else() { +//! Either3::E2("foobar") +//! } else { +//! Either3::E3(StatusCode::NOT_FOUND) +//! } +//! } +//! +//! fn something() -> bool { +//! // ... +//! # false +//! } +//! +//! fn something_else() -> bool { +//! // ... +//! # false +//! } +//! # +//! # let _: axum::routing::MethodRouter = axum::routing::get(handler); +//! ``` +//! +//! The general recommendation is to use [`IntoResponse::into_response`] to return different response +//! types, but if you need to preserve the exact type then `Either*` works as well. +//! +//! [`BytesRejection`]: axum::extract::rejection::BytesRejection +//! [`IntoResponse::into_response`]: https://docs.rs/axum/0.5/axum/response/index.html#returning-different-response-types + +use axum::{ + async_trait, + extract::{FromRequest, RequestParts}, + response::{IntoResponse, Response}, +}; + +/// Combines two extractors or responses into a single type. +/// +/// See the [module docs](self) for examples. +#[derive(Debug, Clone)] +pub enum Either<E1, E2> { + #[allow(missing_docs)] + E1(E1), + #[allow(missing_docs)] + E2(E2), +} + +/// Combines three extractors or responses into a single type. +/// +/// See the [module docs](self) for examples. +#[derive(Debug, Clone)] +pub enum Either3<E1, E2, E3> { + #[allow(missing_docs)] + E1(E1), + #[allow(missing_docs)] + E2(E2), + #[allow(missing_docs)] + E3(E3), +} + +/// Combines four extractors or responses into a single type. +/// +/// See the [module docs](self) for examples. +#[derive(Debug, Clone)] +pub enum Either4<E1, E2, E3, E4> { + #[allow(missing_docs)] + E1(E1), + #[allow(missing_docs)] + E2(E2), + #[allow(missing_docs)] + E3(E3), + #[allow(missing_docs)] + E4(E4), +} + +/// Combines five extractors or responses into a single type. +/// +/// See the [module docs](self) for examples. +#[derive(Debug, Clone)] +pub enum Either5<E1, E2, E3, E4, E5> { + #[allow(missing_docs)] + E1(E1), + #[allow(missing_docs)] + E2(E2), + #[allow(missing_docs)] + E3(E3), + #[allow(missing_docs)] + E4(E4), + #[allow(missing_docs)] + E5(E5), +} + +/// Combines six extractors or responses into a single type. +/// +/// See the [module docs](self) for examples. +#[derive(Debug, Clone)] +pub enum Either6<E1, E2, E3, E4, E5, E6> { + #[allow(missing_docs)] + E1(E1), + #[allow(missing_docs)] + E2(E2), + #[allow(missing_docs)] + E3(E3), + #[allow(missing_docs)] + E4(E4), + #[allow(missing_docs)] + E5(E5), + #[allow(missing_docs)] + E6(E6), +} + +/// Combines seven extractors or responses into a single type. +/// +/// See the [module docs](self) for examples. +#[derive(Debug, Clone)] +pub enum Either7<E1, E2, E3, E4, E5, E6, E7> { + #[allow(missing_docs)] + E1(E1), + #[allow(missing_docs)] + E2(E2), + #[allow(missing_docs)] + E3(E3), + #[allow(missing_docs)] + E4(E4), + #[allow(missing_docs)] + E5(E5), + #[allow(missing_docs)] + E6(E6), + #[allow(missing_docs)] + E7(E7), +} + +/// Combines eight extractors or responses into a single type. +/// +/// See the [module docs](self) for examples. +#[derive(Debug, Clone)] +pub enum Either8<E1, E2, E3, E4, E5, E6, E7, E8> { + #[allow(missing_docs)] + E1(E1), + #[allow(missing_docs)] + E2(E2), + #[allow(missing_docs)] + E3(E3), + #[allow(missing_docs)] + E4(E4), + #[allow(missing_docs)] + E5(E5), + #[allow(missing_docs)] + E6(E6), + #[allow(missing_docs)] + E7(E7), + #[allow(missing_docs)] + E8(E8), +} + +macro_rules! impl_traits_for_either { + ( + $either:ident => + [$($ident:ident),* $(,)?], + $last:ident $(,)? + ) => { + #[async_trait] + impl<B, $($ident),*, $last> FromRequest<B> for $either<$($ident),*, $last> + where + $($ident: FromRequest<B>),*, + $last: FromRequest<B>, + B: Send, + { + type Rejection = $last::Rejection; + + async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> { + $( + if let Ok(value) = req.extract().await { + return Ok(Self::$ident(value)); + } + )* + + req.extract().await.map(Self::$last) + } + } + + impl<$($ident),*, $last> IntoResponse for $either<$($ident),*, $last> + where + $($ident: IntoResponse),*, + $last: IntoResponse, + { + fn into_response(self) -> Response { + match self { + $( Self::$ident(value) => value.into_response(), )* + Self::$last(value) => value.into_response(), + } + } + } + }; +} + +impl_traits_for_either!(Either => [E1], E2); +impl_traits_for_either!(Either3 => [E1, E2], E3); +impl_traits_for_either!(Either4 => [E1, E2, E3], E4); +impl_traits_for_either!(Either5 => [E1, E2, E3, E4], E5); +impl_traits_for_either!(Either6 => [E1, E2, E3, E4, E5], E6); +impl_traits_for_either!(Either7 => [E1, E2, E3, E4, E5, E6], E7); +impl_traits_for_either!(Either8 => [E1, E2, E3, E4, E5, E6, E7], E8); diff --git a/axum-extra/src/handler/or.rs b/axum-extra/src/handler/or.rs index 195dcfd8..ea5eafb9 100644 --- a/axum-extra/src/handler/or.rs +++ b/axum-extra/src/handler/or.rs @@ -1,5 +1,5 @@ use super::HandlerCallWithExtractors; -use crate::Either; +use crate::either::Either; use axum::{ extract::{FromRequest, RequestParts}, handler::Handler, @@ -40,12 +40,12 @@ where extractors: Either<Lt, Rt>, ) -> <Self as HandlerCallWithExtractors<Either<Lt, Rt>, B>>::Future { match extractors { - Either::Left(lt) => self + Either::E1(lt) => self .lhs .call(lt) .map(IntoResponse::into_response as _) .left_future(), - Either::Right(rt) => self + Either::E2(rt) => self .rhs .call(rt) .map(IntoResponse::into_response as _) diff --git a/axum-extra/src/lib.rs b/axum-extra/src/lib.rs index f057f468..d20bae20 100644 --- a/axum-extra/src/lib.rs +++ b/axum-extra/src/lib.rs @@ -63,13 +63,8 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![cfg_attr(test, allow(clippy::float_cmp))] -use axum::{ - async_trait, - extract::{FromRequest, RequestParts}, - response::IntoResponse, -}; - pub mod body; +pub mod either; pub mod extract; pub mod handler; pub mod response; @@ -81,46 +76,6 @@ pub mod json_lines; #[cfg(feature = "protobuf")] pub mod protobuf; -/// Combines two extractors or responses into a single type. -#[derive(Debug, Copy, Clone)] -pub enum Either<L, R> { - /// A value of type L. - Left(L), - /// A value of type R. - Right(R), -} - -#[async_trait] -impl<L, R, B> FromRequest<B> for Either<L, R> -where - L: FromRequest<B>, - R: FromRequest<B>, - B: Send, -{ - type Rejection = R::Rejection; - - async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> { - if let Ok(l) = req.extract().await { - return Ok(Either::Left(l)); - } - - Ok(Either::Right(req.extract().await?)) - } -} - -impl<L, R> IntoResponse for Either<L, R> -where - L: IntoResponse, - R: IntoResponse, -{ - fn into_response(self) -> axum::response::Response { - match self { - Self::Left(inner) => inner.into_response(), - Self::Right(inner) => inner.into_response(), - } - } -} - #[cfg(feature = "typed-routing")] #[doc(hidden)] pub mod __private {