diff --git a/axum-macros/src/typed_path.rs b/axum-macros/src/typed_path.rs index 77ffa8f4..520eefed 100644 --- a/axum-macros/src/typed_path.rs +++ b/axum-macros/src/typed_path.rs @@ -386,6 +386,8 @@ fn parse_path(path: &LitStr) -> syn::Result> { 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> { 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 { let mut possible_capture = 0; while let Some(index) = haystack diff --git a/axum-macros/tests/typed_path/fail/catch_all_not_at_end.rs b/axum-macros/tests/typed_path/fail/catch_all_not_at_end.rs new file mode 100644 index 00000000..8b566a93 --- /dev/null +++ b/axum-macros/tests/typed_path/fail/catch_all_not_at_end.rs @@ -0,0 +1,10 @@ +use axum_macros::TypedPath; +use serde::Deserialize; + +#[derive(TypedPath, Deserialize)] +#[typed_path("/{*rest}/foo")] +struct MyPath { + rest: String, +} + +fn main() {} diff --git a/axum-macros/tests/typed_path/fail/catch_all_not_at_end.stderr b/axum-macros/tests/typed_path/fail/catch_all_not_at_end.stderr new file mode 100644 index 00000000..62c16cd6 --- /dev/null +++ b/axum-macros/tests/typed_path/fail/catch_all_not_at_end.stderr @@ -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")] + | ^^^^^^^^^^^^^^ diff --git a/axum-macros/tests/typed_path/fail/catch_all_not_whole_segment.rs b/axum-macros/tests/typed_path/fail/catch_all_not_whole_segment.rs new file mode 100644 index 00000000..2979f544 --- /dev/null +++ b/axum-macros/tests/typed_path/fail/catch_all_not_whole_segment.rs @@ -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() {} diff --git a/axum-macros/tests/typed_path/fail/catch_all_not_whole_segment.stderr b/axum-macros/tests/typed_path/fail/catch_all_not_whole_segment.stderr new file mode 100644 index 00000000..18cc5757 --- /dev/null +++ b/axum-macros/tests/typed_path/fail/catch_all_not_whole_segment.stderr @@ -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")] + | ^^^^^^^^^^^^^^^^^^^^ diff --git a/axum-macros/tests/typed_path/fail/multiple_params_in_segment.rs b/axum-macros/tests/typed_path/fail/multiple_params_in_segment.rs new file mode 100644 index 00000000..32d4a7dc --- /dev/null +++ b/axum-macros/tests/typed_path/fail/multiple_params_in_segment.rs @@ -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() {} diff --git a/axum-macros/tests/typed_path/fail/multiple_params_in_segment.stderr b/axum-macros/tests/typed_path/fail/multiple_params_in_segment.stderr new file mode 100644 index 00000000..0d209465 --- /dev/null +++ b/axum-macros/tests/typed_path/fail/multiple_params_in_segment.stderr @@ -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}")] + | ^^^^^^^^^^^^^^^^^^^^^^