From fb21561616a3919b9e920547f75090f86ed2139c Mon Sep 17 00:00:00 2001
From: Altair Bueno <67512202+Altair-Bueno@users.noreply.github.com>
Date: Wed, 17 Aug 2022 09:59:25 +0200
Subject: [PATCH] Add `WithRejection` (#1262)

* new(axum-extra): Added `WithRejection` base impl

Based on @jplatte's version (https://github.com/tokio-rs/axum/issues/1116#issuecomment-1215048273), with slight changes

- Using `From<E::Rejection>` to define the trait bound on a more concise way
- Renamed variables to something more meaningfull

* revert(axum-extra): Removed `with_rejection` feat

* ref(axum-extra): Replaced `match` with `?`

* tests(axum-extra): Added test for `WithRejection`

* examples: Replaced custom `Json` extractor with `WithRejection`

* docs(axum-extra): Added doc to `WithRejection`

* fmt(cargo-check): removed whitespaces

* fmt(customize-extractor-error): missing fmt

* docs(axum-extra): doctest includes `Handler` test

Co-authored-by: David Pedersen <david.pdrsn@gmail.com>

* docs(axum-extra):` _ `-> `rejection`

Co-authored-by: David Pedersen <david.pdrsn@gmail.com>

* docs(axum-extra): fixed suggestions

* fix(axum-extra): `WithRejection` manual trait impl

* revert(customize-extractor-error): Undo example changes

refs: d878eede1897456c15d257aca8e52d588e685198 , f9200bf4b950a920d6a565d946221b2bd6ec7bcd

* example(customize-extractor-error): Added reference to `WithRejection`

* docs(axum-extra): Removed `customize-extractor-error` reference

* fmt(axum-extra): cargo fmt

* docs(axum-extra): Added `WithRejection` to CHANGELOG.md

Co-authored-by: David Pedersen <david.pdrsn@gmail.com>
---
 axum-extra/CHANGELOG.md                       |   2 +
 axum-extra/src/extract/mod.rs                 |   4 +
 axum-extra/src/extract/with_rejection.rs      | 165 ++++++++++++++++++
 .../customize-extractor-error/src/main.rs     |   3 +
 4 files changed, 174 insertions(+)
 create mode 100644 axum-extra/src/extract/with_rejection.rs

diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md
index 4722a6ac..0e435aa8 100644
--- a/axum-extra/CHANGELOG.md
+++ b/axum-extra/CHANGELOG.md
@@ -18,6 +18,7 @@ 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:** `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
 
@@ -26,6 +27,7 @@ and this project adheres to [Semantic Versioning].
 [#1170]: https://github.com/tokio-rs/axum/pull/1170
 [#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
 
 # 0.3.5 (27. June, 2022)
 
diff --git a/axum-extra/src/extract/mod.rs b/axum-extra/src/extract/mod.rs
index 22e19559..e795cfb7 100644
--- a/axum-extra/src/extract/mod.rs
+++ b/axum-extra/src/extract/mod.rs
@@ -11,6 +11,8 @@ pub mod cookie;
 #[cfg(feature = "query")]
 mod query;
 
+mod with_rejection;
+
 pub use self::cached::Cached;
 
 #[cfg(feature = "cookie")]
@@ -31,3 +33,5 @@ pub use self::query::Query;
 #[cfg(feature = "json-lines")]
 #[doc(no_inline)]
 pub use crate::json_lines::JsonLines;
+
+pub use self::with_rejection::WithRejection;
diff --git a/axum-extra/src/extract/with_rejection.rs b/axum-extra/src/extract/with_rejection.rs
new file mode 100644
index 00000000..e87c3d5e
--- /dev/null
+++ b/axum-extra/src/extract/with_rejection.rs
@@ -0,0 +1,165 @@
+use axum::async_trait;
+use axum::extract::{FromRequest, RequestParts};
+use axum::response::IntoResponse;
+use std::fmt::Debug;
+use std::marker::PhantomData;
+use std::ops::{Deref, DerefMut};
+
+/// Extractor for customizing extractor rejections
+///
+/// `WithRejection` wraps another extractor and gives you the result. If the
+/// extraction fails, the `Rejection` is transformed into `R` and returned as a
+/// response
+///
+/// `E` is expected to implement [`FromRequest`]
+///
+/// `R` is expected to implement [`IntoResponse`] and [`From<E::Rejection>`]
+///
+///
+/// # Example
+///
+/// ```rust
+/// use axum::extract::rejection::JsonRejection;
+/// use axum::response::{Response, IntoResponse};
+/// use axum::Json;
+/// use axum_extra::extract::WithRejection;
+/// use serde::Deserialize;
+///
+/// struct MyRejection { /* ... */ }
+///
+/// impl From<JsonRejection> for MyRejection {
+///     fn from(rejection: JsonRejection) -> MyRejection {
+///         // ...
+///         # todo!()
+///     }
+/// }
+///
+/// impl IntoResponse for MyRejection {
+///     fn into_response(self) -> Response {
+///         // ...
+///         # todo!()
+///     }
+/// }
+/// #[derive(Debug, Deserialize)]
+/// struct Person { /* ... */ }
+///
+/// async fn handler(
+///     // If the `Json` extractor ever fails, `MyRejection` will be sent to the
+///     // client using the `IntoResponse` impl
+///     WithRejection(Json(Person), _): WithRejection<Json<Person>, MyRejection>
+/// ) { /* ... */ }
+/// # let _: axum::Router = axum::Router::new().route("/", axum::routing::get(handler));
+/// ```
+///
+/// [`FromRequest`]: axum::extract::FromRequest
+/// [`IntoResponse`]: axum::response::IntoResponse
+/// [`From<E::Rejection>`]: std::convert::From
+pub struct WithRejection<E, R>(pub E, pub PhantomData<R>);
+
+impl<E, R> WithRejection<E, R> {
+    /// Returns the wrapped extractor
+    fn into_inner(self) -> E {
+        self.0
+    }
+}
+
+impl<E, R> Debug for WithRejection<E, R>
+where
+    E: Debug,
+{
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_tuple("WithRejection")
+            .field(&self.0)
+            .field(&self.1)
+            .finish()
+    }
+}
+
+impl<E, R> Clone for WithRejection<E, R>
+where
+    E: Clone,
+{
+    fn clone(&self) -> Self {
+        Self(self.0.clone(), self.1.clone())
+    }
+}
+
+impl<E, R> Copy for WithRejection<E, R> where E: Copy {}
+
+impl<E: Default, R> Default for WithRejection<E, R> {
+    fn default() -> Self {
+        Self(Default::default(), Default::default())
+    }
+}
+
+impl<E, R> Deref for WithRejection<E, R> {
+    type Target = E;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl<E, R> DerefMut for WithRejection<E, R> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+#[async_trait]
+impl<B, E, R> FromRequest<B> for WithRejection<E, R>
+where
+    B: Send,
+    E: FromRequest<B>,
+    R: From<E::Rejection> + IntoResponse,
+{
+    type Rejection = R;
+
+    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
+        let extractor = req.extract::<E>().await?;
+        Ok(WithRejection(extractor, PhantomData))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use axum::http::Request;
+    use axum::response::Response;
+
+    use super::*;
+
+    #[tokio::test]
+    async fn extractor_rejection_is_transformed() {
+        struct TestExtractor;
+        struct TestRejection;
+
+        #[async_trait]
+        impl<B: Send> FromRequest<B> for TestExtractor {
+            type Rejection = ();
+
+            async fn from_request(_: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
+                Err(())
+            }
+        }
+
+        impl IntoResponse for TestRejection {
+            fn into_response(self) -> Response {
+                ().into_response()
+            }
+        }
+
+        impl From<()> for TestRejection {
+            fn from(_: ()) -> Self {
+                TestRejection
+            }
+        }
+
+        let mut req = RequestParts::new(Request::new(()));
+
+        let result = req
+            .extract::<WithRejection<TestExtractor, TestRejection>>()
+            .await;
+
+        assert!(matches!(result, Err(TestRejection)))
+    }
+}
diff --git a/examples/customize-extractor-error/src/main.rs b/examples/customize-extractor-error/src/main.rs
index bc0973b4..be5ef595 100644
--- a/examples/customize-extractor-error/src/main.rs
+++ b/examples/customize-extractor-error/src/main.rs
@@ -3,6 +3,9 @@
 //! ```not_rust
 //! cd examples && cargo run -p example-customize-extractor-error
 //! ```
+//!
+//! See https://docs.rs/axum-extra/0.3.7/axum_extra/extract/struct.WithRejection.html
+//! example for creating custom errors from already existing extractors
 
 use axum::{
     async_trait,