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,