Add NestedPath (#1924)

This commit is contained in:
David Pedersen 2023-09-17 10:32:23 +02:00 committed by GitHub
parent 449e4c1138
commit 20f48af914
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 300 additions and 17 deletions

View file

@ -65,10 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **added:** Implement `Handler` for `T: IntoResponse` ([#2140])
- **added:** Implement `IntoResponse` for `(R,) where R: IntoResponse` ([#2143])
- **changed:** For SSE, add space between field and value for compatibility ([#2149])
- **added:** Add `NestedPath` extractor ([#1924])
[#2021]: https://github.com/tokio-rs/axum/pull/2021
[#2014]: https://github.com/tokio-rs/axum/pull/2014
[#2030]: https://github.com/tokio-rs/axum/pull/2030
[#1664]: https://github.com/tokio-rs/axum/pull/1664
[#1751]: https://github.com/tokio-rs/axum/pull/1751
[#1762]: https://github.com/tokio-rs/axum/pull/1762
@ -76,8 +74,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#1835]: https://github.com/tokio-rs/axum/pull/1835
[#1850]: https://github.com/tokio-rs/axum/pull/1850
[#1868]: https://github.com/tokio-rs/axum/pull/1868
[#1924]: https://github.com/tokio-rs/axum/pull/1924
[#1956]: https://github.com/tokio-rs/axum/pull/1956
[#1972]: https://github.com/tokio-rs/axum/pull/1972
[#2014]: https://github.com/tokio-rs/axum/pull/2014
[#2021]: https://github.com/tokio-rs/axum/pull/2021
[#2030]: https://github.com/tokio-rs/axum/pull/2030
[#2058]: https://github.com/tokio-rs/axum/pull/2058
[#2073]: https://github.com/tokio-rs/axum/pull/2073
[#2096]: https://github.com/tokio-rs/axum/pull/2096

View file

@ -11,6 +11,7 @@ pub mod rejection;
pub mod ws;
mod host;
pub(crate) mod nested_path;
mod raw_form;
mod raw_query;
mod request_parts;
@ -26,6 +27,7 @@ pub use axum_macros::{FromRef, FromRequest, FromRequestParts};
#[allow(deprecated)]
pub use self::{
host::Host,
nested_path::NestedPath,
path::{Path, RawPathParams},
raw_form::RawForm,
raw_query::RawQuery,

View file

@ -0,0 +1,265 @@
use std::{
sync::Arc,
task::{Context, Poll},
};
use crate::extract::Request;
use async_trait::async_trait;
use axum_core::extract::FromRequestParts;
use http::request::Parts;
use tower_layer::{layer_fn, Layer};
use tower_service::Service;
use super::rejection::NestedPathRejection;
/// Access the path the matched the route is nested at.
///
/// This can for example be used when doing redirects.
///
/// # Example
///
/// ```
/// use axum::{
/// Router,
/// extract::NestedPath,
/// routing::get,
/// };
///
/// let api = Router::new().route(
/// "/users",
/// get(|path: NestedPath| async move {
/// // `path` will be "/api" because thats what this
/// // router is nested at when we build `app`
/// let path = path.as_str();
/// })
/// );
///
/// let app = Router::new().nest("/api", api);
/// # let _: Router = app;
/// ```
#[derive(Debug, Clone)]
pub struct NestedPath(Arc<str>);
impl NestedPath {
/// Returns a `str` representation of the path.
pub fn as_str(&self) -> &str {
&self.0
}
}
#[async_trait]
impl<S> FromRequestParts<S> for NestedPath
where
S: Send + Sync,
{
type Rejection = NestedPathRejection;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
match parts.extensions.get::<Self>() {
Some(nested_path) => Ok(nested_path.clone()),
None => Err(NestedPathRejection),
}
}
}
#[derive(Clone)]
pub(crate) struct SetNestedPath<S> {
inner: S,
path: Arc<str>,
}
impl<S> SetNestedPath<S> {
pub(crate) fn layer(path: &str) -> impl Layer<S, Service = Self> + Clone {
let path = Arc::from(path);
layer_fn(move |inner| Self {
inner,
path: Arc::clone(&path),
})
}
}
impl<S, B> Service<Request<B>> for SetNestedPath<S>
where
S: Service<Request<B>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
#[inline]
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: Request<B>) -> Self::Future {
if let Some(prev) = req.extensions_mut().get_mut::<NestedPath>() {
let new_path = if prev.as_str() == "/" {
Arc::clone(&self.path)
} else {
format!("{}{}", prev.as_str().trim_end_matches('/'), self.path).into()
};
prev.0 = new_path;
} else {
req.extensions_mut()
.insert(NestedPath(Arc::clone(&self.path)));
};
self.inner.call(req)
}
}
#[cfg(test)]
mod tests {
use axum_core::response::Response;
use http::StatusCode;
use crate::{
extract::{NestedPath, Request},
middleware::{from_fn, Next},
routing::get,
test_helpers::*,
Router,
};
#[crate::test]
async fn one_level_of_nesting() {
let api = Router::new().route(
"/users",
get(|nested_path: NestedPath| {
assert_eq!(nested_path.as_str(), "/api");
async {}
}),
);
let app = Router::new().nest("/api", api);
let client = TestClient::new(app);
let res = client.get("/api/users").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[crate::test]
async fn one_level_of_nesting_with_trailing_slash() {
let api = Router::new().route(
"/users",
get(|nested_path: NestedPath| {
assert_eq!(nested_path.as_str(), "/api/");
async {}
}),
);
let app = Router::new().nest("/api/", api);
let client = TestClient::new(app);
let res = client.get("/api/users").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[crate::test]
async fn two_levels_of_nesting() {
let api = Router::new().route(
"/users",
get(|nested_path: NestedPath| {
assert_eq!(nested_path.as_str(), "/api/v2");
async {}
}),
);
let app = Router::new().nest("/api", Router::new().nest("/v2", api));
let client = TestClient::new(app);
let res = client.get("/api/v2/users").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[crate::test]
async fn two_levels_of_nesting_with_trailing_slash() {
let api = Router::new().route(
"/users",
get(|nested_path: NestedPath| {
assert_eq!(nested_path.as_str(), "/api/v2");
async {}
}),
);
let app = Router::new().nest("/api/", Router::new().nest("/v2", api));
let client = TestClient::new(app);
let res = client.get("/api/v2/users").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[crate::test]
async fn nested_at_root() {
let api = Router::new().route(
"/users",
get(|nested_path: NestedPath| {
assert_eq!(nested_path.as_str(), "/");
async {}
}),
);
let app = Router::new().nest("/", api);
let client = TestClient::new(app);
let res = client.get("/users").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[crate::test]
async fn deeply_nested_from_root() {
let api = Router::new().route(
"/users",
get(|nested_path: NestedPath| {
assert_eq!(nested_path.as_str(), "/api");
async {}
}),
);
let app = Router::new().nest("/", Router::new().nest("/api", api));
let client = TestClient::new(app);
let res = client.get("/api/users").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[crate::test]
async fn in_fallbacks() {
let api = Router::new().fallback(get(|nested_path: NestedPath| {
assert_eq!(nested_path.as_str(), "/api");
async {}
}));
let app = Router::new().nest("/api", api);
let client = TestClient::new(app);
let res = client.get("/api/doesnt-exist").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
#[crate::test]
async fn in_middleware() {
async fn middleware(nested_path: NestedPath, req: Request, next: Next) -> Response {
assert_eq!(nested_path.as_str(), "/api");
next.run(req).await
}
let api = Router::new()
.route("/users", get(|| async {}))
.layer(from_fn(middleware));
let app = Router::new().nest("/api", api);
let client = TestClient::new(app);
let res = client.get("/api/users").send().await;
assert_eq!(res.status(), StatusCode::OK);
}
}

View file

@ -207,3 +207,12 @@ composite_rejection! {
MatchedPathMissing,
}
}
define_rejection! {
#[status = INTERNAL_SERVER_ERROR]
#[body = "The matched route is not nested"]
/// Rejection type for [`NestedPath`](super::NestedPath).
///
/// This rejection is used if the matched route wasn't nested.
pub struct NestedPathRejection;
}

View file

@ -1,4 +1,4 @@
use crate::extract::Request;
use crate::extract::{nested_path::SetNestedPath, Request};
use axum_core::response::IntoResponse;
use matchit::MatchError;
use std::{borrow::Cow, collections::HashMap, convert::Infallible, fmt, sync::Arc};
@ -162,10 +162,10 @@ where
pub(super) fn nest(
&mut self,
path: &str,
path_to_nest_at: &str,
router: PathRouter<S, IS_FALLBACK>,
) -> Result<(), Cow<'static, str>> {
let prefix = validate_nest_path(path);
let prefix = validate_nest_path(path_to_nest_at);
let PathRouter {
routes,
@ -181,7 +181,11 @@ where
let path = path_for_nested_route(prefix, inner_path);
match endpoint.layer(StripPrefix::layer(prefix)) {
let layer = (
StripPrefix::layer(prefix),
SetNestedPath::layer(path_to_nest_at),
);
match endpoint.layer(layer) {
Endpoint::MethodRouter(method_router) => {
self.route(&path, method_router)?;
}
@ -194,13 +198,17 @@ where
Ok(())
}
pub(super) fn nest_service<T>(&mut self, path: &str, svc: T) -> Result<(), Cow<'static, str>>
pub(super) fn nest_service<T>(
&mut self,
path_to_nest_at: &str,
svc: T,
) -> Result<(), Cow<'static, str>>
where
T: Service<Request, Error = Infallible> + Clone + Send + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,
{
let path = validate_nest_path(path);
let path = validate_nest_path(path_to_nest_at);
let prefix = path;
let path = if path.ends_with('/') {
@ -209,7 +217,11 @@ where
format!("{path}/*{NEST_TAIL_PARAM}")
};
let endpoint = Endpoint::Route(Route::new(StripPrefix::new(svc, prefix)));
let layer = (
StripPrefix::layer(prefix),
SetNestedPath::layer(path_to_nest_at),
);
let endpoint = Endpoint::Route(Route::new(layer.layer(svc)));
self.route_endpoint(&path, endpoint.clone())?;

View file

@ -14,13 +14,6 @@ pub(super) struct StripPrefix<S> {
}
impl<S> StripPrefix<S> {
pub(super) fn new(inner: S, prefix: &str) -> Self {
Self {
inner,
prefix: prefix.into(),
}
}
pub(super) fn layer(prefix: &str) -> impl Layer<S, Service = Self> + Clone {
let prefix = Arc::from(prefix);
layer_fn(move |inner| Self {