Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a separate trait for optional extractors #2475

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 6 additions & 30 deletions axum-core/src/extract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ pub mod rejection;

mod default_body_limit;
mod from_ref;
mod option;
mod request_parts;
mod tuple;

pub(crate) use self::default_body_limit::DefaultBodyLimitKind;
pub use self::{default_body_limit::DefaultBodyLimit, from_ref::FromRef};
pub use self::{
default_body_limit::DefaultBodyLimit,
from_ref::FromRef,
option::{OptionalFromRequest, OptionalFromRequestParts},
};

/// Type alias for [`http::Request`] whose body type defaults to [`Body`], the most common body
/// type used with axum.
Expand Down Expand Up @@ -99,35 +104,6 @@ where
}
}

#[async_trait]
impl<S, T> FromRequestParts<S> for Option<T>
where
T: FromRequestParts<S>,
S: Send + Sync,
{
type Rejection = Infallible;

async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> Result<Option<T>, Self::Rejection> {
Ok(T::from_request_parts(parts, state).await.ok())
}
}

#[async_trait]
impl<S, T> FromRequest<S> for Option<T>
where
T: FromRequest<S>,
S: Send + Sync,
{
type Rejection = Infallible;

async fn from_request(req: Request, state: &S) -> Result<Option<T>, Self::Rejection> {
Ok(T::from_request(req, state).await.ok())
}
}

#[async_trait]
impl<S, T> FromRequestParts<S> for Result<T, T::Rejection>
where
Expand Down
60 changes: 60 additions & 0 deletions axum-core/src/extract/option.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use async_trait::async_trait;
use http::request::Parts;

use crate::response::IntoResponse;

use super::{private, FromRequest, FromRequestParts, Request};

/// TODO: DOCS
#[async_trait]
pub trait OptionalFromRequestParts<S>: Sized {
/// If the extractor fails it'll use this "rejection" type. A rejection is
/// a kind of error that can be converted into a response.
type Rejection: IntoResponse;

/// Perform the extraction.
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> Result<Option<Self>, Self::Rejection>;
}

/// TODO: DOCS
#[async_trait]
pub trait OptionalFromRequest<S, M = private::ViaRequest>: Sized {
/// If the extractor fails it'll use this "rejection" type. A rejection is
/// a kind of error that can be converted into a response.
type Rejection: IntoResponse;

/// Perform the extraction.
async fn from_request(req: Request, state: &S) -> Result<Option<Self>, Self::Rejection>;
}

#[async_trait]
impl<S, T> FromRequestParts<S> for Option<T>
where
T: OptionalFromRequestParts<S>,
S: Send + Sync,
{
type Rejection = T::Rejection;

async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> Result<Option<T>, Self::Rejection> {
T::from_request_parts(parts, state).await
}
}

#[async_trait]
impl<S, T> FromRequest<S> for Option<T>
where
T: OptionalFromRequest<S>,
S: Send + Sync,
{
type Rejection = T::Rejection;

async fn from_request(req: Request, state: &S) -> Result<Option<T>, Self::Rejection> {
T::from_request(req, state).await
}
}
9 changes: 7 additions & 2 deletions axum-extra/src/extract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ mod query;
#[cfg(feature = "multipart")]
pub mod multipart;

pub use self::{cached::Cached, optional_path::OptionalPath, with_rejection::WithRejection};
#[allow(deprecated)]
pub use self::optional_path::OptionalPath;
pub use self::{cached::Cached, with_rejection::WithRejection};

#[cfg(feature = "cookie")]
pub use self::cookie::CookieJar;
Expand All @@ -34,7 +36,10 @@ pub use self::cookie::SignedCookieJar;
pub use self::form::{Form, FormRejection};

#[cfg(feature = "query")]
pub use self::query::{OptionalQuery, OptionalQueryRejection, Query, QueryRejection};
#[allow(deprecated)]
pub use self::query::OptionalQuery;
#[cfg(feature = "query")]
pub use self::query::{OptionalQueryRejection, Query, QueryRejection};

