Skip to content

Commit

Permalink
Add WithRejection (#1262)
Browse files Browse the repository at this point in the history
* new(axum-extra): Added `WithRejection` base impl

Based on @jplatte's version (#1116 (comment)), 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 <[email protected]>

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

Co-authored-by: David Pedersen <[email protected]>

* docs(axum-extra): fixed suggestions

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

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

refs: d878eed , f9200bf

* 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 <[email protected]>
  • Loading branch information
Altair-Bueno and davidpdrsn authored Aug 17, 2022
1 parent 01630cf commit fb21561
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 0 deletions.
2 changes: 2 additions & 0 deletions axum-extra/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions axum-extra/src/extract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub mod cookie;
#[cfg(feature = "query")]
mod query;

mod with_rejection;

pub use self::cached::Cached;

#[cfg(feature = "cookie")]
Expand All @@ -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;
165 changes: 165 additions & 0 deletions axum-extra/src/extract/with_rejection.rs
Original file line number Diff line number Diff line change
@@ -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)))
}
}
3 changes: 3 additions & 0 deletions examples/customize-extractor-error/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit fb21561

Please sign in to comment.