Merge branch 'separate-nesting-opaque-services' into separate-nesting-opaque-services--router-state

This commit is contained in:
David Pedersen 2022-07-03 16:50:53 +02:00
commit a7749d2bf2
10 changed files with 351 additions and 66 deletions

View file

@ -46,7 +46,6 @@
#![deny(unreachable_pub, private_in_public)] #![deny(unreachable_pub, private_in_public)]
#![allow(elided_lifetimes_in_paths, clippy::type_complexity)] #![allow(elided_lifetimes_in_paths, clippy::type_complexity)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(test, allow(clippy::float_cmp))] #![cfg_attr(test, allow(clippy::float_cmp))]
#[macro_use] #[macro_use]

View file

@ -58,4 +58,4 @@ tower = { version = "0.4", features = ["util"] }
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdocflags = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]

View file

@ -4,4 +4,14 @@ error[E0277]: the trait bound `bool: FromRequest<Body>` is not satisfied
4 | async fn handler(foo: bool) {} 4 | async fn handler(foo: bool) {}
| ^^^^ the trait `FromRequest<Body>` is not implemented for `bool` | ^^^^ the trait `FromRequest<Body>` is not implemented for `bool`
| |
= help: the following other types implement trait `FromRequest<B>`:
()
(T1, T2)
(T1, T2, T3)
(T1, T2, T3, T4)
(T1, T2, T3, T4, T5)
(T1, T2, T3, T4, T5, T6)
(T1, T2, T3, T4, T5, T6, T7)
(T1, T2, T3, T4, T5, T6, T7, T8)
and 33 others
= help: see issue #48214 = help: see issue #48214

View file

