1pub fn normalize_path(path: &str) -> String {
2 if path.is_empty() {
3 return "/".into();
4 }
5 if path.starts_with('/') {
6 path.to_string()
7 } else {
8 format!("/{path}")
9 }
10}
11
12pub(super) fn normalize_prefix(prefix: &str) -> String {
13 if prefix.is_empty() || prefix == "/" {
14 return String::new();
15 }
16 let mut p = normalize_path(prefix);
17 while p.ends_with('/') && p.len() > 1 {
18 p.pop();
19 }
20 p
21}
22
23pub fn join_paths(prefix: &str, path: &str) -> String {
24 let path = if path.is_empty() { "/" } else { path };
25 if prefix.is_empty() {
26 return normalize_path(path);
27 }
28 if path == "/" {
29 return prefix.to_string();
30 }
31 let path = path.strip_prefix('/').unwrap_or(path);
32 format!("{prefix}/{path}")
33}
34
35pub fn to_brace_path(path: &str) -> Option<String> {
40 let path = normalize_path(path);
41 if path == "/" {
42 return Some("/".into());
43 }
44 let mut out = String::new();
45 for seg in path.trim_matches('/').split('/') {
46 if seg.starts_with('*') {
47 return None;
48 }
49 out.push('/');
50 out.push_str(&colon_params_to_braces(seg));
51 }
52 Some(out)
53}
54
55pub(crate) fn to_matchit_path(path: &str) -> String {
59 let path = normalize_path(path);
60 if path == "/" {
61 return "/".into();
62 }
63 path.trim_matches('/')
64 .split('/')
65 .map(|seg| {
66 if let Some(name) = seg.strip_prefix('*') {
67 if name.is_empty() {
68 "{*path}".to_string()
69 } else {
70 format!("{{*{name}}}")
71 }
72 } else {
73 colon_params_to_braces(seg)
74 }
75 })
76 .fold(String::new(), |mut acc, seg| {
77 acc.push('/');
78 acc.push_str(&seg);
79 acc
80 })
81}
82
83fn colon_params_to_braces(seg: &str) -> String {
85 let bytes = seg.as_bytes();
86 let mut out = String::with_capacity(seg.len() + 2);
87 let mut i = 0;
88 while i < bytes.len() {
89 if bytes[i] == b':' {
90 let start = i + 1;
91 let mut end = start;
92 while end < bytes.len()
93 && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_')
94 {
95 end += 1;
96 }
97 if end > start {
98 out.push('{');
99 out.push_str(&seg[start..end]);
100 out.push('}');
101 i = end;
102 continue;
103 }
104 }
105 out.push(bytes[i] as char);
106 i += 1;
107 }
108 out
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn brace_path_params() {
117 assert_eq!(to_brace_path("/users/:id").as_deref(), Some("/users/{id}"));
118 assert_eq!(to_brace_path("/").as_deref(), Some("/"));
119 assert_eq!(
120 to_brace_path("/sitemap-:n.xml").as_deref(),
121 Some("/sitemap-{n}.xml")
122 );
123 }
124
125 #[test]
126 fn matchit_inline_param() {
127 assert_eq!(to_matchit_path("/sitemap-:n.xml"), "/sitemap-{n}.xml");
128 assert_eq!(to_matchit_path("/users/:id"), "/users/{id}");
129 }
130
131 #[test]
132 fn brace_path_rejects_wildcard() {
133 assert!(to_brace_path("/files/*path").is_none());
134 }
135}