use std::borrow::Cow;
#[derive(Debug, Clone)]
pub(crate) struct MountedPath {
pub(crate) axum: String,
pub(crate) shape: String,
pub(crate) captures: Vec<String>,
pub(crate) unbound: Vec<bool>,
pub(crate) multi_segment: Vec<bool>,
pub(crate) verb: Option<String>,
pub(crate) empty_last: bool,
pub(crate) last_template: Option<Vec<String>>,
pub(crate) last_segment: usize,
pub(crate) unsupported: Option<String>,
pub(crate) composites: Vec<Composite>,
pub(crate) literals: Vec<(usize, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Composite {
pub(crate) field: String,
pub(crate) parts: Vec<Part>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Part {
Literal(String),
Capture(usize),
}
impl MountedPath {
pub(crate) fn new(template: &str) -> Self {
let (base, verb) = split_verb(template);
let segments = split_top_level(base);
let last = segments.len() - 1;
let mut axum = String::with_capacity(base.len());
let mut shape = String::with_capacity(base.len());
let mut captures = Vec::new();
let mut unbound = Vec::new();
let mut multi_segment = Vec::new();
let mut composites = Vec::new();
let mut literals = Vec::new();
let mut unsupported = None;
let mut written = 0;
let empty_last = ends_in_double_wildcard(segments[last]);
let last_template = field_template(segments[last])
.filter(|template| *template != "*" && *template != "**")
.map(|template| {
split_top_level(template)
.into_iter()
.map(str::to_owned)
.collect()
});
let mut last_segment = 0;
for (idx, segment) in segments.iter().enumerate() {
if idx == last {
last_segment = written;
}
let mut push_segment = |axum: &mut String, shape: &mut String| {
if written > 0 {
axum.push('/');
shape.push('/');
}
written += 1;
};
if field_template(segment).is_some_and(|template| template.contains(['{', '}'])) {
unsupported = Some(format!(
"`{segment}` nests a variable in a field template, which holds none"
));
}
if idx == last
&& field_template(segment)
.is_some_and(|template| template.split('/').rev().skip(1).any(|p| p == "**"))
{
unsupported = Some(format!(
"`{segment}` puts `**` before another segment; it must be the last one"
));
}
let inner = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}'));
if let Some((field, template)) = inner
.and_then(|inner| inner.split_once('='))
.filter(|(_, template)| idx != last && *template != "*")
{
if template.split('/').any(|part| part == "**") {
unsupported = Some(format!(
"`**` in `{segment}` before the last segment matches any number of \
segments, which the router cannot hold there"
));
} else {
let multi = template.contains('/');
let mut parts = Vec::new();
for (k, part) in template.split('/').enumerate() {
push_segment(&mut axum, &mut shape);
let escaped = part.contains('%');
if part == "*" || escaped {
let name = format!("{field}.{k}");
axum.push('{');
axum.push_str(&name);
axum.push('}');
shape.push_str("{}");
captures.push(name);
unbound.push(true);
multi_segment.push(multi);
} else {
axum.push_str(part);
shape.push_str(part);
}
if escaped {
let at = axum.bytes().filter(|&b| b == b'/').count();
literals.push((at, part.to_owned()));
}
parts.push(if part == "*" || escaped {
Part::Capture(captures.len() - 1)
} else {
Part::Literal(part.to_owned())
});
}
composites.push(Composite {
field: field.to_owned(),
parts,
});
continue;
}
} else if idx != last && is_double_wildcard(segment) {
unsupported = Some(format!(
"`{segment}` before the last segment matches any number of segments, \
which the router cannot hold there"
));
}
push_segment(&mut axum, &mut shape);
match convert_segment(segment, idx, idx == last) {
Segment::Literal(literal) => {
axum.push_str(literal);
shape.push_str(literal);
}
Segment::Capture { name, catch_all } => {
let open = if catch_all { "{*" } else { "{" };
axum.push_str(open);
axum.push_str(&name);
axum.push('}');
shape.push_str(open);
shape.push('}');
captures.push(name);
unbound.push(*segment == "*" || *segment == "**");
multi_segment.push(catch_all && spans_segments(segment));
}
}
}
Self {
axum,
shape,
captures,
unbound,
multi_segment,
verb: verb.map(str::to_owned),
empty_last,
last_template,
last_segment,
unsupported,
composites,
literals,
}
}
pub(crate) fn routable(&self) -> Result<(), String> {
match &self.unsupported {
Some(reason) => Err(reason.clone()),
None => mountable(&self.axum),
}
}
pub(crate) fn display(&self) -> Cow<'_, str> {
match &self.verb {
None => Cow::Borrowed(&self.axum),
Some(verb) => Cow::Owned(format!("{}{verb}", self.axum)),
}
}
}
pub fn proto_path_to_axum(path: &str) -> String {
MountedPath::new(path).axum
}
pub(crate) fn mountable(axum_path: &str) -> Result<(), String> {
if !axum_path.starts_with('/') {
return Err("a path template must start with '/'".to_string());
}
if axum_path
.split('/')
.any(|segment| segment.starts_with(':') || segment.starts_with('*'))
{
return Err("a path segment must not start with ':' or '*'".to_string());
}
let mut captures = 0usize;
for segment in axum_path.split('/') {
let Some(at) = segment.find(['{', '}']) else {
continue;
};
let inner = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}'));
if at != 0 || inner.is_none_or(|inner| inner.contains(['{', '}'])) {
return Err(format!("`{segment}` is not a whole-segment variable"));
}
if !segment.starts_with("{*") {
captures += 1;
}
}
if captures > 25 {
return Err(format!(
"{captures} variables; the router holds at most 25 before the last"
));
}
matchit::Router::new()
.insert(axum_path, ())
.map_err(|e| e.to_string())
}
fn split_verb(template: &str) -> (&str, Option<&str>) {
let segments = split_top_level(template);
let last = segments[segments.len() - 1];
let start = template.len() - last.len();
let colon = if last.starts_with('{') {
closing_brace(last).map(|close| close + 1)
} else if last.starts_with("**") {
Some(2)
} else if last.starts_with('*') {
Some(1)
} else {
None
};
match colon {
Some(colon) if last[colon..].starts_with(':') && last.len() > colon + 1 => {
let at = start + colon;
(&template[..at], Some(&template[at..]))
}
_ => (template, None),
}
}
pub(crate) fn decode_multi_segment(raw: &str) -> Cow<'_, [u8]> {
let is_slash =
|escape: &[u8]| escape[0] == b'%' && escape[1] == b'2' && escape[2] | 0x20 == b'f';
let bytes = raw.as_bytes();
if !bytes.windows(3).any(is_slash) {
return percent_encoding::percent_decode(bytes).into();
}
let mut decoded = Vec::with_capacity(bytes.len());
let mut rest = bytes;
while let Some(at) = rest.windows(3).position(is_slash) {
decoded.extend(percent_encoding::percent_decode(&rest[..at]));
decoded.extend_from_slice(&rest[at..at + 3]);
rest = &rest[at + 3..];
}
decoded.extend(percent_encoding::percent_decode(rest));
Cow::Owned(decoded)
}
fn spans_segments(segment: &str) -> bool {
segment == "**"
|| field_template(segment)
.is_some_and(|template| template.contains('/') || template == "**")
}
fn is_double_wildcard(segment: &str) -> bool {
segment == "**" || field_template(segment) == Some("**")
}
fn ends_in_double_wildcard(segment: &str) -> bool {
is_double_wildcard(segment)
|| field_template(segment).is_some_and(|template| template.ends_with("/**"))
}
fn field_template(segment: &str) -> Option<&str> {
segment
.strip_prefix('{')
.and_then(|s| s.strip_suffix('}'))
.and_then(|inner| inner.split_once('='))
.map(|(_, template)| template)
}
fn closing_brace(segment: &str) -> Option<usize> {
let mut depth = 0usize;
for (i, ch) in segment.char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth = depth.checked_sub(1)?;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
fn split_top_level(path: &str) -> Vec<&str> {
let mut segments = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
for (i, ch) in path.char_indices() {
match ch {
'{' => depth += 1,
'}' if depth > 0 => depth -= 1,
'/' if depth == 0 => {
segments.push(&path[start..i]);
start = i + 1;
}
_ => {}
}
}
segments.push(&path[start..]);
segments
}
enum Segment<'a> {
Literal(&'a str),
Capture { name: String, catch_all: bool },
}
fn convert_segment(segment: &str, idx: usize, is_last: bool) -> Segment<'_> {
if let Some(inner) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
if let Some((name, template)) = inner.split_once('=') {
return match template {
"*" => capture(name, false),
"**" => capture(name, is_last),
template if !template.contains('/') => capture(name, false),
_ => capture(name, is_last),
};
}
return capture(inner, false);
}
match segment {
"**" => capture(&idx.to_string(), is_last),
"*" => capture(&idx.to_string(), false),
literal => Segment::Literal(literal),
}
}
fn capture(name: &str, catch_all: bool) -> Segment<'static> {
Segment::Capture {
name: name.to_owned(),
catch_all,
}
}