@ -4,6 +4,16 @@ error[E0277]: the trait bound `bool: IntoResponse` is not satisfied
4 | async fn handler() -> bool { 4 | async fn handler() -> bool {
| ^^^^ the trait `IntoResponse` is not implemented for `bool` | ^^^^ the trait `IntoResponse` is not implemented for `bool`
| |
= help: the following other types implement trait `IntoResponse`:
&'static [u8]
&'static str
()
(Response<()>, R)
(Response<()>, T1, R)
(Response<()>, T1, T2, R)
(Response<()>, T1, T2, T3, R)
(Response<()>, T1, T2, T3, T4, R)
and 123 others
note: required by a bound in `__axum_macros_check_handler_into_response::{closure#0}::check` 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 --> tests/debug_handler/fail/wrong_return_type.rs:4:23
| |

View file

@ -4,6 +4,16 @@ error[E0277]: the trait bound `for<'de> MyPath: serde::de::Deserialize<'de>` is
3 | #[derive(TypedPath)] 3 | #[derive(TypedPath)]
| ^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `MyPath` | ^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `MyPath`
| |
= help: the following other types implement trait `serde::de::Deserialize<'de>`:
&'a [u8]
&'a serde_json::raw::RawValue
&'a std::path::Path
&'a str
()
(T0, T1)
(T0, T1, T2)
(T0, T1, T2, T3)
and 138 others
= note: required because of the requirements on the impl of `serde::de::DeserializeOwned` for `MyPath` = note: required because of the requirements on the impl of `serde::de::DeserializeOwned` for `MyPath`
= note: required because of the requirements on the impl of `FromRequest<B>` for `axum::extract::Path<MyPath>` = note: required because of the requirements on the impl of `FromRequest<B>` for `axum::extract::Path<MyPath>`
= note: this error originates in the derive macro `TypedPath` (in Nightly builds, run with -Z macro-backtrace for more info) = note: this error originates in the derive macro `TypedPath` (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -27,11 +27,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`/foo/`). That is no longer supported and such requests will now be sent to `/foo/`). That is no longer supported and such requests will now be sent to
the fallback. Consider using `axum_extra::routing::RouterExt::route_with_tsr` the fallback. Consider using `axum_extra::routing::RouterExt::route_with_tsr`
if you want the old behavior ([#1119]) if you want the old behavior ([#1119])
- **added** Implement `TryFrom<http:: Method>` for `MethodFilter` and use new `NoMatchingMethodFilter` error in case of failure ([#1130])
- **added:** Support running extractors from `middleware::from_fn` functions ([#1088])
[#1077]: https://github.com/tokio-rs/axum/pull/1077 [#1077]: https://github.com/tokio-rs/axum/pull/1077
[#1086]: https://github.com/tokio-rs/axum/pull/1086 [#1086]: https://github.com/tokio-rs/axum/pull/1086
[#1088]: https://github.com/tokio-rs/axum/pull/1088
[#1102]: https://github.com/tokio-rs/axum/pull/1102 [#1102]: https://github.com/tokio-rs/axum/pull/1102
[#1119]: https://github.com/tokio-rs/axum/pull/1119 [#1119]: https://github.com/tokio-rs/axum/pull/1119
[#1130]: https://github.com/tokio-rs/axum/pull/1130
[#924]: https://github.com/tokio-rs/axum/pull/924 [#924]: https://github.com/tokio-rs/axum/pull/924
# 0.5.10 (28. June, 2022) # 0.5.10 (28. June, 2022)

View file

@ -23,6 +23,12 @@ In particular the last point is what sets `axum` apart from other frameworks.
authorization, and more, for free. It also enables you to share middleware with authorization, and more, for free. It also enables you to share middleware with
applications written using [`hyper`] or [`tonic`]. applications written using [`hyper`] or [`tonic`].
## Breaking changes
We are currently working towards axum 0.6 so the `main` branch contains breaking
changes. See the [`0.5.x`] branch for whats released to crates.io and up to date
changelogs.
## Usage example ## Usage example
```rust ```rust
@ -159,3 +165,4 @@ additional terms or conditions.
[showcases]: https://github.com/tokio-rs/axum/blob/main/ECOSYSTEM.md#project-showcase [showcases]: https://github.com/tokio-rs/axum/blob/main/ECOSYSTEM.md#project-showcase
[tutorials]: https://github.com/tokio-rs/axum/blob/main/ECOSYSTEM.md#tutorials [tutorials]: https://github.com/tokio-rs/axum/blob/main/ECOSYSTEM.md#tutorials
[license]: https://github.com/tokio-rs/axum/blob/main/axum/LICENSE [license]: https://github.com/tokio-rs/axum/blob/main/axum/LICENSE
[`0.5.x`]: https://github.com/tokio-rs/axum/tree/0.5.x

View file

@ -12,6 +12,7 @@ Types and traits for extracting data from requests.
- [Defining custom extractors](#defining-custom-extractors) - [Defining custom extractors](#defining-custom-extractors)
- [Accessing other extractors in `FromRequest` implementations](#accessing-other-extractors-in-fromrequest-implementations) - [Accessing other extractors in `FromRequest` implementations](#accessing-other-extractors-in-fromrequest-implementations)
- [Request body extractors](#request-body-extractors) - [Request body extractors](#request-body-extractors)
- [Running extractors from middleware](#running-extractors-from-middleware)
# Intro # Intro
@ -579,6 +580,62 @@ let app = Router::new()
# }; # };
``` ```
# Running extractors from middleware
Extractors can also be run from middleware by making a [`RequestParts`] and
running your extractor:
```rust
use axum::{
Router,
middleware::{self, Next},
extract::{RequestParts, TypedHeader},
http::{Request, StatusCode},
response::Response,
headers::authorization::{Authorization, Bearer},
};
async fn auth_middleware<B>(
request: Request<B>,
next: Next<B>,
) -> Result<Response, StatusCode>
where
B: Send,
{
// running extractors requires a `RequestParts`
let mut request_parts = RequestParts::new(request);
// `TypedHeader<Authorization<Bearer>>` extracts the auth token but
// `RequestParts::extract` works with anything that implements `FromRequest`
let auth = request_parts.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
if !token_is_valid(auth.token()) {
return Err(StatusCode::UNAUTHORIZED);
}
// get the request back so we can run `next`
//
// `try_into_request` will fail if you have extracted the request body. We
// know that `TypedHeader` never does that.
//
// see the `consume-body-in-extractor-or-middleware` example if you need to
// extract the body
let request = request_parts.try_into_request().expect("body extracted");
Ok(next.run(request).await)
}
fn token_is_valid(token: &str) -> bool {
// ...
# false
}
let app = Router::new().layer(middleware::from_fn(auth_middleware));
# let _: Router = app;
```
[`body::Body`]: crate::body::Body [`body::Body`]: crate::body::Body
[customize-extractor-error]: https://github.com/tokio-rs/axum/blob/main/examples/customize-extractor-error/src/main.rs [customize-extractor-error]: https://github.com/tokio-rs/axum/blob/main/examples/customize-extractor-error/src/main.rs
[`HeaderMap`]: https://docs.rs/http/latest/http/header/struct.HeaderMap.html [`HeaderMap`]: https://docs.rs/http/latest/http/header/struct.HeaderMap.html

View file

@ -3,13 +3,15 @@ use crate::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
BoxError, BoxError,
}; };
use axum_core::extract::{FromRequest, RequestParts};
use futures_util::future::BoxFuture;
use http::Request; use http::Request;
use pin_project_lite::pin_project;
use std::{ use std::{
any::type_name, any::type_name,
convert::Infallible, convert::Infallible,
fmt, fmt,
future::Future, future::Future,
marker::PhantomData,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
@ -23,8 +25,8 @@ use tower_service::Service;
/// `from_fn` requires the function given to /// `from_fn` requires the function given to
/// ///
/// 1. Be an `async fn`. /// 1. Be an `async fn`.
/// 2. Take [`Request<B>`](http::Request) as the first argument. /// 2. Take one or more [extractors] as the first arguments.
/// 3. Take [`Next<B>`](Next) as the second argument. /// 3. Take [`Next<B>`](Next) as the final argument.
/// 4. Return something that implements [`IntoResponse`]. /// 4. Return something that implements [`IntoResponse`].
/// ///
/// # Example /// # Example
@ -62,6 +64,37 @@ use tower_service::Service;
/// # let app: Router = app; /// # let app: Router = app;
/// ``` /// ```
/// ///
/// # Running extractors
///
/// ```rust
/// use axum::{
/// Router,
/// extract::{TypedHeader, Query},
/// headers::authorization::{Authorization, Bearer},
/// http::Request,
/// middleware::{self, Next},
/// response::Response,
/// routing::get,
/// };
/// use std::collections::HashMap;
///
/// async fn my_middleware<B>(
/// TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
/// Query(query_params): Query<HashMap<String, String>>,
/// req: Request<B>,
/// next: Next<B>,
/// ) -> Response {
/// // do something with `auth` and `query_params`...
///
/// next.run(req).await
/// }
///
/// let app = Router::new()
/// .route("/", get(|| async { /* ... */ }))
/// .route_layer(middleware::from_fn(my_middleware));
/// # let app: Router = app;
/// ```
///
/// # Passing state /// # Passing state
/// ///
/// State can be passed to the function like so: /// State can be passed to the function like so:
@ -114,11 +147,10 @@ use tower_service::Service;
/// struct State { /* ... */ } /// struct State { /* ... */ }
/// ///
/// async fn my_middleware<B>( /// async fn my_middleware<B>(
/// Extension(state): Extension<State>,
/// req: Request<B>, /// req: Request<B>,
/// next: Next<B>, /// next: Next<B>,
/// ) -> Response { /// ) -> Response {
/// let state: &State = req.extensions().get().unwrap();
///
/// // ... /// // ...
/// # ().into_response() /// # ().into_response()
/// } /// }
@ -134,8 +166,13 @@ use tower_service::Service;
/// ); /// );
/// # let app: Router = app; /// # let app: Router = app;
/// ``` /// ```
pub fn from_fn<F>(f: F) -> FromFnLayer<F> { ///
FromFnLayer { f } /// [extractors]: crate::extract::FromRequest
pub fn from_fn<F, T>(f: F) -> FromFnLayer<F, T> {
FromFnLayer {
f,
_extractor: PhantomData,
}
} }
/// A [`tower::Layer`] from an async function. /// A [`tower::Layer`] from an async function.
@ -143,26 +180,41 @@ pub fn from_fn<F>(f: F) -> FromFnLayer<F> {
/// [`tower::Layer`] is used to apply middleware to [`Router`](crate::Router)'s. /// [`tower::Layer`] is used to apply middleware to [`Router`](crate::Router)'s.
/// ///
/// Created with [`from_fn`]. See that function for more details. /// Created with [`from_fn`]. See that function for more details.
#[derive(Clone, Copy)] pub struct FromFnLayer<F, T> {
pub struct FromFnLayer<F> {
f: F, f: F,
_extractor: PhantomData<fn() -> T>,
} }
impl<S, F> Layer<S> for FromFnLayer<F> impl<F, T> Clone for FromFnLayer<F, T>
where where
F: Clone, F: Clone,
{ {
type Service = FromFn<F, S>; fn clone(&self) -> Self {
Self {
f: self.f.clone(),
_extractor: self._extractor,
}
}
}
impl<F, T> Copy for FromFnLayer<F, T> where F: Copy {}
impl<S, F, T> Layer<S> for FromFnLayer<F, T>
where
F: Clone,
{
type Service = FromFn<F, S, T>;
fn layer(&self, inner: S) -> Self::Service { fn layer(&self, inner: S) -> Self::Service {
FromFn { FromFn {
f: self.f.clone(), f: self.f.clone(),
inner, inner,
_extractor: PhantomData,
} }
} }
} }
impl<F> fmt::Debug for FromFnLayer<F> { impl<F, T> fmt::Debug for FromFnLayer<F, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FromFnLayer") f.debug_struct("FromFnLayer")
// Write out the type name, without quoting it as `&type_name::<F>()` would // Write out the type name, without quoting it as `&type_name::<F>()` would
@ -174,50 +226,94 @@ impl<F> fmt::Debug for FromFnLayer<F> {
/// A middleware created from an async function. /// A middleware created from an async function.
/// ///
/// Created with [`from_fn`]. See that function for more details. /// Created with [`from_fn`]. See that function for more details.
#[derive(Clone, Copy)] pub struct FromFn<F, S, T> {
pub struct FromFn<F, S> {
f: F, f: F,
inner: S, inner: S,
_extractor: PhantomData<fn() -> T>,
} }
impl<F, Fut, Out, S, ReqBody, ResBody> Service<Request<ReqBody>> for FromFn<F, S> impl<F, S, T> Clone for FromFn<F, S, T>
where where
F: FnMut(Request<ReqBody>, Next<ReqBody>) -> Fut, F: Clone,
Fut: Future<Output = Out>, S: Clone,
Out: IntoResponse,
S: Service<Request<ReqBody>, Response = Response<ResBody>, Error = Infallible>
+ Clone
+ Send
+ 'static,
S::Future: Send + 'static,
ResBody: HttpBody<Data = Bytes> + Send + 'static,
ResBody::Error: Into<BoxError>,
{ {
type Response = Response; fn clone(&self) -> Self {
type Error = Infallible; Self {
type Future = ResponseFuture<Fut>; f: self.f.clone(),
inner: self.inner.clone(),
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { _extractor: self._extractor,
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let not_ready_inner = self.inner.clone();
let ready_inner = std::mem::replace(&mut self.inner, not_ready_inner);
let inner = ServiceBuilder::new()
.boxed_clone()
.map_response_body(body::boxed)
.service(ready_inner);
let next = Next { inner };
ResponseFuture {
inner: (self.f)(req, next),
} }
} }
} }
impl<F, S> fmt::Debug for FromFn<F, S> impl<F, S, T> Copy for FromFn<F, S, T>
where
F: Copy,
S: Copy,
{
}
macro_rules! impl_service {
( $($ty:ident),* $(,)? ) => {
#[allow(non_snake_case)]
impl<F, Fut, Out, S, ReqBody, ResBody, $($ty,)*> Service<Request<ReqBody>> for FromFn<F, S, ($($ty,)*)>
where
F: FnMut($($ty),*, Next<ReqBody>) -> Fut + Clone + Send + 'static,
$( $ty: FromRequest<ReqBody> + Send, )*
Fut: Future<Output = Out> + Send + 'static,
Out: IntoResponse + 'static,
S: Service<Request<ReqBody>, Response = Response<ResBody>, Error = Infallible>
+ Clone
+ Send
+ 'static,
S::Future: Send + 'static,
ReqBody: Send + 'static,
ResBody: HttpBody<Data = Bytes> + Send + 'static,
ResBody::Error: Into<BoxError>,
{
type Response = Response;
type Error = Infallible;
type Future = ResponseFuture;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let not_ready_inner = self.inner.clone();
let ready_inner = std::mem::replace(&mut self.inner, not_ready_inner);
let mut f = self.f.clone();
let future = Box::pin(async move {
let mut parts = RequestParts::new(req);
$(
let $ty = match $ty::from_request(&mut parts).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
)*
let inner = ServiceBuilder::new()
.boxed_clone()
.map_response_body(body::boxed)
.service(ready_inner);
let next = Next { inner };
f($($ty),*, next).await.into_response()
});
ResponseFuture {
inner: future
}
}
}
};
}
all_the_tuples!(impl_service);
impl<F, S, T> fmt::Debug for FromFn<F, S, T>
where where
S: fmt::Debug, S: fmt::Debug,
{ {
@ -252,27 +348,22 @@ impl<ReqBody> fmt::Debug for Next<ReqBody> {
} }
} }
pin_project! { /// Response future for [`FromFn`].
/// Response future for [`FromFn`]. pub struct ResponseFuture {
pub struct ResponseFuture<F> { inner: BoxFuture<'static, Response>,
#[pin] }
inner: F,
impl Future for ResponseFuture {
type Output = Result<Response, Infallible>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.inner.as_mut().poll(cx).map(Ok)
} }
} }
impl<F, Out> Future for ResponseFuture<F> impl fmt::Debug for ResponseFuture {
where fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
F: Future<Output = Out>, f.debug_struct("ResponseFuture").finish()
Out: IntoResponse,
{
type Output = Result<Response, Infallible>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.project()
.inner
.poll(cx)
.map(IntoResponse::into_response)
.map(Ok)
} }
} }

View file

@ -1,4 +1,9 @@
use bitflags::bitflags; use bitflags::bitflags;
use http::Method;
use std::{
fmt,
fmt::{Debug, Formatter},
};
bitflags! { bitflags! {
/// A filter that matches one or more HTTP methods. /// A filter that matches one or more HTTP methods.
@ -21,3 +26,95 @@ bitflags! {
const TRACE = 0b100000000; const TRACE = 0b100000000;
} }
} }
/// Error type used when converting a [`Method`] to a [`MethodFilter`] fails.
#[derive(Debug)]
pub struct NoMatchingMethodFilter {
method: Method,
}
impl NoMatchingMethodFilter {
/// Get the [`Method`] that couldn't be converted to a [`MethodFilter`].
pub fn method(&self) -> &Method {
&self.method
}
}
impl fmt::Display for NoMatchingMethodFilter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "no `MethodFilter` for `{}`", self.method.as_str())
}
}
impl std::error::Error for NoMatchingMethodFilter {}
impl TryFrom<Method> for MethodFilter {
type Error = NoMatchingMethodFilter;
fn try_from(m: Method) -> Result<Self, NoMatchingMethodFilter> {
match m {
Method::DELETE => Ok(MethodFilter::DELETE),
Method::GET => Ok(MethodFilter::GET),
Method::HEAD => Ok(MethodFilter::HEAD),
Method::OPTIONS => Ok(MethodFilter::OPTIONS),
Method::PATCH => Ok(MethodFilter::PATCH),
Method::POST => Ok(MethodFilter::POST),
Method::PUT => Ok(MethodFilter::PUT),
Method::TRACE => Ok(MethodFilter::TRACE),
other => Err(NoMatchingMethodFilter { method: other }),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_http_method() {
assert_eq!(
MethodFilter::try_from(Method::DELETE).unwrap(),
MethodFilter::DELETE
);
assert_eq!(
MethodFilter::try_from(Method::GET).unwrap(),
MethodFilter::GET
);
assert_eq!(
MethodFilter::try_from(Method::HEAD).unwrap(),
MethodFilter::HEAD
);
assert_eq!(
MethodFilter::try_from(Method::OPTIONS).unwrap(),
MethodFilter::OPTIONS
);
assert_eq!(
MethodFilter::try_from(Method::PATCH).unwrap(),
MethodFilter::PATCH
);
assert_eq!(
MethodFilter::try_from(Method::POST).unwrap(),
MethodFilter::POST
);
assert_eq!(
MethodFilter::try_from(Method::PUT).unwrap(),
MethodFilter::PUT
);
assert_eq!(
MethodFilter::try_from(Method::TRACE).unwrap(),
MethodFilter::TRACE
);
assert!(MethodFilter::try_from(http::Method::CONNECT)
.unwrap_err()
.to_string()
.contains("CONNECT"));
}
}