#[cfg(feature = "multipart")]
pub use self::multipart::Multipart;
Expand Down
18 changes: 8 additions & 10 deletions axum-extra/src/extract/optional_path.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use axum::{
async_trait,
extract::{path::ErrorKind, rejection::PathRejection, FromRequestParts, Path},
extract::{rejection::PathRejection, FromRequestParts, Path},
RequestPartsExt,
};
use serde::de::DeserializeOwned;
Expand Down Expand Up @@ -32,9 +32,11 @@ use serde::de::DeserializeOwned;
/// .route("/blog/:page", get(render_blog));
/// # let app: Router = app;
/// ```
#[deprecated = "Use Option<Path<_>> instead"]
#[derive(Debug)]
pub struct OptionalPath<T>(pub Option<T>);

#[allow(deprecated)]
#[async_trait]
impl<T, S> FromRequestParts<S> for OptionalPath<T>
where
Expand All @@ -47,19 +49,15 @@ where
parts: &mut http::request::Parts,
_: &S,
) -> Result<Self, Self::Rejection> {
match parts.extract::<Path<T>>().await {
Ok(Path(params)) => Ok(Self(Some(params))),
Err(PathRejection::FailedToDeserializePathParams(e))
if matches!(e.kind(), ErrorKind::WrongNumberOfParameters { got: 0, .. }) =>
{
Ok(Self(None))
}
Err(e) => Err(e),
}
parts
.extract::<Option<Path<T>>>()
.await
.map(|opt| Self(opt.map(|Path(x)| x)))
}
}

#[cfg(test)]
#[allow(deprecated)]
mod tests {
use std::num::NonZeroU32;

Expand Down
29 changes: 28 additions & 1 deletion axum-extra/src/extract/query.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use axum::{
async_trait,
extract::FromRequestParts,
extract::{FromRequestParts, OptionalFromRequestParts},
response::{IntoResponse, Response},
Error,
};
Expand Down Expand Up @@ -71,6 +71,28 @@ where
}
}

#[async_trait]
impl<T, S> OptionalFromRequestParts<S> for Query<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = QueryRejection;

async fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> Result<Option<Self>, Self::Rejection> {
if let Some(query) = parts.uri.query() {
let value = serde_html_form::from_str(query)
.map_err(|err| QueryRejection::FailedToDeserializeQueryString(Error::new(err)))?;
Ok(Some(Self(value)))
} else {
Ok(None)
}
}
}

axum_core::__impl_deref!(Query);

/// Rejection used for [`Query`].
Expand Down Expand Up @@ -152,9 +174,11 @@ impl std::error::Error for QueryRejection {
///
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
#[deprecated = "Use Option<Path<_>> instead"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query instead of Path

#[derive(Debug, Clone, Copy, Default)]
pub struct OptionalQuery<T>(pub Option<T>);

#[allow(deprecated)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OptionalPath became this:

        parts
            .extract::<Option<Path<T>>>()
            .await
            .map(|opt| Self(opt.map(|Path(x)| x)))

I think that a similar thing can be done here.

#[async_trait]
impl<T, S> FromRequestParts<S> for OptionalQuery<T>
where
Expand All @@ -175,6 +199,7 @@ where
}
}

#[allow(deprecated)]
impl<T> std::ops::Deref for OptionalQuery<T> {
type Target = Option<T>;

Expand All @@ -184,6 +209,7 @@ impl<T> std::ops::Deref for OptionalQuery<T> {
}
}

#[allow(deprecated)]
impl<T> std::ops::DerefMut for OptionalQuery<T> {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
Expand Down Expand Up @@ -231,6 +257,7 @@ impl std::error::Error for OptionalQueryRejection {
}

#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
use crate::test_helpers::*;
Expand Down
27 changes: 26 additions & 1 deletion axum-extra/src/typed_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use axum::{
async_trait,
extract::FromRequestParts,
extract::{FromRequestParts, OptionalFromRequestParts},
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
};
use headers::{Header, HeaderMapExt};
Expand Down Expand Up @@ -80,6 +80,31 @@ where
}
}

