Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feature: percent-decode before routing #2729

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions axum-extra/src/extract/optional_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,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)]
Expand Down Expand Up @@ -77,7 +77,7 @@ mod tests {

let app = Router::new()
.route("/", get(handle))
.route("/:num", get(handle));
.route("/{num}", get(handle));

let client = TestClient::new(app);

Expand Down
2 changes: 1 addition & 1 deletion axum-extra/src/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,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`
Expand Down
2 changes: 1 addition & 1 deletion axum-extra/src/handler/or.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion axum-extra/src/protobuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,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)]
Expand Down
4 changes: 2 additions & 2 deletions axum-extra/src/routing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
);

Expand Down
28 changes: 18 additions & 10 deletions axum-extra/src/routing/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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>,
Expand All @@ -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>,
Expand All @@ -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>,
Expand All @@ -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 {
Expand Down
22 changes: 11 additions & 11 deletions axum-extra/src/routing/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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,
/// }
Expand All @@ -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,
/// }
Expand All @@ -117,7 +117,7 @@ use serde::Serialize;
/// struct UsersCollection;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// #[typed_path("/users/{id}")]
/// struct UsersMember(u32);
/// ```
///
Expand All @@ -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,
/// }
Expand Down Expand Up @@ -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,
/// }
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -398,7 +398,7 @@ mod tests {
use serde::Deserialize;

#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
#[typed_path("/users/{id}")]
struct UsersShow {
id: i32,
}
Expand Down
2 changes: 1 addition & 1 deletion axum-extra/src/typed_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,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;
/// ```
///
Expand Down
8 changes: 6 additions & 2 deletions axum-macros/src/typed_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,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 {
Expand Down
2 changes: 1 addition & 1 deletion axum-macros/tests/typed_path/fail/missing_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions axum-macros/tests/typed_path/fail/missing_field.stderr
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion axum-macros/tests/typed_path/fail/not_deserialize.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use axum_macros::TypedPath;

#[derive(TypedPath)]
#[typed_path("/users/:id")]
#[typed_path("/users/{id}")]
struct MyPath {
id: u32,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use axum_extra::routing::TypedPath;

#[derive(TypedPath)]
#[typed_path(":foo")]
#[typed_path("{foo}")]
struct MyPath;

fn main() {}
Original file line number Diff line number Diff line change
@@ -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}")]
| ^^^^^^^
2 changes: 1 addition & 1 deletion axum-macros/tests/typed_path/fail/unit_with_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
4 changes: 2 additions & 2 deletions axum-macros/tests/typed_path/fail/unit_with_capture.stderr
Original file line number Diff line number Diff line change
@@ -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}")]
| ^^^^^^^^^^^^^
4 changes: 2 additions & 2 deletions axum-macros/tests/typed_path/pass/customize_rejection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions axum-macros/tests/typed_path/pass/into_uri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
4 changes: 2 additions & 2 deletions axum-macros/tests/typed_path/pass/named_fields_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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!(
"{}",
Expand Down
2 changes: 1 addition & 1 deletion axum-macros/tests/typed_path/pass/option_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
4 changes: 2 additions & 2 deletions axum-macros/tests/typed_path/pass/tuple_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
4 changes: 2 additions & 2 deletions axum-macros/tests/typed_path/pass/url_encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading
Loading