mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-07 00:00:15 +02:00
feat(macros): add compile-time validations to TypedPath macro (#3797)
This commit is contained in:
@@ -386,6 +386,8 @@ fn parse_path(path: &LitStr) -> syn::Result<Vec<Segment>> {
|
||||
return Err(syn::Error::new_spanned(path, "paths must start with a `/`"));
|
||||
}
|
||||
|
||||
validate_path_segments(&value, path)?;
|
||||
|
||||
let mut segments = Vec::new();
|
||||
let mut rest = value.as_str();
|
||||
|
||||
@@ -421,6 +423,46 @@ fn parse_path(path: &LitStr) -> syn::Result<Vec<Segment>> {
|
||||
Ok(segments)
|
||||
}
|
||||
|
||||
fn validate_path_segments(value: &str, path: &LitStr) -> syn::Result<()> {
|
||||
let mut wildcard_seen = false;
|
||||
|
||||
for segment in value.split('/') {
|
||||
if wildcard_seen {
|
||||
return Err(syn::Error::new_spanned(
|
||||
path,
|
||||
"Wildcards must be at the end of the path",
|
||||
));
|
||||
}
|
||||
|
||||
let mut capture_count = 0;
|
||||
let mut rest = segment;
|
||||
while let Some(start) = find_first_not_double(b'{', rest.as_bytes()) {
|
||||
capture_count += 1;
|
||||
rest = &rest[start + 1..];
|
||||
|
||||
if rest.starts_with('*') {
|
||||
wildcard_seen = true;
|
||||
// a wildcard capture must cover its entire segment
|
||||
if start != 0 || rest.find('}') != Some(rest.len() - 1) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
path,
|
||||
"Wildcards must be at the end of the path",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if capture_count > 1 {
|
||||
return Err(syn::Error::new_spanned(
|
||||
path,
|
||||
"Cannot have multiple path parameters in a single segment",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_first_not_double(needle: u8, haystack: &[u8]) -> Option<usize> {
|
||||
let mut possible_capture = 0;
|
||||
while let Some(index) = haystack
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
use axum_macros::TypedPath;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(TypedPath, Deserialize)]
|
||||
#[typed_path("/{*rest}/foo")]
|
||||
struct MyPath {
|
||||
rest: String,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: Wildcards must be at the end of the path
|
||||
--> tests/typed_path/fail/catch_all_not_at_end.rs:5:14
|
||||
|
|
||||
5 | #[typed_path("/{*rest}/foo")]
|
||||
| ^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1,10 @@
|
||||
use axum_macros::TypedPath;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(TypedPath, Deserialize)]
|
||||
#[typed_path("/files/{*rest}.txt")]
|
||||
struct MyPath {
|
||||
rest: String,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: Wildcards must be at the end of the path
|
||||
--> tests/typed_path/fail/catch_all_not_whole_segment.rs:5:14
|
||||
|
|
||||
5 | #[typed_path("/files/{*rest}.txt")]
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1,11 @@
|
||||
use axum_macros::TypedPath;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(TypedPath, Deserialize)]
|
||||
#[typed_path("/user-{first}-{last}")]
|
||||
struct MyPath {
|
||||
first: String,
|
||||
last: String,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: Cannot have multiple path parameters in a single segment
|
||||
--> tests/typed_path/fail/multiple_params_in_segment.rs:5:14
|
||||
|
|
||||
5 | #[typed_path("/user-{first}-{last}")]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
Reference in New Issue
Block a user