mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-17 00:00:16 +02:00
Support affixed typed path captures (#3782)
This commit is contained in:
@@ -362,8 +362,7 @@ fn format_str_from_path(segments: &[Segment]) -> String {
|
||||
Segment::Capture(capture, _) => format!("{{{capture}}}"),
|
||||
Segment::Static(segment) => segment.to_owned(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn captures_from_path(segments: &[Segment]) -> Vec<syn::Ident> {
|
||||
@@ -387,23 +386,58 @@ fn parse_path(path: &LitStr) -> syn::Result<Vec<Segment>> {
|
||||
return Err(syn::Error::new_spanned(path, "paths must start with a `/`"));
|
||||
}
|
||||
|
||||
path.value()
|
||||
.split('/')
|
||||
.map(|segment| {
|
||||
if let Some(capture) = segment
|
||||
.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 {
|
||||
Ok(Segment::Static(segment.to_owned()))
|
||||
let mut segments = Vec::new();
|
||||
let mut rest = value.as_str();
|
||||
|
||||
while let Some(start) = find_first_not_double(b'{', rest.as_bytes()) {
|
||||
if start > 0 {
|
||||
segments.push(Segment::Static(rest[..start].to_owned()));
|
||||
}
|
||||
|
||||
rest = &rest[start + 1..];
|
||||
|
||||
if let Some(end) = rest.find('}') {
|
||||
let capture = &rest[..end];
|
||||
if capture.is_empty() || capture.contains('{') {
|
||||
return Err(syn::Error::new_spanned(path, "invalid capture in path"));
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
segments.push(Segment::Capture(
|
||||
capture.strip_prefix('*').unwrap_or(capture).to_owned(),
|
||||
path.span(),
|
||||
));
|
||||
|
||||
rest = &rest[end + 1..];
|
||||
} else {
|
||||
segments.push(Segment::Static(format!("{{{rest}")));
|
||||
rest = "";
|
||||
}
|
||||
}
|
||||
|
||||
if !rest.is_empty() {
|
||||
segments.push(Segment::Static(rest.to_owned()));
|
||||
}
|
||||
|
||||
Ok(segments)
|
||||
}
|
||||
|
||||
fn find_first_not_double(needle: u8, haystack: &[u8]) -> Option<usize> {
|
||||
let mut possible_capture = 0;
|
||||
while let Some(index) = haystack
|
||||
.get(possible_capture..)
|
||||
.and_then(|haystack| haystack.iter().position(|byte| byte == &needle))
|
||||
{
|
||||
let index = index + possible_capture;
|
||||
|
||||
if haystack.get(index + 1) == Some(&needle) {
|
||||
possible_capture = index + 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
return Some(index);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
enum Segment {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
use axum_extra::routing::{RouterExt, TypedPath};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(TypedPath, Deserialize)]
|
||||
#[typed_path("/@{username}")]
|
||||
struct PrefixedCapture {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[derive(TypedPath, Deserialize)]
|
||||
#[typed_path("/files/{name}.json")]
|
||||
struct SuffixedCapture {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(TypedPath, Deserialize)]
|
||||
#[typed_path("/users/{user_id}/teams/team-{team_id}")]
|
||||
struct MixedCaptures {
|
||||
user_id: u32,
|
||||
team_id: u32,
|
||||
}
|
||||
|
||||
#[derive(TypedPath, Deserialize)]
|
||||
#[typed_path("/{{escaped_capture}}/{id}")]
|
||||
struct EscapedBrace {
|
||||
id: u32,
|
||||
}
|
||||
|
||||
#[derive(TypedPath, Deserialize)]
|
||||
#[typed_path("/@{username}")]
|
||||
struct UnnamedPrefixedCapture(String);
|
||||
|
||||
fn main() {
|
||||
_ = axum::Router::<()>::new().typed_get(|_: PrefixedCapture| async {});
|
||||
_ = axum::Router::<()>::new().typed_get(|_: SuffixedCapture| async {});
|
||||
_ = axum::Router::<()>::new().typed_get(|_: MixedCaptures| async {});
|
||||
_ = axum::Router::<()>::new().typed_get(|_: EscapedBrace| async {});
|
||||
|
||||
assert_eq!(PrefixedCapture::PATH, "/@{username}");
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
PrefixedCapture {
|
||||
username: "alice".to_owned(),
|
||||
}
|
||||
),
|
||||
"/@alice"
|
||||
);
|
||||
|
||||
assert_eq!(SuffixedCapture::PATH, "/files/{name}.json");
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
SuffixedCapture {
|
||||
name: "report final".to_owned(),
|
||||
}
|
||||
),
|
||||
"/files/report%20final.json"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
MixedCaptures {
|
||||
user_id: 1,
|
||||
team_id: 2,
|
||||
}
|
||||
),
|
||||
"/users/1/teams/team-2"
|
||||
);
|
||||
|
||||
assert_eq!(EscapedBrace::PATH, "/{{escaped_capture}}/{id}");
|
||||
assert_eq!(format!("{}", EscapedBrace { id: 7 }), "/{escaped_capture}/7");
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", UnnamedPrefixedCapture("bob".to_owned())),
|
||||
"/@bob"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user