mirror of
https://github.com/tokio-rs/axum.git
synced 2026-09-06 00:00:17 +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::Capture(capture, _) => format!("{{{capture}}}"),
|
||||||
Segment::Static(segment) => segment.to_owned(),
|
Segment::Static(segment) => segment.to_owned(),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect()
|
||||||
.join("/")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn captures_from_path(segments: &[Segment]) -> Vec<syn::Ident> {
|
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 `/`"));
|
return Err(syn::Error::new_spanned(path, "paths must start with a `/`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
path.value()
|
let mut segments = Vec::new();
|
||||||
.split('/')
|
let mut rest = value.as_str();
|
||||||
.map(|segment| {
|
|
||||||
if let Some(capture) = segment
|
while let Some(start) = find_first_not_double(b'{', rest.as_bytes()) {
|
||||||
.strip_prefix('{')
|
if start > 0 {
|
||||||
.and_then(|segment| segment.strip_suffix('}'))
|
segments.push(Segment::Static(rest[..start].to_owned()));
|
||||||
.and_then(|segment| {
|
}
|
||||||
(!segment.starts_with('{') && !segment.ends_with('}')).then_some(segment)
|
|
||||||
})
|
rest = &rest[start + 1..];
|
||||||
.map(|capture| capture.strip_prefix('*').unwrap_or(capture))
|
|
||||||
{
|
if let Some(end) = rest.find('}') {
|
||||||
Ok(Segment::Capture(capture.to_owned(), path.span()))
|
let capture = &rest[..end];
|
||||||
} else {
|
if capture.is_empty() || capture.contains('{') {
|
||||||
Ok(Segment::Static(segment.to_owned()))
|
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 {
|
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