chore: Upgrade matchit to 0.8 (#2645)

This commit is contained in:
David Mládek 2024-10-03 17:46:58 +02:00 committed by GitHub
parent 5db62e8452
commit 6318b57fda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 414 additions and 208 deletions

View file

@ -28,7 +28,7 @@ use serde::de::DeserializeOwned;
///
/// let app = Router::new()
/// .route("/blog", get(render_blog))
/// .route("/blog/:page", get(render_blog));
/// .route("/blog/{page}", get(render_blog));
/// # let app: Router = app;
/// ```
#[derive(Debug)]
@ -75,7 +75,7 @@ mod tests {
let app = Router::new()
.route("/", get(handle))
.route("/:num", get(handle));
.route("/{num}", get(handle));
let client = TestClient::new(app);

View file

@ -92,7 +92,7 @@ pub trait HandlerCallWithExtractors<T, S>: Sized {
/// }
///
/// let app = Router::new().route(
/// "/users/:id",
/// "/users/{id}",
/// get(
/// // first try `admin`, if that rejects run `user`, finally falling back
/// // to `guest`

View file

@ -134,7 +134,7 @@ mod tests {
"fallback"
}
let app = Router::new().route("/:id", get(one.or(two).or(three)));
let app = Router::new().route("/{id}", get(one.or(two).or(three)));
let client = TestClient::new(app);

View file

@ -81,7 +81,7 @@ use prost::Message;
/// # unimplemented!()
/// }
///
/// let app = Router::new().route("/users/:id", get(get_user));
/// let app = Router::new().route("/users/{id}", get(get_user));
/// # let _: Router = app;
/// ```
#[derive(Debug, Clone, Copy, Default)]

View file

@ -371,11 +371,11 @@ mod tests {
async fn tsr_with_params() {
let app = Router::new()
.route_with_tsr(
"/a/:a",
"/a/{a}",
get(|Path(param): Path<String>| async move { param }),
)
.route_with_tsr(
"/b/:b/",
"/b/{b}/",
get(|Path(param): Path<String>| async move { param }),
);

View file

@ -19,13 +19,13 @@ use axum::{
/// .create(|| async {})
/// // `GET /users/new`
/// .new(|| async {})
/// // `GET /users/:users_id`
/// // `GET /users/{users_id}`
/// .show(|Path(user_id): Path<u64>| async {})
/// // `GET /users/:users_id/edit`
/// // `GET /users/{users_id}/edit`
/// .edit(|Path(user_id): Path<u64>| async {})
/// // `PUT or PATCH /users/:users_id`
/// // `PUT or PATCH /users/{users_id}`
/// .update(|Path(user_id): Path<u64>| async {})
/// // `DELETE /users/:users_id`
/// // `DELETE /users/{users_id}`
/// .destroy(|Path(user_id): Path<u64>| async {});
///
/// let app = Router::new().merge(users);
@ -82,7 +82,9 @@ where
self.route(&path, get(handler))
}
/// Add a handler at `GET /{resource_name}/:{resource_name}_id`.
/// Add a handler at `GET /<resource_name>/{<resource_name>_id}`.
///
/// For example when the resources are posts: `GET /post/{post_id}`.
pub fn show<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
@ -92,17 +94,21 @@ where
self.route(&path, get(handler))
}
/// Add a handler at `GET /{resource_name}/:{resource_name}_id/edit`.
/// Add a handler at `GET /<resource_name>/{<resource_name>_id}/edit`.
///
/// For example when the resources are posts: `GET /post/{post_id}/edit`.
pub fn edit<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
let path = format!("/{0}/:{0}_id/edit", self.name);
let path = format!("/{0}/{{{0}_id}}/edit", self.name);
self.route(&path, get(handler))
}
/// Add a handler at `PUT or PATCH /resource_name/:{resource_name}_id`.
/// Add a handler at `PUT or PATCH /<resource_name>/{<resource_name>_id}`.
///
/// For example when the resources are posts: `PUT /post/{post_id}`.
pub fn update<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
@ -115,7 +121,9 @@ where
)
}
/// Add a handler at `DELETE /{resource_name}/:{resource_name}_id`.
/// Add a handler at `DELETE /<resource_name>/{<resource_name>_id}`.
///
/// For example when the resources are posts: `DELETE /post/{post_id}`.
pub fn destroy<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
@ -130,7 +138,7 @@ where
}
fn show_update_destroy_path(&self) -> String {
format!("/{0}/:{0}_id", self.name)
format!("/{0}/{{{0}_id}}", self.name)
}
fn route(mut self, path: &str, method_router: MethodRouter<S>) -> Self {

View file

@ -19,15 +19,15 @@ use serde::Serialize;
/// RouterExt, // for `Router::typed_*`
/// };
///
/// // A type safe route with `/users/:id` as its associated path.
/// // A type safe route with `/users/{id}` as its associated path.
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// #[typed_path("/users/{id}")]
/// struct UsersMember {
/// id: u32,
/// }
///
/// // A regular handler function that takes `UsersMember` as the first argument
/// // and thus creates a typed connection between this handler and the `/users/:id` path.
/// // and thus creates a typed connection between this handler and the `/users/{id}` path.
/// //
/// // The `TypedPath` must be the first argument to the function.
/// async fn users_show(
@ -39,7 +39,7 @@ use serde::Serialize;
/// let app = Router::new()
/// // Add our typed route to the router.
/// //
/// // The path will be inferred to `/users/:id` since `users_show`'s
/// // The path will be inferred to `/users/{id}` since `users_show`'s
/// // first argument is `UsersMember` which implements `TypedPath`
/// .typed_get(users_show)
/// .typed_post(users_create)
@ -75,7 +75,7 @@ use serde::Serialize;
/// use axum_extra::routing::TypedPath;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// #[typed_path("/users/{id}")]
/// struct UsersMember {
/// id: u32,
/// }
@ -100,7 +100,7 @@ use serde::Serialize;
/// use axum_extra::routing::TypedPath;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id/teams/:team_id")]
/// #[typed_path("/users/{id}/teams/{team_id}")]
/// struct UsersMember {
/// id: u32,
/// }
@ -117,7 +117,7 @@ use serde::Serialize;
/// struct UsersCollection;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// #[typed_path("/users/{id}")]
/// struct UsersMember(u32);
/// ```
///
@ -130,7 +130,7 @@ use serde::Serialize;
/// use axum_extra::routing::TypedPath;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// #[typed_path("/users/{id}")]
/// struct UsersMember {
/// id: String,
/// }
@ -158,7 +158,7 @@ use serde::Serialize;
/// };
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id", rejection(UsersMemberRejection))]
/// #[typed_path("/users/{id}", rejection(UsersMemberRejection))]
/// struct UsersMember {
/// id: String,
/// }
@ -215,7 +215,7 @@ use serde::Serialize;
/// [`Deserialize`]: serde::Deserialize
/// [`PathRejection`]: axum::extract::rejection::PathRejection
pub trait TypedPath: std::fmt::Display {
/// The path with optional captures such as `/users/:id`.
/// The path with optional captures such as `/users/{id}`.
const PATH: &'static str;
/// Convert the path into a `Uri`.
@ -398,7 +398,7 @@ mod tests {
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
#[typed_path("/users/{id}")]
struct UsersShow {
id: i32,
}

View file

@ -29,7 +29,7 @@ use std::convert::Infallible;
/// // ...
/// }
///
/// let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show));
/// let app = Router::new().route("/users/{user_id}/team/{team_id}", get(users_teams_show));
/// # let _: Router = app;
/// ```
///

View file

@ -383,8 +383,12 @@ fn parse_path(path: &LitStr) -> syn::Result<Vec<Segment>> {
.split('/')
.map(|segment| {
if let Some(capture) = segment
.strip_prefix(':')
.or_else(|| segment.strip_prefix('*'))
.strip_prefix('{')
.and_then(|segment| segment.strip_suffix('}'))
.and_then(|segment| {
(!segment.starts_with('{') && !segment.ends_with('}')).then_some(segment)
})
.map(|capture| capture.strip_prefix('*').unwrap_or(capture))
{
Ok(Segment::Capture(capture.to_owned(), path.span()))
} else {

View file

@ -2,7 +2,7 @@ use axum_macros::TypedPath;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
#[typed_path("/users/{id}")]
struct MyPath {}
fn main() {

View file

@ -1,5 +1,5 @@
error[E0026]: struct `MyPath` does not have a field named `id`
--> tests/typed_path/fail/missing_field.rs:5:14
|
5 | #[typed_path("/users/:id")]
| ^^^^^^^^^^^^ struct `MyPath` does not have this field
5 | #[typed_path("/users/{id}")]
| ^^^^^^^^^^^^^ struct `MyPath` does not have this field

View file

@ -1,7 +1,7 @@
use axum_macros::TypedPath;
#[derive(TypedPath)]
#[typed_path("/users/:id")]
#[typed_path("/users/{id}")]
struct MyPath {
id: u32,
}

View file

@ -1,7 +1,7 @@
use axum_extra::routing::TypedPath;
#[derive(TypedPath)]
#[typed_path(":foo")]
#[typed_path("{foo}")]
struct MyPath;
fn main() {}

View file

@ -1,5 +1,5 @@
error: paths must start with a `/`
--> tests/typed_path/fail/route_not_starting_with_slash_non_empty.rs:4:14
|
4 | #[typed_path(":foo")]
| ^^^^^^
4 | #[typed_path("{foo}")]
| ^^^^^^^

View file

@ -2,7 +2,7 @@ use axum_macros::TypedPath;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
#[typed_path("/users/{id}")]
struct MyPath;
fn main() {}

View file

@ -1,5 +1,5 @@
error: Typed paths for unit structs cannot contain captures
--> tests/typed_path/fail/unit_with_capture.rs:5:14
|
5 | #[typed_path("/users/:id")]
| ^^^^^^^^^^^^
5 | #[typed_path("/users/{id}")]
| ^^^^^^^^^^^^^

View file

@ -6,7 +6,7 @@ use axum_extra::routing::{RouterExt, TypedPath};
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/:foo", rejection(MyRejection))]
#[typed_path("/{foo}", rejection(MyRejection))]
struct MyPathNamed {
foo: String,
}
@ -16,7 +16,7 @@ struct MyPathNamed {
struct MyPathUnit;
#[derive(TypedPath, Deserialize)]
#[typed_path("/:foo", rejection(MyRejection))]
#[typed_path("/{foo}", rejection(MyRejection))]
struct MyPathUnnamed(String);
struct MyRejection;

View file

@ -3,13 +3,13 @@ use axum::http::Uri;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/:id")]
#[typed_path("/{id}")]
struct Named {
id: u32,
}
#[derive(TypedPath, Deserialize)]
#[typed_path("/:id")]
#[typed_path("/{id}")]
struct Unnamed(u32);
#[derive(TypedPath, Deserialize)]

View file

@ -2,7 +2,7 @@ use axum_extra::routing::TypedPath;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:user_id/teams/:team_id")]
#[typed_path("/users/{user_id}/teams/{team_id}")]
struct MyPath {
user_id: u32,
team_id: u32,
@ -11,7 +11,7 @@ struct MyPath {
fn main() {
_ = axum::Router::<()>::new().route("/", axum::routing::get(|_: MyPath| async {}));
assert_eq!(MyPath::PATH, "/users/:user_id/teams/:team_id");
assert_eq!(MyPath::PATH, "/users/{user_id}/teams/{team_id}");
assert_eq!(
format!(
"{}",

View file

@ -3,7 +3,7 @@ use axum::{extract::rejection::PathRejection, http::StatusCode};
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
#[typed_path("/users/{id}")]
struct UsersShow {
id: String,
}

View file

@ -4,12 +4,12 @@ use serde::Deserialize;
pub type Result<T> = std::result::Result<T, ()>;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:user_id/teams/:team_id")]
#[typed_path("/users/{user_id}/teams/{team_id}")]
struct MyPath(u32, u32);
fn main() {
_ = axum::Router::<()>::new().route("/", axum::routing::get(|_: MyPath| async {}));
assert_eq!(MyPath::PATH, "/users/:user_id/teams/:team_id");
assert_eq!(MyPath::PATH, "/users/{user_id}/teams/{team_id}");
assert_eq!(format!("{}", MyPath(1, 2)), "/users/1/teams/2");
}

View file

@ -2,13 +2,13 @@ use axum_extra::routing::TypedPath;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/:param")]
#[typed_path("/{param}")]
struct Named {
param: String,
}
#[derive(TypedPath, Deserialize)]
#[typed_path("/:param")]
#[typed_path("/{param}")]
struct Unnamed(String);
fn main() {

View file

@ -2,7 +2,7 @@ use axum_extra::routing::{RouterExt, TypedPath};
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/*rest")]
#[typed_path("/{*rest}")]
struct MyPath {
rest: String,
}

View file

@ -48,7 +48,7 @@ http = "1.0.0"
http-body = "1.0.0"
http-body-util = "0.1.0"
itoa = "1.0.5"
matchit = "0.7"
matchit = "=0.8.0"
memchr = "2.4.1"
mime = "0.3.16"
percent-encoding = "2.1"

View file

@ -77,7 +77,7 @@ async fn extension(Extension(state): Extension<State>) {}
struct State { /* ... */ }
let app = Router::new()
.route("/path/:user_id", post(path))
.route("/path/{user_id}", post(path))
.route("/query", post(query))
.route("/string", post(string))
.route("/bytes", post(bytes))
@ -100,7 +100,7 @@ use axum::{
use uuid::Uuid;
use serde::Deserialize;
let app = Router::new().route("/users/:id/things", get(get_user_things));
let app = Router::new().route("/users/{id}/things", get(get_user_things));
#[derive(Deserialize)]
struct Pagination {

View file

@ -16,7 +16,7 @@ use axum::{
// define some routes separately
let user_routes = Router::new()
.route("/users", get(users_list))
.route("/users/:id", get(users_show));
.route("/users/{id}", get(users_show));
let team_routes = Router::new()
.route("/teams", get(teams_list));
@ -30,7 +30,7 @@ let app = Router::new()
// Our app now accepts
// - GET /users
// - GET /users/:id
// - GET /users/{id}
// - GET /teams
# let _: Router = app;
```

View file

@ -11,7 +11,7 @@ use axum::{
Router,
};
let user_routes = Router::new().route("/:id", get(|| async {}));
let user_routes = Router::new().route("/{id}", get(|| async {}));
let team_routes = Router::new().route("/", post(|| async {}));
@ -22,7 +22,7 @@ let api_routes = Router::new()
let app = Router::new().nest("/api", api_routes);
// Our app now accepts
// - GET /api/users/:id
// - GET /api/users/{id}
// - POST /api/teams
# let _: Router = app;
```
@ -54,9 +54,9 @@ async fn users_get(Path(params): Path<HashMap<String, String>>) {
let id = params.get("id");
}
let users_api = Router::new().route("/users/:id", get(users_get));
let users_api = Router::new().route("/users/{id}", get(users_get));
let app = Router::new().nest("/:version/api", users_api);
let app = Router::new().nest("/{version}/api", users_api);
# let _: Router = app;
```
@ -75,7 +75,7 @@ let nested_router = Router::new()
}));
let app = Router::new()
.route("/foo/*rest", get(|uri: Uri| async {
.route("/foo/{*rest}", get(|uri: Uri| async {
// `uri` will contain `/foo`
}))
.nest("/bar", nested_router);

View file

@ -20,15 +20,15 @@ be called.
# Captures
Paths can contain segments like `/:key` which matches any single segment and
Paths can contain segments like `/{key}` which matches any single segment and
will store the value captured at `key`. The value captured can be zero-length
except for in the invalid path `//`.
Examples:
- `/:key`
- `/users/:id`
- `/users/:id/tweets`
- `/{key}`
- `/users/{id}`
- `/users/{id}/tweets`
Captures can be extracted using [`Path`](crate::extract::Path). See its
documentation for more details.
@ -41,19 +41,19 @@ path rather than the actual path.
# Wildcards
Paths can end in `/*key` which matches all segments and will store the segments
Paths can end in `/{*key}` which matches all segments and will store the segments
captured at `key`.
Examples:
- `/*key`
- `/assets/*path`
- `/:id/:repo/*tree`
- `/{*key}`
- `/assets/{*path}`
- `/{id}/{repo}/{*tree}`
Note that `/*key` doesn't match empty segments. Thus:
Note that `/{*key}` doesn't match empty segments. Thus:
- `/*key` doesn't match `/` but does match `/a`, `/a/`, etc.
- `/x/*key` doesn't match `/x` or `/x/` but does match `/x/a`, `/x/a/`, etc.
- `/{*key}` doesn't match `/` but does match `/a`, `/a/`, etc.
- `/x/{*key}` doesn't match `/x` or `/x/` but does match `/x/a`, `/x/a/`, etc.
Wildcard captures can also be extracted using [`Path`](crate::extract::Path):
@ -64,14 +64,14 @@ use axum::{
extract::Path,
};
let app: Router = Router::new().route("/*key", get(handler));
let app: Router = Router::new().route("/{*key}", get(handler));
async fn handler(Path(path): Path<String>) -> String {
path
}
```
Note that the leading slash is not included, i.e. for the route `/foo/*rest` and
Note that the leading slash is not included, i.e. for the route `/foo/{*rest}` and
the path `/foo/bar/baz` the value of `rest` will be `bar/baz`.
# Accepting multiple methods
@ -120,9 +120,9 @@ use axum::{Router, routing::{get, delete}, extract::Path};
let app = Router::new()
.route("/", get(root))
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(show_user))
.route("/api/:version/users/:id/action", delete(do_users_action))
.route("/assets/*path", get(serve_asset));
.route("/users/{id}", get(show_user))
.route("/api/{version}/users/{id}/action", delete(do_users_action))
.route("/assets/{*path}", get(serve_asset));
async fn root() {}
@ -151,7 +151,7 @@ let app = Router::new()
# let _: Router = app;
```
The static route `/foo` and the dynamic route `/:key` are not considered to
The static route `/foo` and the dynamic route `/{key}` are not considered to
overlap and `/foo` will take precedence.
Also panics if `path` is empty.

View file

@ -0,0 +1,43 @@
Turn off checks for compatibility with route matching syntax from 0.7.
This allows usage of paths starting with a colon `:` or an asterisk `*` which are otherwise prohibited.
# Example
```rust
use axum::{
routing::get,
Router,
};
let app = Router::<()>::new()
.without_v07_checks()
.route("/:colon", get(|| async {}))
.route("/*asterisk", get(|| async {}));
// Our app now accepts
// - GET /:colon
// - GET /*asterisk
# let _: Router = app;
```
Adding such routes without calling this method first will panic.
```rust,should_panic
use axum::{
routing::get,
Router,
};
// This panics...
let app = Router::<()>::new()
.route("/:colon", get(|| async {}));
```
# Merging
When two routers are merged, v0.7 checks are disabled for route registrations on the resulting router if both of the two routers had them also disabled.
# Nesting
Each router needs to have the checks explicitly disabled. Nesting a router with the checks either enabled or disabled has no effect on the outer router.

View file

@ -13,10 +13,10 @@ use std::{collections::HashMap, sync::Arc};
/// };
///
/// let app = Router::new().route(
/// "/users/:id",
/// "/users/{id}",
/// get(|path: MatchedPath| async move {
/// let path = path.as_str();
/// // `path` will be "/users/:id"
/// // `path` will be "/users/{id}"
/// })
/// );
/// # let _: Router = app;
@ -38,7 +38,7 @@ use std::{collections::HashMap, sync::Arc};
/// use tower_http::trace::TraceLayer;
///
/// let app = Router::new()
/// .route("/users/:id", get(|| async { /* ... */ }))
/// .route("/users/{id}", get(|| async { /* ... */ }))
/// .layer(
/// TraceLayer::new_for_http().make_span_with(|req: &Request<_>| {
/// let path = if let Some(path) = req.extensions().get::<MatchedPath>() {
@ -141,22 +141,22 @@ mod tests {
#[crate::test]
async fn extracting_on_handler() {
let app = Router::new().route(
"/:a",
"/{a}",
get(|path: MatchedPath| async move { path.as_str().to_owned() }),
);
let client = TestClient::new(app);
let res = client.get("/foo").await;
assert_eq!(res.text().await, "/:a");
assert_eq!(res.text().await, "/{a}");
}
#[crate::test]
async fn extracting_on_handler_in_nested_router() {
let app = Router::new().nest(
"/:a",
"/{a}",
Router::new().route(
"/:b",
"/{b}",
get(|path: MatchedPath| async move { path.as_str().to_owned() }),
),
);
@ -164,17 +164,17 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar").await;
assert_eq!(res.text().await, "/:a/:b");
assert_eq!(res.text().await, "/{a}/{b}");
}
#[crate::test]
async fn extracting_on_handler_in_deeply_nested_router() {
let app = Router::new().nest(
"/:a",
"/{a}",
Router::new().nest(
"/:b",
"/{b}",
Router::new().route(
"/:c",
"/{c}",
get(|path: MatchedPath| async move { path.as_str().to_owned() }),
),
),
@ -183,7 +183,7 @@ mod tests {
let client = TestClient::new(app);
let res = client.get("/foo/bar/baz").await;
assert_eq!(res.text().await, "/:a/:b/:c");
assert_eq!(res.text().await, "/{a}/{b}/{c}");
}
#[crate::test]
@ -197,7 +197,7 @@ mod tests {
}
let app = Router::new()
.nest_service("/:a", Router::new().route("/:b", get(|| async move {})))
.nest_service("/{a}", Router::new().route("/{b}", get(|| async move {})))
.layer(map_request(extract_matched_path));
let client = TestClient::new(app);
@ -212,12 +212,12 @@ mod tests {
matched_path: Option<MatchedPath>,
req: Request<B>,
) -> Request<B> {
assert_eq!(matched_path.unwrap().as_str(), "/:a/:b");
assert_eq!(matched_path.unwrap().as_str(), "/{a}/{b}");
req
}
let app = Router::new()
.nest("/:a", Router::new().route("/:b", get(|| async move {})))
.nest("/{a}", Router::new().route("/{b}", get(|| async move {})))
.layer(map_request(extract_matched_path));
let client = TestClient::new(app);
@ -234,7 +234,7 @@ mod tests {
}
let app = Router::new()
.nest_service("/:a", Router::new().route("/:b", get(|| async move {})))
.nest_service("/{a}", Router::new().route("/{b}", get(|| async move {})))
.layer(map_request(assert_no_matched_path));
let client = TestClient::new(app);
@ -251,7 +251,7 @@ mod tests {
}
let app = Router::new()
.nest("/:a", Router::new().route("/:b", get(|| async move {})))
.nest("/{a}", Router::new().route("/{b}", get(|| async move {})))
.layer(map_request(assert_matched_path));
let client = TestClient::new(app);
@ -263,14 +263,14 @@ mod tests {
#[crate::test]
async fn can_extract_nested_matched_path_in_middleware_on_nested_router() {
async fn extract_matched_path<B>(matched_path: MatchedPath, req: Request<B>) -> Request<B> {
assert_eq!(matched_path.as_str(), "/:a/:b");
assert_eq!(matched_path.as_str(), "/{a}/{b}");
req
}
let app = Router::new().nest(
"/:a",
"/{a}",
Router::new()
.route("/:b", get(|| async move {}))
.route("/{b}", get(|| async move {}))
.layer(map_request(extract_matched_path)),
);
@ -284,14 +284,14 @@ mod tests {
async fn can_extract_nested_matched_path_in_middleware_on_nested_router_via_extension() {
async fn extract_matched_path<B>(req: Request<B>) -> Request<B> {
let matched_path = req.extensions().get::<MatchedPath>().unwrap();
assert_eq!(matched_path.as_str(), "/:a/:b");
assert_eq!(matched_path.as_str(), "/{a}/{b}");
req
}
let app = Router::new().nest(
"/:a",
"/{a}",
Router::new()
.route("/:b", get(|| async move {}))
.route("/{b}", get(|| async move {}))
.layer(map_request(extract_matched_path)),
);
@ -307,7 +307,7 @@ mod tests {
assert!(path.is_none());
}
let app = Router::new().nest_service("/:a", handler.into_service());
let app = Router::new().nest_service("/{a}", handler.into_service());
let client = TestClient::new(app);
@ -321,7 +321,7 @@ mod tests {
use tower::ServiceExt;
let app = Router::new().route(
"/*path",
"/{*path}",
any(|req: Request| {
Router::new()
.nest("/", Router::new().route("/foo", get(|| async {})))
@ -349,4 +349,44 @@ mod tests {
let res = client.get("/foo/bar").await;
assert_eq!(res.status(), StatusCode::OK);
}
#[crate::test]
async fn matching_colon() {
let app = Router::new().without_v07_checks().route(
"/:foo",
get(|path: MatchedPath| async move { path.as_str().to_owned() }),
);
let client = TestClient::new(app);
let res = client.get("/:foo").await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "/:foo");
let res = client.get("/:bar").await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let res = client.get("/foo").await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[crate::test]
async fn matching_asterisk() {
let app = Router::new().without_v07_checks().route(
"/*foo",
get(|path: MatchedPath| async move { path.as_str().to_owned() }),
);
let client = TestClient::new(app);
let res = client.get("/*foo").await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "/*foo");
let res = client.get("/*bar").await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let res = client.get("/foo").await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
}

View file

@ -43,7 +43,7 @@ use std::{fmt, sync::Arc};
/// // ...
/// }
///
/// let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show));
/// let app = Router::new().route("/users/{user_id}/team/{team_id}", get(users_teams_show));
/// # let _: Router = app;
/// ```
///
@ -61,7 +61,7 @@ use std::{fmt, sync::Arc};
/// // ...
/// }
///
/// let app = Router::new().route("/users/:user_id", get(user_info));
/// let app = Router::new().route("/users/{user_id}", get(user_info));
/// # let _: Router = app;
/// ```
///
@ -98,7 +98,7 @@ use std::{fmt, sync::Arc};
/// }
///
/// let app = Router::new().route(
/// "/users/:user_id/team/:team_id",
/// "/users/{user_id}/team/{team_id}",
/// get(users_teams_show).post(users_teams_create),
/// );
/// # let _: Router = app;
@ -127,7 +127,7 @@ use std::{fmt, sync::Arc};
/// }
///
/// let app = Router::new()
/// .route("/users/:user_id/team/:team_id", get(params_map).post(params_vec));
/// .route("/users/{user_id}/team/{team_id}", get(params_map).post(params_vec));
/// # let _: Router = app;
/// ```
///
@ -438,7 +438,7 @@ impl std::error::Error for FailedToDeserializePathParams {}
/// }
/// }
///
/// let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show));
/// let app = Router::new().route("/users/{user_id}/team/{team_id}", get(users_teams_show));
/// # let _: Router = app;
/// ```
#[derive(Debug)]
@ -548,7 +548,7 @@ mod tests {
#[crate::test]
async fn extracting_url_params() {
let app = Router::new().route(
"/users/:id",
"/users/{id}",
get(|Path(id): Path<i32>| async move {
assert_eq!(id, 42);
})
@ -568,7 +568,7 @@ mod tests {
#[crate::test]
async fn extracting_url_params_multiple_times() {
let app = Router::new().route("/users/:id", get(|_: Path<i32>, _: Path<String>| async {}));
let app = Router::new().route("/users/{id}", get(|_: Path<i32>, _: Path<String>| async {}));
let client = TestClient::new(app);
@ -579,7 +579,7 @@ mod tests {
#[crate::test]
async fn percent_decoding() {
let app = Router::new().route(
"/:key",
"/{key}",
get(|Path(param): Path<String>| async move { param }),
);
@ -594,11 +594,11 @@ mod tests {
async fn supports_128_bit_numbers() {
let app = Router::new()
.route(
"/i/:key",
"/i/{key}",
get(|Path(param): Path<i128>| async move { param.to_string() }),
)
.route(
"/u/:key",
"/u/{key}",
get(|Path(param): Path<u128>| async move { param.to_string() }),
);
@ -615,11 +615,11 @@ mod tests {
async fn wildcard() {
let app = Router::new()
.route(
"/foo/*rest",
"/foo/{*rest}",
get(|Path(param): Path<String>| async move { param }),
)
.route(
"/bar/*rest",
"/bar/{*rest}",
get(|Path(params): Path<HashMap<String, String>>| async move {
params.get("rest").unwrap().clone()
}),
@ -636,7 +636,7 @@ mod tests {
#[crate::test]
async fn captures_dont_match_empty_path() {
let app = Router::new().route("/:key", get(|| async {}));
let app = Router::new().route("/{key}", get(|| async {}));
let client = TestClient::new(app);
@ -650,7 +650,7 @@ mod tests {
#[crate::test]
async fn captures_match_empty_inner_segments() {
let app = Router::new().route(
"/:key/method",
"/{key}/method",
get(|Path(param): Path<String>| async move { param.to_string() }),
);
@ -666,7 +666,7 @@ mod tests {
#[crate::test]
async fn captures_match_empty_inner_segments_near_end() {
let app = Router::new().route(
"/method/:key/",
"/method/{key}/",
get(|Path(param): Path<String>| async move { param.to_string() }),
);
@ -685,7 +685,7 @@ mod tests {
#[crate::test]
async fn captures_match_empty_trailing_segment() {
let app = Router::new().route(
"/method/:key",
"/method/{key}",
get(|Path(param): Path<String>| async move { param.to_string() }),
);
@ -717,7 +717,10 @@ mod tests {
}
}
let app = Router::new().route("/:key", get(|param: Path<Param>| async move { param.0 .0 }));
let app = Router::new().route(
"/{key}",
get(|param: Path<Param>| async move { param.0 .0 }),
);
let client = TestClient::new(app);
@ -731,7 +734,7 @@ mod tests {
#[crate::test]
async fn two_path_extractors() {
let app = Router::new().route("/:a/:b", get(|_: Path<String>, _: Path<String>| async {}));
let app = Router::new().route("/{a}/{b}", get(|_: Path<String>, _: Path<String>| async {}));
let client = TestClient::new(app);
@ -751,8 +754,11 @@ mod tests {
struct Tuple(String, String);
let app = Router::new()
.route("/foo/:a/:b/:c", get(|_: Path<(String, String)>| async {}))
.route("/bar/:a/:b/:c", get(|_: Path<Tuple>| async {}));
.route(
"/foo/{a}/{b}/{c}",
get(|_: Path<(String, String)>| async {}),
)
.route("/bar/{a}/{b}/{c}", get(|_: Path<Tuple>| async {}));
let client = TestClient::new(app);
@ -774,7 +780,7 @@ mod tests {
#[crate::test]
async fn deserialize_into_vec_of_tuples() {
let app = Router::new().route(
"/:a/:b",
"/{a}/{b}",
get(|Path(params): Path<Vec<(String, String)>>| async move {
assert_eq!(
params,
@ -805,31 +811,31 @@ mod tests {
let app = Router::new()
.route(
"/single/:a",
"/single/{a}",
get(|Path(a): Path<Date>| async move { format!("single: {a}") }),
)
.route(
"/tuple/:a/:b/:c",
"/tuple/{a}/{b}/{c}",
get(|Path((a, b, c)): Path<(Date, Date, Date)>| async move {
format!("tuple: {a} {b} {c}")
}),
)
.route(
"/vec/:a/:b/:c",
"/vec/{a}/{b}/{c}",
get(|Path(vec): Path<Vec<Date>>| async move {
let [a, b, c]: [Date; 3] = vec.try_into().unwrap();
format!("vec: {a} {b} {c}")
}),
)
.route(
"/vec_pairs/:a/:b/:c",
"/vec_pairs/{a}/{b}/{c}",
get(|Path(vec): Path<Vec<(String, Date)>>| async move {
let [(_, a), (_, b), (_, c)]: [(String, Date); 3] = vec.try_into().unwrap();
format!("vec_pairs: {a} {b} {c}")
}),
)
.route(
"/map/:a/:b/:c",
"/map/{a}/{b}/{c}",
get(|Path(mut map): Path<HashMap<String, Date>>| async move {
let a = map.remove("a").unwrap();
let b = map.remove("b").unwrap();
@ -838,7 +844,7 @@ mod tests {
}),
)
.route(
"/struct/:a/:b/:c",
"/struct/{a}/{b}/{c}",
get(|Path(params): Path<Params>| async move {
format!("struct: {} {} {}", params.a, params.b, params.c)
}),
@ -875,8 +881,8 @@ mod tests {
use serde_json::Value;
let app = Router::new()
.route("/one/:a", get(|_: Path<(Value, Value)>| async {}))
.route("/two/:a/:b", get(|_: Path<Value>| async {}));
.route("/one/{a}", get(|_: Path<(Value, Value)>| async {}))
.route("/two/{a}/{b}", get(|_: Path<Value>| async {}));
let client = TestClient::new(app);
@ -896,7 +902,7 @@ mod tests {
#[crate::test]
async fn raw_path_params() {
let app = Router::new().route(
"/:a/:b/:c",
"/{a}/{b}/{c}",
get(|params: RawPathParams| async move {
params
.into_iter()

View file

@ -46,7 +46,7 @@ use std::convert::Infallible;
/// use tower_http::trace::TraceLayer;
///
/// let api_routes = Router::new()
/// .route("/users/:id", get(|| async { /* ... */ }))
/// .route("/users/{id}", get(|| async { /* ... */ }))
/// .layer(
/// TraceLayer::new_for_http().make_span_with(|req: &Request<_>| {
/// let path = if let Some(path) = req.extensions().get::<OriginalUri>() {

View file

@ -82,7 +82,7 @@ use serde::{de::DeserializeOwned, Serialize};
/// # unimplemented!()
/// }
///
/// let app = Router::new().route("/users/:id", get(get_user));
/// let app = Router::new().route("/users/{id}", get(get_user));
/// # let _: Router = app;
/// ```
#[derive(Debug, Clone, Copy, Default)]

View file

@ -251,7 +251,7 @@
//! }),
//! )
//! .route(
//! "/users/:id",
//! "/users/{id}",
//! get({
//! let shared_state = Arc::clone(&shared_state);
//! move |path| get_user(path, shared_state)

View file

@ -99,9 +99,9 @@ impl<S> fmt::Debug for Router<S> {
}
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";
pub(crate) const NEST_TAIL_PARAM_CAPTURE: &str = "/{*__private__axum_nest_tail_param}";
pub(crate) const FALLBACK_PARAM: &str = "__private__axum_fallback";
pub(crate) const FALLBACK_PARAM_PATH: &str = "/*__private__axum_fallback";
pub(crate) const FALLBACK_PARAM_PATH: &str = "/{*__private__axum_fallback}";
impl<S> Router<S>
where
@ -154,6 +154,13 @@ where
}
}
#[doc = include_str!("../docs/routing/without_v07_checks.md")]
pub fn without_v07_checks(self) -> Self {
self.tap_inner_mut(|this| {
this.path_router.without_v07_checks();
})
}
#[doc = include_str!("../docs/routing/route.md")]
#[track_caller]
pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self {

View file

@ -14,6 +14,7 @@ pub(super) struct PathRouter<S, const IS_FALLBACK: bool> {
routes: HashMap<RouteId, Endpoint<S>>,
node: Arc<Node>,
prev_route_id: RouteId,
v7_checks: bool,
}
impl<S> PathRouter<S, true>
@ -32,26 +33,56 @@ where
}
}
fn validate_path(v7_checks: bool, path: &str) -> Result<(), &'static str> {
if path.is_empty() {
return Err("Paths must start with a `/`. Use \"/\" for root routes");
} else if !path.starts_with('/') {
return Err("Paths must start with a `/`");
}
if v7_checks {
validate_v07_paths(path)?;
}
Ok(())
}
fn validate_v07_paths(path: &str) -> Result<(), &'static str> {
path.split('/')
.find_map(|segment| {
if segment.starts_with(':') {
Some(Err(
"Path segments must not start with `:`. For capture groups, use \
`{capture}`. If you meant to literally match a segment starting with \
a colon, call `without_v07_checks` on the router.",
))
} else if segment.starts_with('*') {
Some(Err(
"Path segments must not start with `*`. For wildcard capture, use \
`{*wildcard}`. If you meant to literally match a segment starting with \
an asterisk, call `without_v07_checks` on the router.",
))
} else {
None
}
})
.unwrap_or(Ok(()))
}
impl<S, const IS_FALLBACK: bool> PathRouter<S, IS_FALLBACK>
where
S: Clone + Send + Sync + 'static,
{
pub(super) fn without_v07_checks(&mut self) {
self.v7_checks = false;
}
pub(super) fn route(
&mut self,
path: &str,
method_router: MethodRouter<S>,
) -> Result<(), Cow<'static, str>> {
fn validate_path(path: &str) -> Result<(), &'static str> {
if path.is_empty() {
return Err("Paths must start with a `/`. Use \"/\" for root routes");
} else if !path.starts_with('/') {
return Err("Paths must start with a `/`");
}
Ok(())
}
validate_path(path)?;
validate_path(self.v7_checks, path)?;
let endpoint = if let Some((route_id, Endpoint::MethodRouter(prev_method_router))) = self
.node
@ -97,11 +128,7 @@ where
path: &str,
endpoint: Endpoint<S>,
) -> Result<(), Cow<'static, str>> {
if path.is_empty() {
return Err("Paths must start with a `/`. Use \"/\" for root routes".into());
} else if !path.starts_with('/') {
return Err("Paths must start with a `/`".into());
}
validate_path(self.v7_checks, path)?;
let id = self.next_route_id();
self.set_node(path, id)?;
@ -125,8 +152,12 @@ where
routes,
node,
prev_route_id: _,
v7_checks,
} = other;
// If either of the two did not allow paths starting with `:` or `*`, do not allow them for the merged router either.
self.v7_checks |= v7_checks;
for (id, route) in routes {
let path = node
.route_id_to_path
@ -162,12 +193,14 @@ where
path_to_nest_at: &str,
router: PathRouter<S, IS_FALLBACK>,
) -> Result<(), Cow<'static, str>> {
let prefix = validate_nest_path(path_to_nest_at);
let prefix = validate_nest_path(self.v7_checks, path_to_nest_at);
let PathRouter {
routes,
node,
prev_route_id: _,
// Ignore the configuration of the nested router
v7_checks: _,
} = router;
for (id, endpoint) in routes {
@ -205,13 +238,13 @@ where
T::Response: IntoResponse,
T::Future: Send + 'static,
{
let path = validate_nest_path(path_to_nest_at);
let path = validate_nest_path(self.v7_checks, path_to_nest_at);
let prefix = path;
let path = if path.ends_with('/') {
format!("{path}*{NEST_TAIL_PARAM}")
format!("{path}{{*{NEST_TAIL_PARAM}}}")
} else {
format!("{path}/*{NEST_TAIL_PARAM}")
format!("{path}/{{*{NEST_TAIL_PARAM}}}")
};
let layer = (
@ -222,7 +255,7 @@ where
self.route_endpoint(&path, endpoint.clone())?;
// `/*rest` is not matched by `/` so we need to also register a router at the
// `/{*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.route_endpoint(prefix, endpoint.clone())?;
@ -255,6 +288,7 @@ where
routes,
node: self.node,
prev_route_id: self.prev_route_id,
v7_checks: self.v7_checks,
}
}
@ -287,6 +321,7 @@ where
routes,
node: self.node,
prev_route_id: self.prev_route_id,
v7_checks: self.v7_checks,
}
}
@ -313,6 +348,7 @@ where
routes,
node: self.node,
prev_route_id: self.prev_route_id,
v7_checks: self.v7_checks,
}
}
@ -362,11 +398,7 @@ where
}
// explicitly handle all variants in case matchit adds
// new ones we need to handle differently
Err(
MatchError::NotFound
| MatchError::ExtraTrailingSlash
| MatchError::MissingTrailingSlash,
) => Err((req, state)),
Err(MatchError::NotFound) => Err((req, state)),
}
}
@ -399,6 +431,7 @@ impl<S, const IS_FALLBACK: bool> Default for PathRouter<S, IS_FALLBACK> {
routes: Default::default(),
node: Default::default(),
prev_route_id: RouteId(0),
v7_checks: true,
}
}
}
@ -418,6 +451,7 @@ impl<S, const IS_FALLBACK: bool> Clone for PathRouter<S, IS_FALLBACK> {
routes: self.routes.clone(),
node: self.node.clone(),
prev_route_id: self.prev_route_id,
v7_checks: self.v7_checks,
}
}
}
@ -464,16 +498,22 @@ impl fmt::Debug for Node {
}
#[track_caller]
fn validate_nest_path(path: &str) -> &str {
fn validate_nest_path(v7_checks: bool, path: &str) -> &str {
if path.is_empty() {
// nesting at `""` and `"/"` should mean the same thing
return "/";
}
if path.contains('*') {
if path.split('/').any(|segment| {
segment.starts_with("{*") && segment.ends_with('}') && !segment.ends_with("}}")
}) {
panic!("Invalid route: nested routes cannot contain wildcards (*)");
}
if v7_checks {
validate_v07_paths(path).unwrap();
}
path
}

View file

@ -56,7 +56,7 @@ fn strip_prefix(uri: &Uri, prefix: &str) -> Option<Uri> {
// ^^^^ this much is matched and the length is 4. Thus if we chop off the first 4
// characters we get the remainder
//
// prefix = /api/:version
// prefix = /api/{version}
// path = /api/v0/users
// ^^^^^^^ this much is matched and the length is 7.
let mut matching_prefix_length = Some(0);
@ -66,7 +66,7 @@ fn strip_prefix(uri: &Uri, prefix: &str) -> Option<Uri> {
match item {
Item::Both(path_segment, prefix_segment) => {
if prefix_segment.starts_with(':') || path_segment == prefix_segment {
if is_capture(prefix_segment) || path_segment == prefix_segment {
// the prefix segment is either a param, which matches anything, or
// it actually matches the path segment
*matching_prefix_length.as_mut().unwrap() += path_segment.len();
@ -148,6 +148,14 @@ where
})
}
fn is_capture(segment: &str) -> bool {
segment.starts_with('{')
&& segment.ends_with('}')
&& !segment.starts_with("{{")
&& !segment.ends_with("}}")
&& !segment.starts_with("{*")
}
#[derive(Debug)]
enum Item<T> {
Both(T, T),
@ -279,74 +287,89 @@ mod tests {
expected = Some("/"),
);
test!(param_0, uri = "/", prefix = "/:param", expected = Some("/"),);
test!(
param_0,
uri = "/",
prefix = "/{param}",
expected = Some("/"),
);
test!(
param_1,
uri = "/a",
prefix = "/:param",
prefix = "/{param}",
expected = Some("/"),
);
test!(
param_2,
uri = "/a/b",
prefix = "/:param",
prefix = "/{param}",
expected = Some("/b"),
);
test!(
param_3,
uri = "/b/a",
prefix = "/:param",
prefix = "/{param}",
expected = Some("/a"),
);
test!(
param_4,
uri = "/a/b",
prefix = "/a/:param",
prefix = "/a/{param}",
expected = Some("/"),
);
test!(param_5, uri = "/b/a", prefix = "/a/:param", expected = None,);
test!(
param_5,
uri = "/b/a",
prefix = "/a/{param}",
expected = None,
);
test!(param_6, uri = "/a/b", prefix = "/:param/a", expected = None,);
test!(
param_6,
uri = "/a/b",
prefix = "/{param}/a",
expected = None,
);
test!(
param_7,
uri = "/b/a",
prefix = "/:param/a",
prefix = "/{param}/a",
expected = Some("/"),
);
test!(
param_8,
uri = "/a/b/c",
prefix = "/a/:param/c",
prefix = "/a/{param}/c",
expected = Some("/"),
);
test!(
param_9,
uri = "/c/b/a",
prefix = "/a/:param/c",
prefix = "/a/{param}/c",
expected = None,
);
test!(
param_10,
uri = "/a/",
prefix = "/:param",
prefix = "/{param}",
expected = Some("/"),
);
test!(param_11, uri = "/a", prefix = "/:param/", expected = None,);
test!(param_11, uri = "/a", prefix = "/{param}/", expected = None,);
test!(
param_12,
uri = "/a/",
prefix = "/:param/",
prefix = "/{param}/",
expected = Some("/"),
);

View file

@ -83,9 +83,9 @@ async fn routing() {
"/users",
get(|_: Request| async { "users#index" }).post(|_: Request| async { "users#create" }),
)
.route("/users/:id", get(|_: Request| async { "users#show" }))
.route("/users/{id}", get(|_: Request| async { "users#show" }))
.route(
"/users/:id/action",
"/users/{id}/action",
get(|_: Request| async { "users#action" }),
);
@ -289,7 +289,10 @@ async fn multiple_methods_for_one_handler() {
#[crate::test]
async fn wildcard_sees_whole_url() {
let app = Router::new().route("/api/*rest", get(|uri: Uri| async move { uri.to_string() }));
let app = Router::new().route(
"/api/{*rest}",
get(|uri: Uri| async move { uri.to_string() }),
);
let client = TestClient::new(app);
@ -357,7 +360,7 @@ async fn with_and_without_trailing_slash() {
#[crate::test]
async fn wildcard_doesnt_match_just_trailing_slash() {
let app = Router::new().route(
"/x/*path",
"/x/{*path}",
get(|Path(path): Path<String>| async move { path }),
);
@ -377,8 +380,8 @@ async fn wildcard_doesnt_match_just_trailing_slash() {
#[crate::test]
async fn what_matches_wildcard() {
let app = Router::new()
.route("/*key", get(|| async { "root" }))
.route("/x/*key", get(|| async { "x" }))
.route("/{*key}", get(|| async { "root" }))
.route("/x/{*key}", get(|| async { "x" }))
.fallback(|| async { "fallback" });
let client = TestClient::new(app);
@ -406,7 +409,7 @@ async fn what_matches_wildcard() {
async fn static_and_dynamic_paths() {
let app = Router::new()
.route(
"/:key",
"/{key}",
get(|Path(key): Path<String>| async move { format!("dynamic: {key}") }),
)
.route("/foo", get(|| async { "static" }));
@ -1054,3 +1057,19 @@ async fn impl_handler_for_into_response() {
assert_eq!(res.status(), StatusCode::CREATED);
assert_eq!(res.text().await, "thing created");
}
#[crate::test]
#[should_panic(
expected = "Path segments must not start with `:`. For capture groups, use `{capture}`. If you meant to literally match a segment starting with a colon, call `without_v07_checks` on the router."
)]
async fn colon_in_route() {
_ = Router::<()>::new().route("/:foo", get(|| async move {}));
}
#[crate::test]
#[should_panic(
expected = "Path segments must not start with `*`. For wildcard capture, use `{*wildcard}`. If you meant to literally match a segment starting with an asterisk, call `without_v07_checks` on the router."
)]
async fn asterisk_in_route() {
_ = Router::<()>::new().route("/*foo", get(|| async move {}));
}

View file

@ -10,7 +10,7 @@ async fn nesting_apps() {
get(|| async { "users#index" }).post(|| async { "users#create" }),
)
.route(
"/users/:id",
"/users/{id}",
get(
|params: extract::Path<HashMap<String, String>>| async move {
format!(
@ -22,7 +22,7 @@ async fn nesting_apps() {
),
)
.route(
"/games/:id",
"/games/{id}",
get(
|params: extract::Path<HashMap<String, String>>| async move {
format!(
@ -36,7 +36,7 @@ async fn nesting_apps() {
let app = Router::new()
.route("/", get(|| async { "hi" }))
.nest("/:version/api", api_routes);
.nest("/{version}/api", api_routes);
let client = TestClient::new(app);
@ -228,7 +228,7 @@ async fn nested_multiple_routes() {
}
#[test]
#[should_panic = "Invalid route \"/\": insertion failed due to conflict with previously registered route: /*__private__axum_nest_tail_param"]
#[should_panic = "Invalid route \"/\": insertion failed due to conflict with previously registered route: /"]
fn nested_service_at_root_with_other_routes() {
let _: Router = Router::new()
.nest_service("/", Router::new().route("/users", get(|| async {})))
@ -263,7 +263,7 @@ async fn multiple_top_level_nests() {
#[crate::test]
#[should_panic(expected = "Invalid route: nested routes cannot contain wildcards (*)")]
async fn nest_cannot_contain_wildcards() {
_ = Router::<()>::new().nest("/one/*rest", Router::new());
_ = Router::<()>::new().nest("/one/{*rest}", Router::new());
}
#[crate::test]
@ -317,11 +317,11 @@ async fn outer_middleware_still_see_whole_url() {
#[crate::test]
async fn nest_at_capture() {
let api_routes = Router::new().route(
"/:b",
"/{b}",
get(|Path((a, b)): Path<(String, String)>| async move { format!("a={a} b={b}") }),
);
let app = Router::new().nest("/:a", api_routes);
let app = Router::new().nest("/{a}", api_routes);
let client = TestClient::new(app);
@ -417,3 +417,19 @@ 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/");
#[crate::test]
#[should_panic(
expected = "Path segments must not start with `:`. For capture groups, use `{capture}`. If you meant to literally match a segment starting with a colon, call `without_v07_checks` on the router."
)]
async fn colon_in_route() {
_ = Router::<()>::new().nest("/:foo", Router::new());
}
#[crate::test]
#[should_panic(
expected = "Path segments must not start with `*`. For wildcard capture, use `{*wildcard}`. If you meant to literally match a segment starting with an asterisk, call `without_v07_checks` on the router."
)]
async fn asterisk_in_route() {
_ = Router::<()>::new().nest("/*foo", Router::new());
}

View file

@ -25,7 +25,7 @@ async fn main() {
.init();
// build our application with a route
let app = Router::new().route("/users/:user_id/teams/:team_id", get(handler));
let app = Router::new().route("/users/{user_id}/teams/{team_id}", get(handler));
// run it
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")

View file

@ -52,14 +52,14 @@ async fn main() {
// Using trait objects is recommended unless you really need generics.
let using_dyn = Router::new()
.route("/users/:id", get(get_user_dyn))
.route("/users/{id}", get(get_user_dyn))
.route("/users", post(create_user_dyn))
.with_state(AppStateDyn {
user_repo: Arc::new(user_repo.clone()),
});
let using_generic = Router::new()
.route("/users/:id", get(get_user_generic::<InMemoryUserRepo>))
.route("/users/{id}", get(get_user_generic::<InMemoryUserRepo>))
.route("/users", post(create_user_generic::<InMemoryUserRepo>))
.with_state(AppStateGeneric { user_repo });

View file

@ -45,7 +45,7 @@ async fn main() {
// Build our application by composing routes
let app = Router::new()
.route(
"/:key",
"/{key}",
// Add compression to `kv_get`
get(kv_get.layer(CompressionLayer::new()))
// But don't compress `kv_set`
@ -125,7 +125,7 @@ fn admin_routes() -> Router<SharedState> {
Router::new()
.route("/keys", delete(delete_all_keys))
.route("/key/:key", delete(remove_key))
.route("/key/{key}", delete(remove_key))
// Require bearer auth for all admin routes
.layer(ValidateRequestHeaderLayer::bearer("secret-token"))
}

View file

@ -37,7 +37,7 @@ async fn main() {
let app = Router::new()
.route("/", get(show_form).post(accept_form))
.route("/file/:file_name", post(save_request_body));
.route("/file/{file_name}", post(save_request_body));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await

View file

@ -25,7 +25,7 @@ async fn main() {
.init();
// build our application with some routes
let app = Router::new().route("/greet/:name", get(greet));
let app = Router::new().route("/greet/{name}", get(greet));
// run it
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")

View file

@ -4,8 +4,8 @@
//!
//! - `GET /todos`: return a JSON list of Todos.
//! - `POST /todos`: create a new Todo.
//! - `PATCH /todos/:id`: update a specific Todo.
//! - `DELETE /todos/:id`: delete a specific Todo.
//! - `PATCH /todos/{id}`: update a specific Todo.
//! - `DELETE /todos/{id}`: delete a specific Todo.
//!
//! Run with
//!
@ -48,7 +48,7 @@ async fn main() {
// Compose the routes
let app = Router::new()
.route("/todos", get(todos_index).post(todos_create))
.route("/todos/:id", patch(todos_update).delete(todos_delete))
.route("/todos/{id}", patch(todos_update).delete(todos_delete))
// Add middleware to all routes
.layer(
ServiceBuilder::new()

View file

@ -25,7 +25,7 @@ async fn main() {
.init();
// build our application with some routes
let app = Router::new().route("/:version/foo", get(handler));
let app = Router::new().route("/{version}/foo", get(handler));
// run it
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")