diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index 0e435aa8bc..dda3e2eb49 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 0000000000..d342fc193c --- /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, 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, &'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 { + #[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 { + #[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 { + #[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 { + #[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 { + #[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 { + #[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 { + #[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 FromRequest for $either<$($ident),*, $last> + where + $($ident: FromRequest),*, + $last: FromRequest, + B: Send, + { + type Rejection = $last::Rejection; + + async fn from_request(req: &mut RequestParts) -> Result { + $( + 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 195dcfd896..ea5eafb932 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, ) -> , 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 f057f4688f..d20bae2012 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 { - /// A value of type L. - Left(L), - /// A value of type R. - Right(R), -} - -#[async_trait] -impl FromRequest for Either -where - L: FromRequest, - R: FromRequest, - B: Send, -{ - type Rejection = R::Rejection; - - async fn from_request(req: &mut RequestParts) -> Result { - if let Ok(l) = req.extract().await { - return Ok(Either::Left(l)); - } - - Ok(Either::Right(req.extract().await?)) - } -} - -impl IntoResponse for Either -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 {