Type safe state inheritance (#1532)

* Make state type safe

* fix examples

* remove unnecessary `#[track_caller]`s

* Router::into_service -> Router::with_state

* fixup docs

* macro docs

* add missing docs

* fix examples

* format

* changelog

* Update trybuild tests

* Make sure fallbacks are still inherited for opaque services (#1540)

* Document nesting routers with different state

* fix leftover conflicts
This commit is contained in:
David Pedersen
2022-11-18 11:02:58 +00:00
committed by GitHub
parent ba8e9c1b21
commit 64960bb19c
62 changed files with 675 additions and 736 deletions
+22
View File
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
# Unreleased
- **breaking:** `Router::with_state` is no longer a constructor. It is instead
used to convert the router into a `RouterService` ([#1532])
This nested router on 0.6.0-rc.4
```rust
Router::with_state(state).route(...);
```
Becomes this in 0.6.0-rc.5
```rust
Router::new().route(...).with_state(state);
```
- **breaking:**: `Router::nest` and `Router::merge` now only supports nesting
routers that use the same state type as the router they're being merged into.
Use `FromRef` for substates ([#1532])
- **added:** Add `accept_unmasked_frames` setting in WebSocketUpgrade ([#1529])
- **fixed:** Nested routers will now inherit fallbacks from outer routers ([#1521])
- **added:** Add `accept_unmasked_frames` setting in WebSocketUpgrade ([#1529])
- **added:** Add `WebSocketUpgrade::on_failed_upgrade` to customize what to do
@@ -15,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#1539]: https://github.com/tokio-rs/axum/pull/1539
[#1521]: https://github.com/tokio-rs/axum/pull/1521
[#1529]: https://github.com/tokio-rs/axum/pull/1529
[#1532]: https://github.com/tokio-rs/axum/pull/1532
# 0.6.0-rc.4 (9. November, 2022)
+33 -20
View File
@@ -1,7 +1,7 @@
use axum::{
extract::State,
routing::{get, post},
Extension, Json, Router, Server,
Extension, Json, Router, RouterService, Server,
};
use hyper::server::conn::AddrIncoming;
use serde::{Deserialize, Serialize};
@@ -17,9 +17,13 @@ fn main() {
ensure_rewrk_is_installed();
}
benchmark("minimal").run(Router::new);
benchmark("minimal").run(|| Router::new().into_service());
benchmark("basic").run(|| Router::new().route("/", get(|| async { "Hello, World!" })));
benchmark("basic").run(|| {
Router::new()
.route("/", get(|| async { "Hello, World!" }))
.into_service()
});
benchmark("routing").path("/foo/bar/baz").run(|| {
let mut app = Router::new();
@@ -30,26 +34,32 @@ fn main() {
}
}
}
app.route("/foo/bar/baz", get(|| async {}))
app.route("/foo/bar/baz", get(|| async {})).into_service()
});
benchmark("receive-json")
.method("post")
.headers(&[("content-type", "application/json")])
.body(r#"{"n": 123, "s": "hi there", "b": false}"#)
.run(|| Router::new().route("/", post(|_: Json<Payload>| async {})));
.run(|| {
Router::new()
.route("/", post(|_: Json<Payload>| async {}))
.into_service()
});
benchmark("send-json").run(|| {
Router::new().route(
"/",
get(|| async {
Json(Payload {
n: 123,
s: "hi there".to_owned(),
b: false,
})
}),
)
Router::new()
.route(
"/",
get(|| async {
Json(Payload {
n: 123,
s: "hi there".to_owned(),
b: false,
})
}),
)
.into_service()
});
let state = AppState {
@@ -65,10 +75,14 @@ fn main() {
Router::new()
.route("/", get(|_: Extension<AppState>| async {}))
.layer(Extension(state.clone()))
.into_service()
});
benchmark("state")
.run(|| Router::with_state(state.clone()).route("/", get(|_: State<AppState>| async {})));
benchmark("state").run(|| {
Router::new()
.route("/", get(|_: State<AppState>| async {}))
.with_state(state.clone())
});
}
#[derive(Clone)]
@@ -117,10 +131,9 @@ impl BenchmarkBuilder {
config_method!(headers, &'static [(&'static str, &'static str)]);
config_method!(body, &'static str);
fn run<F, S>(self, f: F)
fn run<F>(self, f: F)
where
F: FnOnce() -> Router<S>,
S: Clone + Send + Sync + 'static,
F: FnOnce() -> RouterService,
{
// support only running some benchmarks with
// ```
+173
View File
@@ -0,0 +1,173 @@
use std::{convert::Infallible, fmt};
use crate::{body::HttpBody, handler::Handler, routing::Route, Router};
pub(crate) struct BoxedIntoRoute<S, B, E>(Box<dyn ErasedIntoRoute<S, B, E>>);
impl<S, B> BoxedIntoRoute<S, B, Infallible>
where
S: Clone + Send + Sync + 'static,
B: Send + 'static,
{
pub(crate) fn from_handler<H, T>(handler: H) -> Self
where
H: Handler<T, S, B>,
T: 'static,
{
Self(Box::new(MakeErasedHandler {
handler,
into_route: |handler, state| Route::new(Handler::with_state(handler, state)),
}))
}
pub(crate) fn from_router(router: Router<S, B>) -> Self
where
B: HttpBody + Send + 'static,
S: Clone + Send + Sync + 'static,
{
Self(Box::new(MakeErasedRouter {
router,
into_route: |router, state| Route::new(router.with_state(state)),
}))
}
}
impl<S, B, E> BoxedIntoRoute<S, B, E> {
pub(crate) fn map<F, B2, E2>(self, f: F) -> BoxedIntoRoute<S, B2, E2>
where
S: 'static,
B: 'static,
E: 'static,
F: FnOnce(Route<B, E>) -> Route<B2, E2> + Clone + Send + 'static,
B2: 'static,
E2: 'static,
{
BoxedIntoRoute(Box::new(Map {
inner: self.0,
layer: Box::new(f),
}))
}
pub(crate) fn into_route(self, state: S) -> Route<B, E> {
self.0.into_route(state)
}
}
impl<S, B, E> Clone for BoxedIntoRoute<S, B, E> {
fn clone(&self) -> Self {
Self(self.0.clone_box())
}
}
impl<S, B, E> fmt::Debug for BoxedIntoRoute<S, B, E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("BoxedIntoRoute").finish()
}
}
pub(crate) trait ErasedIntoRoute<S, B, E>: Send {
fn clone_box(&self) -> Box<dyn ErasedIntoRoute<S, B, E>>;
fn into_route(self: Box<Self>, state: S) -> Route<B, E>;
}
pub(crate) struct MakeErasedHandler<H, S, B> {
pub(crate) handler: H,
pub(crate) into_route: fn(H, S) -> Route<B>,
}
impl<H, S, B> ErasedIntoRoute<S, B, Infallible> for MakeErasedHandler<H, S, B>
where
H: Clone + Send + 'static,
S: 'static,
B: 'static,
{
fn clone_box(&self) -> Box<dyn ErasedIntoRoute<S, B, Infallible>> {
Box::new(self.clone())
}
fn into_route(self: Box<Self>, state: S) -> Route<B> {
(self.into_route)(self.handler, state)
}
}
impl<H, S, B> Clone for MakeErasedHandler<H, S, B>
where
H: Clone,
{
fn clone(&self) -> Self {
Self {
handler: self.handler.clone(),
into_route: self.into_route,
}
}
}
pub(crate) struct MakeErasedRouter<S, B> {
pub(crate) router: Router<S, B>,
pub(crate) into_route: fn(Router<S, B>, S) -> Route<B>,
}
impl<S, B> ErasedIntoRoute<S, B, Infallible> for MakeErasedRouter<S, B>
where
S: Clone + Send + 'static,
B: 'static,
{
fn clone_box(&self) -> Box<dyn ErasedIntoRoute<S, B, Infallible>> {
Box::new(self.clone())
}
fn into_route(self: Box<Self>, state: S) -> Route<B> {
(self.into_route)(self.router, state)
}
}
impl<S, B> Clone for MakeErasedRouter<S, B>
where
S: Clone,
{
fn clone(&self) -> Self {
Self {
router: self.router.clone(),
into_route: self.into_route,
}
}
}
pub(crate) struct Map<S, B, E, B2, E2> {
pub(crate) inner: Box<dyn ErasedIntoRoute<S, B, E>>,
pub(crate) layer: Box<dyn LayerFn<B, E, B2, E2>>,
}
impl<S, B, E, B2, E2> ErasedIntoRoute<S, B2, E2> for Map<S, B, E, B2, E2>
where
S: 'static,
B: 'static,
E: 'static,
B2: 'static,
E2: 'static,
{
fn clone_box(&self) -> Box<dyn ErasedIntoRoute<S, B2, E2>> {
Box::new(Self {
inner: self.inner.clone_box(),
layer: self.layer.clone_box(),
})
}
fn into_route(self: Box<Self>, state: S) -> Route<B2, E2> {
(self.layer)(self.inner.into_route(state))
}
}
pub(crate) trait LayerFn<B, E, B2, E2>: FnOnce(Route<B, E>) -> Route<B2, E2> + Send {
fn clone_box(&self) -> Box<dyn LayerFn<B, E, B2, E2>>;
}
impl<F, B, E, B2, E2> LayerFn<B, E, B2, E2> for F
where
F: FnOnce(Route<B, E>) -> Route<B2, E2> + Clone + Send + 'static,
{
fn clone_box(&self) -> Box<dyn LayerFn<B, E, B2, E2>> {
Box::new(self.clone())
}
}
+4 -3
View File
@@ -462,10 +462,11 @@ async fn handler(_: State<AppState>) {}
let state = AppState {};
let app = Router::with_state(state.clone())
let app = Router::new()
.route("/", get(handler))
.layer(MyLayer { state });
# let _: Router<_> = app;
.layer(MyLayer { state: state.clone() })
.with_state(state);
# let _: axum::routing::RouterService = app;
```
# Passing state from middleware to handlers
@@ -2,6 +2,10 @@ Convert this router into a [`MakeService`], that will store `C`'s
associated `ConnectInfo` in a request extension such that [`ConnectInfo`]
can extract it.
This is a convenience method for routers that don't have any state (i.e. the
state type is `()`). Use [`RouterService::into_make_service_with_connect_info`]
otherwise.
This enables extracting things like the client's remote address.
Extracting [`std::net::SocketAddr`] is supported out of the box:
+37
View File
@@ -147,6 +147,43 @@ let app = Router::new()
Here requests like `GET /api/not-found` will go to `api_fallback`.
# Nesting a router with a different state type
By default `nest` requires a `Router` with the same state type as the outer
`Router`. If you need to nest a `Router` with a different state type you can
use [`Router::with_state`] and [`Router::nest_service`]:
```rust
use axum::{
Router,
routing::get,
extract::State,
};
#[derive(Clone)]
struct InnerState {}
#[derive(Clone)]
struct OuterState {}
async fn inner_handler(state: State<InnerState>) {}
let inner_router = Router::new()
.route("/bar", get(inner_handler))
.with_state(InnerState {});
async fn outer_handler(state: State<OuterState>) {}
let app = Router::new()
.route("/", get(outer_handler))
.nest_service("/foo", inner_router)
.with_state(OuterState {});
# let _: axum::routing::RouterService = app;
```
Note that the inner router will still inherit the fallback from the outer
router.
# Panics
- If the route overlaps with another route. See [`Router::route`]
+11 -5
View File
@@ -31,7 +31,10 @@ use std::{
/// let state = AppState {};
///
/// // create a `Router` that holds our state
/// let app = Router::with_state(state).route("/", get(handler));
/// let app = Router::new()
/// .route("/", get(handler))
/// // provide the state so the router can access it
/// .with_state(state);
///
/// async fn handler(
/// // access the state via the `State` extractor
@@ -40,7 +43,7 @@ use std::{
/// ) {
/// // use `state`...
/// }
/// # let _: Router<AppState> = app;
/// # let _: axum::routing::RouterService = app;
/// ```
///
/// # With `MethodRouter`
@@ -119,9 +122,10 @@ use std::{
/// api_state: ApiState {},
/// };
///
/// let app = Router::with_state(state)
/// let app = Router::new()
/// .route("/", get(handler))
/// .route("/api/users", get(api_users));
/// .route("/api/users", get(api_users))
/// .with_state(state);
///
/// async fn api_users(
/// // access the api specific state
@@ -134,9 +138,11 @@ use std::{
/// State(state): State<AppState>,
/// ) {
/// }
/// # let _: Router<AppState> = app;
/// # let _: axum::routing::RouterService = app;
/// ```
///
/// For convenience `FromRef` can also be derived using `#[derive(FromRef)]`.
///
/// # For library authors
///
/// If you're writing a library that has an extractor that needs state, this is the recommended way
-123
View File
@@ -1,123 +0,0 @@
use std::convert::Infallible;
use super::Handler;
use crate::routing::Route;
pub(crate) struct BoxedHandler<S, B, E = Infallible>(Box<dyn ErasedHandler<S, B, E>>);
impl<S, B> BoxedHandler<S, B>
where
S: Clone + Send + Sync + 'static,
B: Send + 'static,
{
pub(crate) fn new<H, T>(handler: H) -> Self
where
H: Handler<T, S, B>,
T: 'static,
{
Self(Box::new(MakeErasedHandler {
handler,
into_route: |handler, state| Route::new(Handler::with_state(handler, state)),
}))
}
}
impl<S, B, E> BoxedHandler<S, B, E> {
pub(crate) fn map<F, B2, E2>(self, f: F) -> BoxedHandler<S, B2, E2>
where
S: 'static,
B: 'static,
E: 'static,
F: FnOnce(Route<B, E>) -> Route<B2, E2> + Clone + Send + 'static,
B2: 'static,
E2: 'static,
{
BoxedHandler(Box::new(Map {
handler: self.0,
layer: Box::new(f),
}))
}
pub(crate) fn into_route(self, state: S) -> Route<B, E> {
self.0.into_route(state)
}
}
impl<S, B, E> Clone for BoxedHandler<S, B, E> {
fn clone(&self) -> Self {
Self(self.0.clone_box())
}
}
trait ErasedHandler<S, B, E = Infallible>: Send {
fn clone_box(&self) -> Box<dyn ErasedHandler<S, B, E>>;
fn into_route(self: Box<Self>, state: S) -> Route<B, E>;
}
struct MakeErasedHandler<H, S, B> {
handler: H,
into_route: fn(H, S) -> Route<B>,
}
impl<H, S, B> ErasedHandler<S, B> for MakeErasedHandler<H, S, B>
where
H: Clone + Send + 'static,
S: 'static,
B: 'static,
{
fn clone_box(&self) -> Box<dyn ErasedHandler<S, B>> {
Box::new(self.clone())
}
fn into_route(self: Box<Self>, state: S) -> Route<B> {
(self.into_route)(self.handler, state)
}
}
impl<H: Clone, S, B> Clone for MakeErasedHandler<H, S, B> {
fn clone(&self) -> Self {
Self {
handler: self.handler.clone(),
into_route: self.into_route,
}
}
}
struct Map<S, B, E, B2, E2> {
handler: Box<dyn ErasedHandler<S, B, E>>,
layer: Box<dyn LayerFn<B, E, B2, E2>>,
}
impl<S, B, E, B2, E2> ErasedHandler<S, B2, E2> for Map<S, B, E, B2, E2>
where
S: 'static,
B: 'static,
E: 'static,
B2: 'static,
E2: 'static,
{
fn clone_box(&self) -> Box<dyn ErasedHandler<S, B2, E2>> {
Box::new(Self {
handler: self.handler.clone_box(),
layer: self.layer.clone_box(),
})
}
fn into_route(self: Box<Self>, state: S) -> Route<B2, E2> {
(self.layer)(self.handler.into_route(state))
}
}
trait LayerFn<B, E, B2, E2>: FnOnce(Route<B, E>) -> Route<B2, E2> + Send {
fn clone_box(&self) -> Box<dyn LayerFn<B, E, B2, E2>>;
}
impl<F, B, E, B2, E2> LayerFn<B, E, B2, E2> for F
where
F: FnOnce(Route<B, E>) -> Route<B2, E2> + Clone + Send + 'static,
{
fn clone_box(&self) -> Box<dyn LayerFn<B, E, B2, E2>> {
Box::new(self.clone())
}
}
-2
View File
@@ -49,11 +49,9 @@ use tower::ServiceExt;
use tower_layer::Layer;
use tower_service::Service;
mod boxed;
pub mod future;
mod service;
pub(crate) use self::boxed::BoxedHandler;
pub use self::service::HandlerService;
/// Trait for async functions that can be used to handle requests.
+4 -2
View File
@@ -188,8 +188,9 @@
//!
//! let shared_state = Arc::new(AppState { /* ... */ });
//!
//! let app = Router::with_state(shared_state)
//! .route("/", get(handler));
//! let app = Router::new()
//! .route("/", get(handler))
//! .with_state(shared_state);
//!
//! async fn handler(
//! State(state): State<Arc<AppState>>,
@@ -434,6 +435,7 @@
#[macro_use]
pub(crate) mod macros;
mod boxed;
mod extension;
#[cfg(feature = "form")]
mod form;
+4 -3
View File
@@ -133,10 +133,11 @@ pub fn from_fn<F, T>(f: F) -> FromFnLayer<F, (), T> {
///
/// let state = AppState { /* ... */ };
///
/// let app = Router::with_state(state.clone())
/// let app = Router::new()
/// .route("/", get(|| async { /* ... */ }))
/// .route_layer(middleware::from_fn_with_state(state, my_middleware));
/// # let app: Router<_> = app;
/// .route_layer(middleware::from_fn_with_state(state.clone(), my_middleware))
/// .with_state(state);
/// # let _: axum::routing::RouterService = app;
/// ```
pub fn from_fn_with_state<F, S, T>(state: S, f: F) -> FromFnLayer<F, S, T> {
FromFnLayer {
+4 -3
View File
@@ -148,10 +148,11 @@ pub fn map_request<F, T>(f: F) -> MapRequestLayer<F, (), T> {
///
/// let state = AppState { /* ... */ };
///
/// let app = Router::with_state(state.clone())
/// let app = Router::new()
/// .route("/", get(|| async { /* ... */ }))
/// .route_layer(map_request_with_state(state, my_middleware));
/// # let app: Router<_> = app;
/// .route_layer(map_request_with_state(state.clone(), my_middleware))
/// .with_state(state);
/// # let _: axum::routing::RouterService = app;
/// ```
pub fn map_request_with_state<F, S, T>(state: S, f: F) -> MapRequestLayer<F, S, T> {
MapRequestLayer {
+4 -3
View File
@@ -132,10 +132,11 @@ pub fn map_response<F, T>(f: F) -> MapResponseLayer<F, (), T> {
///
/// let state = AppState { /* ... */ };
///
/// let app = Router::with_state(state.clone())
/// let app = Router::new()
/// .route("/", get(|| async { /* ... */ }))
/// .route_layer(map_response_with_state(state, my_middleware));
/// # let app: Router<_> = app;
/// .route_layer(map_response_with_state(state.clone(), my_middleware))
/// .with_state(state);
/// # let _: axum::routing::RouterService = app;
/// ```
pub fn map_response_with_state<F, S, T>(state: S, f: F) -> MapResponseLayer<F, S, T> {
MapResponseLayer {
+2 -2
View File
@@ -98,7 +98,7 @@ mod tests {
}
}
Router::<_, Body>::new()
Router::<(), Body>::new()
.route("/", get(impl_trait_ok))
.route("/", get(impl_trait_err))
.route("/", get(impl_trait_both))
@@ -208,7 +208,7 @@ mod tests {
)
}
Router::<_, Body>::new()
Router::<(), Body>::new()
.route("/", get(status))
.route("/", get(status_headermap))
.route("/", get(status_header_array))
+8 -77
View File
@@ -5,12 +5,12 @@ use super::{FallbackRoute, IntoMakeService};
use crate::extract::connect_info::IntoMakeServiceWithConnectInfo;
use crate::{
body::{Body, Bytes, HttpBody},
boxed::BoxedIntoRoute,
error_handling::{HandleError, HandleErrorLayer},
handler::{BoxedHandler, Handler},
handler::Handler,
http::{Method, Request, StatusCode},
response::Response,
routing::{future::RouteFuture, Fallback, MethodFilter, Route},
util::try_downcast,
};
use axum_core::response::IntoResponse;
use bytes::BytesMut;
@@ -606,7 +606,7 @@ where
{
self.on_endpoint(
filter,
MethodEndpoint::BoxedHandler(BoxedHandler::new(handler)),
MethodEndpoint::BoxedHandler(BoxedIntoRoute::from_handler(handler)),
)
}
@@ -626,7 +626,7 @@ where
T: 'static,
S: Send + Sync + 'static,
{
self.fallback = Fallback::BoxedHandler(BoxedHandler::new(handler));
self.fallback = Fallback::BoxedHandler(BoxedIntoRoute::from_handler(handler));
self
}
}
@@ -749,46 +749,6 @@ where
}
}
pub(crate) fn map_state<S2>(self, state: &S) -> MethodRouter<S2, B, E>
where
E: 'static,
S: 'static,
S2: 'static,
{
MethodRouter {
get: self.get.map_state(state),
head: self.head.map_state(state),
delete: self.delete.map_state(state),
options: self.options.map_state(state),
patch: self.patch.map_state(state),
post: self.post.map_state(state),
put: self.put.map_state(state),
trace: self.trace.map_state(state),
fallback: self.fallback.map_state(state),
allow_header: self.allow_header,
}
}
pub(crate) fn downcast_state<S2>(self) -> Option<MethodRouter<S2, B, E>>
where
E: 'static,
S: 'static,
S2: 'static,
{
Some(MethodRouter {
get: self.get.downcast_state()?,
head: self.head.downcast_state()?,
delete: self.delete.downcast_state()?,
options: self.options.downcast_state()?,
patch: self.patch.downcast_state()?,
post: self.post.downcast_state()?,
put: self.put.downcast_state()?,
trace: self.trace.downcast_state()?,
fallback: self.fallback.downcast_state()?,
allow_header: self.allow_header,
})
}
/// Chain an additional service that will accept requests matching the given
/// `MethodFilter`.
///
@@ -964,17 +924,14 @@ where
) -> MethodRouter<S, NewReqBody, NewError>
where
L: Layer<Route<B, E>> + Clone + Send + 'static,
L::Service: Service<Request<NewReqBody>, Error = NewError> + Clone + Send + 'static,
L::Service: Service<Request<NewReqBody>> + Clone + Send + 'static,
<L::Service as Service<Request<NewReqBody>>>::Response: IntoResponse + 'static,
<L::Service as Service<Request<NewReqBody>>>::Error: Into<NewError> + 'static,
<L::Service as Service<Request<NewReqBody>>>::Future: Send + 'static,
E: 'static,
S: 'static,
{
let layer_fn = move |svc| {
let svc = layer.layer(svc);
let svc = MapResponseLayer::new(IntoResponse::into_response).layer(svc);
Route::new(svc)
};
let layer_fn = move |route: Route<B, E>| route.layer(layer.clone());
MethodRouter {
get: self.get.map(layer_fn.clone()),
@@ -1182,7 +1139,7 @@ where
enum MethodEndpoint<S, B, E> {
None,
Route(Route<B, E>),
BoxedHandler(BoxedHandler<S, B, E>),
BoxedHandler(BoxedIntoRoute<S, B, E>),
}
impl<S, B, E> MethodEndpoint<S, B, E>
@@ -1213,32 +1170,6 @@ where
}
}
fn map_state<S2>(self, state: &S) -> MethodEndpoint<S2, B, E> {
match self {
Self::None => MethodEndpoint::None,
Self::Route(route) => MethodEndpoint::Route(route),
Self::BoxedHandler(handler) => MethodEndpoint::Route(handler.into_route(state.clone())),
}
}
fn downcast_state<S2>(self) -> Option<MethodEndpoint<S2, B, E>>
where
S: 'static,
B: 'static,
E: 'static,
S2: 'static,
{
match self {
Self::None => Some(MethodEndpoint::None),
Self::Route(route) => Some(MethodEndpoint::Route(route)),
Self::BoxedHandler(handler) => {
try_downcast::<BoxedHandler<S2, B, E>, BoxedHandler<S, B, E>>(handler)
.map(MethodEndpoint::BoxedHandler)
.ok()
}
}
}
fn into_route(self, state: &S) -> Option<Route<B, E>> {
match self {
Self::None => None,
+137 -219
View File
@@ -1,27 +1,19 @@
//! Routing between [`Service`]s and handlers.
use self::not_found::NotFound;
use self::{not_found::NotFound, strip_prefix::StripPrefix};
#[cfg(feature = "tokio")]
use crate::extract::connect_info::IntoMakeServiceWithConnectInfo;
use crate::{
body::{Body, HttpBody},
handler::{BoxedHandler, Handler},
boxed::BoxedIntoRoute,
handler::Handler,
util::try_downcast,
};
use axum_core::response::{IntoResponse, Response};
use http::Request;
use matchit::MatchError;
use std::{
any::{type_name, TypeId},
collections::HashMap,
convert::Infallible,
fmt,
sync::Arc,
};
use tower::{
util::{BoxCloneService, MapResponseLayer, Oneshot},
ServiceBuilder,
};
use std::{collections::HashMap, convert::Infallible, fmt, sync::Arc};
use tower::util::{BoxCloneService, Oneshot};
use tower_layer::Layer;
use tower_service::Service;
@@ -68,19 +60,14 @@ impl RouteId {
/// The router type for composing handlers and services.
pub struct Router<S = (), B = Body> {
state: Option<S>,
routes: HashMap<RouteId, Endpoint<S, B>>,
node: Arc<Node>,
fallback: Fallback<S, B>,
}
impl<S, B> Clone for Router<S, B>
where
S: Clone,
{
impl<S, B> Clone for Router<S, B> {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
routes: self.routes.clone(),
node: Arc::clone(&self.node),
fallback: self.fallback.clone(),
@@ -91,10 +78,10 @@ where
impl<S, B> Default for Router<S, B>
where
B: HttpBody + Send + 'static,
S: Default + Clone + Send + Sync + 'static,
S: Clone + Send + Sync + 'static,
{
fn default() -> Self {
Self::with_state(S::default())
Self::new()
}
}
@@ -104,7 +91,6 @@ where
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Router")
.field("state", &self.state)
.field("routes", &self.routes)
.field("node", &self.node)
.field("fallback", &self.fallback)
@@ -115,71 +101,19 @@ where
pub(crate) const NEST_TAIL_PARAM: &str = "__private__axum_nest_tail_param";
pub(crate) const NEST_TAIL_PARAM_CAPTURE: &str = "/*__private__axum_nest_tail_param";
impl<B> Router<(), B>
where
B: HttpBody + Send + 'static,
{
/// Create a new `Router`.
///
/// Unless you add additional routes this will respond with `404 Not Found` to
/// all requests.
pub fn new() -> Self {
Self::with_state(())
}
}
impl<B> Router<(), B> where B: HttpBody + Send + 'static {}
impl<S, B> Router<S, B>
where
B: HttpBody + Send + 'static,
S: Clone + Send + Sync + 'static,
{
/// Create a new `Router` with the given state.
///
/// See [`State`](crate::extract::State) for more details about accessing state.
/// Create a new `Router`.
///
/// Unless you add additional routes this will respond with `404 Not Found` to
/// all requests.
pub fn with_state(state: S) -> Self {
pub fn new() -> Self {
Self {
state: Some(state),
routes: Default::default(),
node: Default::default(),
fallback: Fallback::Default(Route::new(NotFound)),
}
}
/// Create a new `Router` that inherits its state from another `Router` that it is merged into
/// or nested under.
///
/// # Example
///
/// ```
/// use axum::{Router, routing::get, extract::State};
///
/// #[derive(Clone)]
/// struct AppState {}
///
/// // A router that will be nested under the `app` router.
/// //
/// // By using `inherit_state` we'll reuse the state from the `app` router.
/// let nested_router = Router::inherit_state()
/// .route("/bar", get(|state: State<AppState>| async {}));
///
/// // A router that will be merged into the `app` router.
/// let merged_router = Router::inherit_state()
/// .route("/baz", get(|state: State<AppState>| async {}));
///
/// let app = Router::with_state(AppState {})
/// .route("/", get(|state: State<AppState>| async {}))
/// .nest("/foo", nested_router)
/// .merge(merged_router);
///
/// // `app` now has routes for `/`, `/foo/bar`, and `/baz` that all use the same state.
/// # let _: Router<AppState> = app;
/// ```
pub fn inherit_state() -> Self {
Self {
state: None,
routes: Default::default(),
node: Default::default(),
fallback: Fallback::Default(Route::new(NotFound)),
@@ -228,18 +162,12 @@ where
}
#[doc = include_str!("../docs/routing/route_service.md")]
pub fn route_service<T>(mut self, path: &str, service: T) -> Self
pub fn route_service<T>(self, path: &str, service: T) -> Self
where
T: Service<Request<B>, Error = Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,
{
if path.is_empty() {
panic!("Paths must start with a `/`. Use \"/\" for root routes");
} else if !path.starts_with('/') {
panic!("Paths must start with a `/`");
}
let service = match try_downcast::<RouterService<B>, _>(service) {
Ok(_) => {
panic!(
@@ -250,11 +178,20 @@ where
Err(svc) => svc,
};
self.route_endpoint(path, Endpoint::Route(Route::new(service)))
}
#[track_caller]
fn route_endpoint(mut self, path: &str, endpoint: Endpoint<S, B>) -> Self {
if path.is_empty() {
panic!("Paths must start with a `/`. Use \"/\" for root routes");
} else if !path.starts_with('/') {
panic!("Paths must start with a `/`");
}
let id = RouteId::next();
let endpoint = Endpoint::Route(Route::new(service));
self.set_node(path, id);
self.routes.insert(id, endpoint);
self
}
@@ -270,29 +207,27 @@ where
#[doc = include_str!("../docs/routing/nest.md")]
#[track_caller]
pub fn nest<S2>(self, path: &str, mut router: Router<S2, B>) -> Self
where
S2: Clone + Send + Sync + 'static,
{
if router.state.is_none() {
let s = self.state.clone();
router.state = match try_downcast::<Option<S2>, Option<S>>(s) {
Ok(state) => state,
Err(_) => panic!(
"can't nest a `Router` that wants to inherit state of type `{}` \
into a `Router` with a state type of `{}`",
type_name::<S2>(),
type_name::<S>(),
),
};
}
self.nest_service(path, router.into_service())
pub fn nest(self, path: &str, router: Router<S, B>) -> Self {
self.nest_endpoint(path, RouterOrService::<_, _, NotFound>::Router(router))
}
/// Like [`nest`](Self::nest), but accepts an arbitrary `Service`.
#[track_caller]
pub fn nest_service<T>(mut self, mut path: &str, svc: T) -> Self
pub fn nest_service<T>(self, path: &str, svc: T) -> Self
where
T: Service<Request<B>, Error = Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,
{
self.nest_endpoint(path, RouterOrService::Service(svc))
}
#[track_caller]
fn nest_endpoint<T>(
mut self,
mut path: &str,
router_or_service: RouterOrService<S, B, T>,
) -> Self
where
T: Service<Request<B>, Error = Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
@@ -315,16 +250,27 @@ where
format!("{path}/*{NEST_TAIL_PARAM}")
};
let svc = strip_prefix::StripPrefix::new(svc, prefix);
self = self.route_service(&path, svc.clone());
let endpoint = match router_or_service {
RouterOrService::Router(router) => {
let prefix = prefix.to_owned();
let boxed = BoxedIntoRoute::from_router(router)
.map(move |route| Route::new(StripPrefix::new(route, &prefix)));
Endpoint::NestedRouter(boxed)
}
RouterOrService::Service(svc) => {
Endpoint::Route(Route::new(StripPrefix::new(svc, prefix)))
}
};
self = self.route_endpoint(&path, endpoint.clone());
// `/*rest` is not matched by `/` so we need to also register a router at the
// prefix itself. Otherwise if you were to nest at `/foo` then `/foo` itself
// wouldn't match, which it should
self = self.route_service(prefix, svc.clone());
self = self.route_endpoint(prefix, endpoint.clone());
if !prefix.ends_with('/') {
// same goes for `/foo/`, that should also match
self = self.route_service(&format!("{prefix}/"), svc);
self = self.route_endpoint(&format!("{prefix}/"), endpoint);
}
self
@@ -332,66 +278,27 @@ where
#[doc = include_str!("../docs/routing/merge.md")]
#[track_caller]
pub fn merge<S2, R>(mut self, other: R) -> Self
pub fn merge<R>(mut self, other: R) -> Self
where
R: Into<Router<S2, B>>,
S2: Clone + Send + Sync + 'static,
R: Into<Router<S, B>>,
{
let Router {
state,
routes,
node,
fallback,
} = other.into();
let cast_method_router_closure_slot;
let (fallback, cast_method_router) = match state {
// other has its state set
Some(state) => {
let fallback = fallback.map_state(&state);
cast_method_router_closure_slot = move |r: MethodRouter<_, _>| r.map_state(&state);
let cast_method_router = &cast_method_router_closure_slot
as &dyn Fn(MethodRouter<_, _>) -> MethodRouter<_, _>;
(fallback, cast_method_router)
}
// other wants to inherit its state
None => {
if TypeId::of::<S>() != TypeId::of::<S2>() {
panic!(
"can't merge a `Router` that wants to inherit state of type `{}` \
into a `Router` with a state type of `{}`",
type_name::<S2>(),
type_name::<S>(),
);
}
// With the branch above not taken, we know we can cast S2 to S
let fallback = fallback.downcast_state::<S>().unwrap();
fn cast_method_router<S, S2, B>(r: MethodRouter<S2, B>) -> MethodRouter<S, B>
where
B: Send + 'static,
S: 'static,
S2: Clone + 'static,
{
r.downcast_state().unwrap()
}
(fallback, &cast_method_router as _)
}
};
for (id, route) in routes {
let path = node
.route_id_to_path
.get(&id)
.expect("no path for route id. This is a bug in axum. Please file an issue");
self = match route {
Endpoint::MethodRouter(method_router) => {
self.route(path, cast_method_router(method_router))
}
Endpoint::MethodRouter(method_router) => self.route(path, method_router),
Endpoint::Route(route) => self.route_service(path, route),
Endpoint::NestedRouter(router) => {
self.route_endpoint(path, Endpoint::NestedRouter(router))
}
};
}
@@ -412,30 +319,18 @@ where
<L::Service as Service<Request<NewReqBody>>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request<NewReqBody>>>::Future: Send + 'static,
{
let layer = ServiceBuilder::new()
.map_err(Into::into)
.layer(MapResponseLayer::new(IntoResponse::into_response))
.layer(layer)
.into_inner();
let routes = self
.routes
.into_iter()
.map(|(id, route)| {
let route = match route {
Endpoint::MethodRouter(method_router) => {
Endpoint::MethodRouter(method_router.layer(layer.clone()))
}
Endpoint::Route(route) => Endpoint::Route(Route::new(layer.layer(route))),
};
.map(|(id, endpoint)| {
let route = endpoint.layer(layer.clone());
(id, route)
})
.collect();
let fallback = self.fallback.map(move |svc| Route::new(layer.layer(svc)));
let fallback = self.fallback.map(|route| route.layer(layer));
Router {
state: self.state,
routes,
node: self.node,
fallback,
@@ -459,28 +354,16 @@ where
);
}
let layer = ServiceBuilder::new()
.map_err(Into::into)
.layer(MapResponseLayer::new(IntoResponse::into_response))
.layer(layer)
.into_inner();
let routes = self
.routes
.into_iter()
.map(|(id, route)| {
let route = match route {
Endpoint::MethodRouter(method_router) => {
Endpoint::MethodRouter(method_router.layer(layer.clone()))
}
Endpoint::Route(route) => Endpoint::Route(Route::new(layer.layer(route))),
};
.map(|(id, endpoint)| {
let route = endpoint.layer(layer.clone());
(id, route)
})
.collect();
Router {
state: self.state,
routes,
node: self.node,
fallback: self.fallback,
@@ -493,7 +376,7 @@ where
H: Handler<T, S, B>,
T: 'static,
{
self.fallback = Fallback::BoxedHandler(BoxedHandler::new(handler));
self.fallback = Fallback::BoxedHandler(BoxedIntoRoute::from_handler(handler));
self
}
@@ -510,14 +393,26 @@ where
self
}
/// Convert this router into a [`RouterService`] by providing the state.
///
/// Once this method has been called you cannot add more routes. So it must be called as last.
pub fn with_state(self, state: S) -> RouterService<B> {
RouterService::new(self, state)
}
}
impl<B> Router<(), B>
where
B: HttpBody + Send + 'static,
{
/// Convert this router into a [`RouterService`].
///
/// # Panics
/// This is a convenience method for routers that don't have any state (i.e. the state type is
/// `()`). Use [`Router::with_state`] otherwise.
///
/// Panics if the router was constructed with [`Router::inherit_state`].
#[track_caller]
/// Once this method has been called you cannot add more routes. So it must be called as last.
pub fn into_service(self) -> RouterService<B> {
RouterService::new(self)
RouterService::new(self, ())
}
/// Convert this router into a [`MakeService`], that is a [`Service`] whose
@@ -542,8 +437,10 @@ where
/// # };
/// ```
///
/// This is a convenience method for routers that don't have any state (i.e. the state type is
/// `()`). Use [`RouterService::into_make_service`] otherwise.
///
/// [`MakeService`]: tower::make::MakeService
#[track_caller]
pub fn into_make_service(self) -> IntoMakeService<RouterService<B>> {
IntoMakeService::new(self.into_service())
}
@@ -601,39 +498,13 @@ impl fmt::Debug for Node {
enum Fallback<S, B, E = Infallible> {
Default(Route<B, E>),
Service(Route<B, E>),
BoxedHandler(BoxedHandler<S, B, E>),
BoxedHandler(BoxedIntoRoute<S, B, E>),
}
impl<S, B, E> Fallback<S, B, E>
where
S: Clone,
{
fn map_state<S2>(self, state: &S) -> Fallback<S2, B, E> {
match self {
Self::Default(route) => Fallback::Default(route),
Self::Service(route) => Fallback::Service(route),
Self::BoxedHandler(handler) => Fallback::Service(handler.into_route(state.clone())),
}
}
fn downcast_state<S2>(self) -> Option<Fallback<S2, B, E>>
where
S: 'static,
B: 'static,
E: 'static,
S2: 'static,
{
match self {
Self::Default(route) => Some(Fallback::Default(route)),
Self::Service(route) => Some(Fallback::Service(route)),
Self::BoxedHandler(handler) => {
try_downcast::<BoxedHandler<S2, B, E>, BoxedHandler<S, B, E>>(handler)
.map(Fallback::BoxedHandler)
.ok()
}
}
}
fn merge(self, other: Self) -> Option<Self> {
match (self, other) {
(Self::Default(_), pick @ Self::Default(_)) => Some(pick),
@@ -729,6 +600,41 @@ impl<B, E> FallbackRoute<B, E> {
enum Endpoint<S, B> {
MethodRouter(MethodRouter<S, B>),
Route(Route<B>),
NestedRouter(BoxedIntoRoute<S, B, Infallible>),
}
impl<S, B> Endpoint<S, B>
where
B: HttpBody + Send + 'static,
S: Clone + Send + Sync + 'static,
{
fn into_route(self, state: S) -> Route<B> {
match self {
Endpoint::MethodRouter(method_router) => Route::new(method_router.with_state(state)),
Endpoint::Route(route) => route,
Endpoint::NestedRouter(router) => router.into_route(state),
}
}
fn layer<L, NewReqBody>(self, layer: L) -> Endpoint<S, NewReqBody>
where
L: Layer<Route<B>> + Clone + Send + 'static,
L::Service: Service<Request<NewReqBody>> + Clone + Send + 'static,
<L::Service as Service<Request<NewReqBody>>>::Response: IntoResponse + 'static,
<L::Service as Service<Request<NewReqBody>>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request<NewReqBody>>>::Future: Send + 'static,
NewReqBody: 'static,
{
match self {
Endpoint::MethodRouter(method_router) => {
Endpoint::MethodRouter(method_router.layer(layer))
}
Endpoint::Route(route) => Endpoint::Route(route.layer(layer)),
Endpoint::NestedRouter(router) => {
Endpoint::NestedRouter(router.map(|route| route.layer(layer)))
}
}
}
}
impl<S, B> Clone for Endpoint<S, B> {
@@ -736,19 +642,31 @@ impl<S, B> Clone for Endpoint<S, B> {
match self {
Self::MethodRouter(inner) => Self::MethodRouter(inner.clone()),
Self::Route(inner) => Self::Route(inner.clone()),
Self::NestedRouter(router) => Self::NestedRouter(router.clone()),
}
}
}
impl<S, B> fmt::Debug for Endpoint<S, B> {
impl<S, B> fmt::Debug for Endpoint<S, B>
where
S: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MethodRouter(inner) => inner.fmt(f),
Self::Route(inner) => inner.fmt(f),
Self::MethodRouter(method_router) => {
f.debug_tuple("MethodRouter").field(method_router).finish()
}
Self::Route(route) => f.debug_tuple("Route").field(route).finish(),
Self::NestedRouter(router) => f.debug_tuple("NestedRouter").field(router).finish(),
}
}
}
enum RouterOrService<S, B, T> {
Router(Router<S, B>),
Service(T),
}
#[test]
#[allow(warnings)]
fn traits() {
+22 -2
View File
@@ -17,9 +17,10 @@ use std::{
task::{Context, Poll},
};
use tower::{
util::{BoxCloneService, Oneshot},
ServiceExt,
util::{BoxCloneService, MapResponseLayer, Oneshot},
ServiceBuilder, ServiceExt,
};
use tower_layer::Layer;
use tower_service::Service;
/// How routes are stored inside a [`Router`](super::Router).
@@ -46,6 +47,25 @@ impl<B, E> Route<B, E> {
) -> Oneshot<BoxCloneService<Request<B>, Response, E>, Request<B>> {
self.0.clone().oneshot(req)
}
pub(crate) fn layer<L, NewReqBody, NewError>(self, layer: L) -> Route<NewReqBody, NewError>
where
L: Layer<Route<B, E>> + Clone + Send + 'static,
L::Service: Service<Request<NewReqBody>> + Clone + Send + 'static,
<L::Service as Service<Request<NewReqBody>>>::Response: IntoResponse + 'static,
<L::Service as Service<Request<NewReqBody>>>::Error: Into<NewError> + 'static,
<L::Service as Service<Request<NewReqBody>>>::Future: Send + 'static,
NewReqBody: 'static,
NewError: 'static,
{
let layer = ServiceBuilder::new()
.map_err(Into::into)
.layer(MapResponseLayer::new(IntoResponse::into_response))
.layer(layer)
.into_inner();
Route::new(layer.layer(self))
}
}
impl<B, E> Clone for Route<B, E> {
+27 -14
View File
@@ -1,5 +1,5 @@
use super::{
future::RouteFuture, url_params, Endpoint, FallbackRoute, Node, Route, RouteId, Router,
future::RouteFuture, url_params, FallbackRoute, IntoMakeService, Node, Route, RouteId, Router,
};
use crate::{
body::{Body, HttpBody},
@@ -28,26 +28,17 @@ impl<B> RouterService<B>
where
B: HttpBody + Send + 'static,
{
#[track_caller]
pub(super) fn new<S>(router: Router<S, B>) -> Self
pub(super) fn new<S>(router: Router<S, B>, state: S) -> Self
where
S: Clone + Send + Sync + 'static,
{
let state = router
.state
.expect("Can't turn a `Router` that wants to inherit state into a service");
let fallback = router.fallback.into_fallback_route(&state);
let routes = router
.routes
.into_iter()
.map(|(route_id, endpoint)| {
let route = match endpoint {
Endpoint::MethodRouter(method_router) => {
Route::new(method_router.with_state(state.clone()))
}
Endpoint::Route(route) => route,
};
let route = endpoint.into_route(state.clone());
(route_id, route)
})
.collect();
@@ -55,7 +46,7 @@ where
Self {
routes,
node: router.node,
fallback: router.fallback.into_fallback_route(&state),
fallback,
}
}
@@ -84,6 +75,28 @@ where
route.call(req)
}
/// Convert the router into a [`MakeService`] and no state.
///
/// See [`Router::into_make_service`] for more details.
///
/// [`MakeService`]: tower::make::MakeService
pub fn into_make_service(self) -> IntoMakeService<RouterService<B>> {
IntoMakeService::new(self)
}
/// Convert the router into a [`MakeService`] which stores information
/// about the incoming connection and has no state.
///
/// See [`Router::into_make_service_with_connect_info`] for more details.
///
/// [`MakeService`]: tower::make::MakeService
#[cfg(feature = "tokio")]
pub fn into_make_service_with_connect_info<C>(
self,
) -> crate::extract::connect_info::IntoMakeServiceWithConnectInfo<RouterService<B>, C> {
crate::extract::connect_info::IntoMakeServiceWithConnectInfo::new(self)
}
}
impl<B> Clone for RouterService<B> {
+48 -3
View File
@@ -1,3 +1,5 @@
use tower::ServiceExt;
use super::*;
use crate::middleware::{map_request, map_response};
@@ -50,10 +52,11 @@ async fn or() {
#[tokio::test]
async fn fallback_accessing_state() {
let app = Router::with_state("state")
.fallback(|State(state): State<&'static str>| async move { state });
let app = Router::new()
.fallback(|State(state): State<&'static str>| async move { state })
.with_state("state");
let client = TestClient::new(app);
let client = TestClient::from_service(app);
let res = client.get("/does-not-exist").send().await;
assert_eq!(res.status(), StatusCode::OK);
@@ -158,3 +161,45 @@ async fn also_inherits_default_layered_fallback() {
assert_eq!(res.headers()["x-from-fallback"], "1");
assert_eq!(res.text().await, "outer");
}
#[tokio::test]
async fn fallback_inherited_into_nested_router_service() {
let inner = Router::new()
.route(
"/bar",
get(|State(state): State<&'static str>| async move { state }),
)
.with_state("inner");
// with a different state
let app = Router::<()>::new()
.nest_service("/foo", inner)
.fallback(outer_fallback);
let client = TestClient::new(app);
let res = client.get("/foo/not-found").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "outer");
}
#[tokio::test]
async fn fallback_inherited_into_nested_opaque_service() {
let inner = Router::new()
.route(
"/bar",
get(|State(state): State<&'static str>| async move { state }),
)
.with_state("inner")
// even if the service is made more opaque it should still inherit the fallback
.boxed_clone();
// with a different state
let app = Router::<()>::new()
.nest_service("/foo", inner)
.fallback(outer_fallback);
let client = TestClient::new(app);
let res = client.get("/foo/not-found").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
assert_eq!(res.text().await, "outer");
}
-84
View File
@@ -397,87 +397,3 @@ async fn middleware_that_return_early() {
);
assert_eq!(client.get("/public").send().await.status(), StatusCode::OK);
}
#[tokio::test]
async fn merge_with_different_state_type() {
let inner = Router::with_state("inner".to_owned()).route(
"/foo",
get(|State(state): State<String>| async move { state }),
);
let app = Router::with_state("outer").merge(inner).route(
"/bar",
get(|State(state): State<&'static str>| async move { state }),
);
let client = TestClient::new(app);
let res = client.get("/foo").send().await;
assert_eq!(res.text().await, "inner");
let res = client.get("/bar").send().await;
assert_eq!(res.text().await, "outer");
}
#[tokio::test]
async fn merging_routes_different_method_different_states() {
let get = Router::with_state("get state").route(
"/",
get(|State(state): State<&'static str>| async move { state }),
);
let post = Router::with_state("post state").route(
"/",
post(|State(state): State<&'static str>| async move { state }),
);
let app = Router::new().merge(get).merge(post);
let client = TestClient::new(app);
let res = client.get("/").send().await;
assert_eq!(res.text().await, "get state");
let res = client.post("/").send().await;
assert_eq!(res.text().await, "post state");
}
#[tokio::test]
async fn merging_routes_different_paths_different_states() {
let foo = Router::with_state("foo state").route(
"/foo",
get(|State(state): State<&'static str>| async move { state }),
);
let bar = Router::with_state("bar state").route(
"/bar",
get(|State(state): State<&'static str>| async move { state }),
);
let app = Router::new().merge(foo).merge(bar);
let client = TestClient::new(app);
let res = client.get("/foo").send().await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "foo state");
let res = client.get("/bar").send().await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "bar state");
}
#[tokio::test]
async fn inherit_state_via_merge() {
let foo = Router::inherit_state().route(
"/foo",
get(|State(state): State<&'static str>| async move { state }),
);
let app = Router::with_state("state").merge(foo);
let client = TestClient::new(app);
let res = client.get("/foo").send().await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "state");
}
+9 -7
View File
@@ -760,8 +760,8 @@ async fn extract_state() {
inner: InnerState { value: 2 },
};
let app = Router::with_state(state).route("/", get(handler));
let client = TestClient::new(app);
let app = Router::new().route("/", get(handler)).with_state(state);
let client = TestClient::from_service(app);
let res = client.get("/").send().await;
assert_eq!(res.status(), StatusCode::OK);
@@ -769,12 +769,14 @@ async fn extract_state() {
#[tokio::test]
async fn explicitly_set_state() {
let app = Router::with_state("...").route_service(
"/",
get(|State(state): State<&'static str>| async move { state }).with_state("foo"),
);
let app = Router::new()
.route_service(
"/",
get(|State(state): State<&'static str>| async move { state }).with_state("foo"),
)
.with_state("...");
let client = TestClient::new(app);
let client = TestClient::from_service(app);
let res = client.get("/").send().await;
assert_eq!(res.text().await, "foo");
}
+1 -46
View File
@@ -265,7 +265,7 @@ async fn multiple_top_level_nests() {
#[tokio::test]
#[should_panic(expected = "Invalid route: nested routes cannot contain wildcards (*)")]
async fn nest_cannot_contain_wildcards() {
Router::<_, Body>::new().nest("/one/*rest", Router::new());
Router::<(), Body>::new().nest("/one/*rest", Router::new());
}
#[tokio::test]
@@ -424,48 +424,3 @@ nested_route_test!(nest_9, nest = "/a", route = "/a/", expected = "/a/a/");
nested_route_test!(nest_11, nest = "/a/", route = "/", expected = "/a/");
nested_route_test!(nest_12, nest = "/a/", route = "/a", expected = "/a/a");
nested_route_test!(nest_13, nest = "/a/", route = "/a/", expected = "/a/a/");
#[tokio::test]
async fn nesting_with_different_state() {
let inner = Router::with_state("inner".to_owned()).route(
"/foo",
get(|State(state): State<String>| async move { state }),
);
let outer = Router::with_state("outer")
.route(
"/foo",
get(|State(state): State<&'static str>| async move { state }),
)
.nest("/nested", inner)
.route(
"/bar",
get(|State(state): State<&'static str>| async move { state }),
);
let client = TestClient::new(outer);
let res = client.get("/foo").send().await;
assert_eq!(res.text().await, "outer");
let res = client.get("/nested/foo").send().await;
assert_eq!(res.text().await, "inner");
let res = client.get("/bar").send().await;
assert_eq!(res.text().await, "outer");
}
#[tokio::test]
async fn inherit_state_via_nest() {
let foo = Router::inherit_state().route(
"/foo",
get(|State(state): State<&'static str>| async move { state }),
);
let app = Router::with_state("state").nest("/test", foo);
let client = TestClient::new(app);
let res = client.get("/test/foo").send().await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "state");
}
+1 -4
View File
@@ -15,10 +15,7 @@ pub(crate) struct TestClient {
}
impl TestClient {
pub(crate) fn new<S>(router: Router<S, Body>) -> Self
where
S: Clone + Send + Sync + 'static,
{
pub(crate) fn new(router: Router<(), Body>) -> Self {
Self::from_service(router.into_service())
}