1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct RouteSegment {
7 pub value: String,
8}
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct RouteParam {
13 pub name: String,
14 pub value: String,
15}
16
17#[must_use]
19pub fn normalize_route(input: &str) -> String {
20 let trimmed = input.trim();
21 if trimmed.is_empty() {
22 return String::from("/");
23 }
24
25 let absolute = trimmed.starts_with('/');
26 let segments = trimmed
27 .split('/')
28 .filter(|segment| !segment.is_empty())
29 .collect::<Vec<_>>();
30
31 if segments.is_empty() {
32 return String::from("/");
33 }
34
35 let joined = segments.join("/");
36 if absolute {
37 format!("/{joined}")
38 } else {
39 joined
40 }
41}
42
43#[must_use]
45pub fn join_routes(a: &str, b: &str) -> String {
46 let absolute = a.trim().starts_with('/');
47 let segments = a
48 .split('/')
49 .chain(b.split('/'))
50 .filter(|segment| !segment.trim().is_empty())
51 .collect::<Vec<_>>();
52
53 if segments.is_empty() {
54 return if absolute {
55 String::from("/")
56 } else {
57 String::new()
58 };
59 }
60
61 let joined = segments.join("/");
62 if absolute {
63 format!("/{joined}")
64 } else {
65 joined
66 }
67}
68
69#[must_use]
71pub fn split_route(input: &str) -> Vec<RouteSegment> {
72 let normalized = normalize_route(input);
73 normalized
74 .split('/')
75 .filter(|segment| !segment.is_empty())
76 .map(|segment| RouteSegment {
77 value: segment.to_string(),
78 })
79 .collect()
80}
81
82#[must_use]
84pub fn is_absolute_route(input: &str) -> bool {
85 input.trim().starts_with('/')
86}
87
88#[must_use]
90pub fn route_depth(input: &str) -> usize {
91 split_route(input).len()
92}
93
94#[must_use]
96pub fn matches_route_pattern(pattern: &str, path: &str) -> bool {
97 let pattern_segments = split_route(pattern);
98 let path_segments = split_route(path);
99 if pattern_segments.len() != path_segments.len() {
100 return false;
101 }
102
103 pattern_segments
104 .iter()
105 .zip(path_segments.iter())
106 .all(|(pattern_segment, path_segment)| {
107 pattern_segment.value.starts_with(':') || pattern_segment.value == path_segment.value
108 })
109}
110
111#[must_use]
113pub fn extract_route_params(pattern: &str, path: &str) -> Vec<RouteParam> {
114 if !matches_route_pattern(pattern, path) {
115 return Vec::new();
116 }
117
118 split_route(pattern)
119 .into_iter()
120 .zip(split_route(path))
121 .filter_map(|(pattern_segment, path_segment)| {
122 pattern_segment
123 .value
124 .strip_prefix(':')
125 .filter(|name| !name.is_empty())
126 .map(|name| RouteParam {
127 name: name.to_string(),
128 value: path_segment.value,
129 })
130 })
131 .collect()
132}