#[async_trait]
impl<T, S> OptionalFromRequestParts<S> for TypedHeader<T>
where
T: Header,
S: Send + Sync,
{
type Rejection = TypedHeaderRejection;

async fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> Result<Option<Self>, Self::Rejection> {
let mut values = parts.headers.get_all(T::name()).iter();
let is_missing = values.size_hint() == (0, Some(0));
match T::decode(&mut values) {
Ok(res) => Ok(Some(Self(res))),
Err(_) if is_missing => Ok(None),
Err(err) => Err(TypedHeaderRejection {
name: T::name(),
reason: TypedHeaderRejectionReason::Error(err),
}),
}
}
}

axum_core::__impl_deref!(TypedHeader);

impl<T> IntoResponseParts for TypedHeader<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ error[E0277]: the trait bound `bool: FromRequestParts<()>` is not satisfied
<Version as FromRequestParts<S>>
<Extensions as FromRequestParts<S>>
<ConnectInfo<T> as FromRequestParts<S>>
and $N others
and 28 others
= note: required for `bool` to implement `FromRequest<(), axum_core::extract::private::ViaParts>`
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
--> tests/debug_handler/fail/argument_not_extractor.rs:5:24
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ error[E0277]: the trait bound `for<'de> Struct: serde::de::Deserialize<'de>` is
i32
i64
i128
and $N others
and 131 others
= note: required for `Struct` to implement `serde::de::DeserializeOwned`
= note: required for `Json<Struct>` to implement `FromRequest<()>`
= help: see issue #48214
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ error[E0277]: the trait bound `NotIntoResponse: IntoResponse` is not satisfied
axum::body::Bytes
Body
axum::extract::rejection::FailedToBufferBody
bytes::bytes_mut::BytesMut
axum::extract::rejection::LengthLimitError
axum::extract::rejection::UnknownBodyError
bytes::bytes_mut::BytesMut
and $N others
and 121 others
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check`
--> tests/debug_handler/fail/single_wrong_return_tuple.rs:6:23
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not sat
[(K, V); N]
()
(T1,)
and $N others
and 15 others
= help: see issue #48214
= help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
4 changes: 2 additions & 2 deletions axum-macros/tests/debug_handler/fail/wrong_return_type.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
axum::body::Bytes
Body
axum::extract::rejection::FailedToBufferBody
bytes::bytes_mut::BytesMut
axum::extract::rejection::LengthLimitError
axum::extract::rejection::UnknownBodyError
bytes::bytes_mut::BytesMut
and $N others
and 121 others
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check`
--> tests/debug_handler/fail/wrong_return_type.rs:4:23
|
Expand Down
38 changes: 19 additions & 19 deletions axum-macros/tests/from_request/fail/generic_without_via.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ error: #[derive(FromRequest)] only supports generics when used with #[from_reque
| ^

error[E0277]: the trait bound `fn(Extractor<()>) -> impl Future<Output = ()> {foo}: Handler<_, _>` is not satisfied
--> tests/from_request/fail/generic_without_via.rs:11:44
|
11 | _ = Router::<()>::new().route("/", get(foo));
| --- ^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(Extractor<()>) -> impl Future<Output = ()> {foo}`
| |
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
= help: the following other types implement trait `Handler<T, S>`:
<Layered<L, H, T, S> as Handler<T, S>>
<MethodRouter<S> as Handler<(), S>>
--> tests/from_request/fail/generic_without_via.rs:11:44
|
11 | _ = Router::<()>::new().route("/", get(foo));
| --- ^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(Extractor<()>) -> impl Future<Output = ()> {foo}`
| |
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
= help: the following other types implement trait `Handler<T, S>`:
<Layered<L, H, T, S> as Handler<T, S>>
<MethodRouter<S> as Handler<(), S>>
note: required by a bound in `axum::routing::get`
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
| top_level_handler_fn!(get, GET);
| ^^^^^^^^^^^^^^^^^^^^^^---^^^^^^
| | |
| | required by a bound in this function
| required by this bound in `get`
= note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
| top_level_handler_fn!(get, GET);
| ^^^^^^^^^^^^^^^^^^^^^^---^^^^^^
| | |
| | required by a bound in this function
| required by this bound in `get`
= note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
Loading
Loading