feat(macros): add compile-time validations to TypedPath macro (#3797)

This commit is contained in:
Shudhanshu Patel
2026-07-14 15:06:04 +02:00
committed by GitHub
parent b70b6ad90b
commit 98aea470f9
7 changed files with 88 additions and 0 deletions
+42
View File
@@ -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}")]
| ^^^^^^^^^^^^^^^^^^^^^^