1use std::collections::BTreeMap;
9
10pub const IMPLICIT_METHODS: [&str; 2] = ["HEAD", "OPTIONS"];
12
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct RouteMeta {
16 pub summary: Option<String>,
18}
19
20impl RouteMeta {
21 pub fn summary<S: Into<String>>(summary: S) -> Self {
23 Self {
24 summary: Some(summary.into()),
25 }
26 }
27}
28
29pub type PathParams = BTreeMap<String, String>;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct RouteEntry {
35 pub pattern: String,
37 pub methods: Vec<(String, RouteMeta)>,
39}
40
41impl RouteEntry {
42 pub fn has_method(&self, method: &str) -> bool {
44 self.methods.iter().any(|(name, _)| name == method)
45 }
46}
47
48fn segments(path: &str) -> Vec<&str> {
50 let trimmed = if path.len() > 1 {
51 path.strip_suffix('/').unwrap_or(path)
52 } else {
53 path
54 };
55 trimmed.split('/').collect()
56}
57
58pub fn match_path(pattern: &str, pathname: &str) -> Option<PathParams> {
73 let pattern_segments = segments(pattern);
74 let path_segments = segments(pathname);
75 let mut params = PathParams::new();
76
77 for (index, expected) in pattern_segments.iter().enumerate() {
78 if let Some(name) = expected.strip_prefix('*') {
79 let rest = path_segments.get(index..).unwrap_or(&[]).join("/");
81 if !name.is_empty() {
82 params.insert(name.to_string(), rest);
83 }
84 return Some(params);
85 }
86 let actual = path_segments.get(index)?;
87 match expected.strip_prefix(':') {
88 Some(name) => {
89 if actual.is_empty() {
90 return None;
91 }
92 params.insert(name.to_string(), (*actual).to_string());
93 }
94 None => {
95 if expected != actual {
96 return None;
97 }
98 }
99 }
100 }
101
102 if path_segments.len() > pattern_segments.len() {
103 return None;
104 }
105 Some(params)
106}
107
108#[derive(Debug, Clone, Default)]
110pub struct RouteTable {
111 routes: Vec<RouteEntry>,
112}
113
114impl RouteTable {
115 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn register(&mut self, method: &str, pattern: &str, meta: RouteMeta) {
125 let method = method.to_uppercase();
126 let entry = match self
127 .routes
128 .iter_mut()
129 .position(|entry| entry.pattern == pattern)
130 {
131 Some(index) => &mut self.routes[index],
132 None => {
133 self.routes.push(RouteEntry {
134 pattern: pattern.to_string(),
135 methods: Vec::new(),
136 });
137 self.routes.last_mut().expect("just pushed")
138 }
139 };
140 match entry.methods.iter_mut().find(|(name, _)| *name == method) {
141 Some(existing) => existing.1 = meta,
142 None => entry.methods.push((method, meta)),
143 }
144 }
145
146 pub fn remove(&mut self, method: &str, pattern: &str) {
151 let method = method.to_uppercase();
152 let Some(index) = self
153 .routes
154 .iter()
155 .position(|entry| entry.pattern == pattern)
156 else {
157 return;
158 };
159 self.routes[index]
160 .methods
161 .retain(|(name, _)| *name != method);
162 if self.routes[index].methods.is_empty() {
163 self.routes.remove(index);
164 }
165 }
166
167 pub fn find(&self, pathname: &str) -> Option<&RouteEntry> {
169 self.match_path(pathname).map(|(entry, _)| entry)
170 }
171
172 pub fn match_path(&self, pathname: &str) -> Option<(&RouteEntry, PathParams)> {
176 self.routes
177 .iter()
178 .find_map(|entry| match_path(&entry.pattern, pathname).map(|params| (entry, params)))
179 }
180
181 pub fn allowed_methods(&self, pathname: &str) -> Option<Vec<String>> {
186 let entry = self.find(pathname)?;
187 Some(self.allowed_methods_of(entry))
188 }
189
190 pub fn allowed_methods_of(&self, entry: &RouteEntry) -> Vec<String> {
192 let mut methods: Vec<String> = entry.methods.iter().map(|(name, _)| name.clone()).collect();
193 if methods.iter().any(|method| method == "GET") && !methods.iter().any(|m| m == "HEAD") {
194 methods.push("HEAD".to_string());
195 }
196 if !methods.iter().any(|method| method == "OPTIONS") {
197 methods.push("OPTIONS".to_string());
198 }
199 methods.sort();
200 methods.dedup();
201 methods
202 }
203
204 pub fn entries(&self) -> &[RouteEntry] {
206 &self.routes
207 }
208
209 pub fn describe(&self) -> Vec<RouteDescription> {
214 let mut descriptions: Vec<RouteDescription> = self
215 .routes
216 .iter()
217 .map(|entry| RouteDescription {
218 path: entry.pattern.clone(),
219 methods: self.allowed_methods_of(entry),
220 summary: entry
221 .methods
222 .iter()
223 .find_map(|(_, meta)| meta.summary.clone()),
224 })
225 .collect();
226 descriptions.sort_by(|left, right| left.path.cmp(&right.path));
227 descriptions
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct RouteDescription {
234 pub path: String,
236 pub methods: Vec<String>,
238 pub summary: Option<String>,
240}