gpui_navigator/
matcher.rs1use std::collections::HashMap;
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum RoutePath {
17 Static(&'static str),
19 Dynamic(String),
21 Pattern(RoutePattern),
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct RoutePattern {
28 pub segments: Vec<Segment>,
30 pub priority: u8,
33}
34
35impl RoutePattern {
36 pub fn from_path(path: &str) -> Self {
43 let segments: Vec<Segment> = path
44 .split('/')
45 .filter(|s| !s.is_empty())
46 .map(Segment::parse)
47 .collect();
48
49 let priority = Self::calculate_priority(&segments);
50
51 Self { segments, priority }
52 }
53
54 fn calculate_priority(segments: &[Segment]) -> u8 {
62 let mut priority: u8 = 100;
63
64 for segment in segments {
65 match segment {
66 Segment::Static(_) => {
67 }
69 Segment::Param { .. } => {
70 priority = priority.saturating_sub(10);
71 }
72 Segment::Optional(_) => {
73 priority = priority.saturating_sub(5);
74 }
75 Segment::Wildcard => {
76 return 0;
78 }
79 }
80 }
81
82 priority
83 }
84
85 pub fn matches(&self, path: &str) -> Option<HashMap<String, String>> {
89 let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
90
91 self.match_segments(&path_segments)
92 }
93
94 fn match_segments(&self, path_segments: &[&str]) -> Option<HashMap<String, String>> {
96 let mut params = HashMap::new();
97 let mut path_idx = 0;
98 let mut pattern_idx = 0;
99
100 while pattern_idx < self.segments.len() {
101 let segment = &self.segments[pattern_idx];
102
103 match segment {
104 Segment::Static(expected) => {
105 if path_idx >= path_segments.len() || path_segments[path_idx] != expected {
107 return None;
108 }
109 path_idx += 1;
110 }
111 Segment::Param { name, constraint } => {
112 if path_idx >= path_segments.len() {
114 return None;
115 }
116
117 let value = path_segments[path_idx];
118
119 if let Some(constraint) = constraint {
121 if !constraint.validate(value) {
122 return None;
123 }
124 }
125
126 params.insert(name.clone(), value.to_string());
127 path_idx += 1;
128 }
129 Segment::Optional(inner) => {
130 if path_idx < path_segments.len() {
132 if let Segment::Param { name, constraint } = &**inner {
133 let value = path_segments[path_idx];
134
135 let is_valid = if let Some(constraint) = constraint {
136 constraint.validate(value)
137 } else {
138 true
139 };
140
141 if is_valid {
142 params.insert(name.clone(), value.to_string());
143 path_idx += 1;
144 }
145 }
146 }
147 }
148 Segment::Wildcard => {
149 return Some(params);
151 }
152 }
153
154 pattern_idx += 1;
155 }
156
157 if path_idx == path_segments.len() {
159 Some(params)
160 } else {
161 None
162 }
163 }
164}
165
166#[derive(Debug, Clone, PartialEq)]
168pub enum Segment {
169 Static(String),
171 Param {
173 name: String,
174 constraint: Option<Constraint>,
175 },
176 Optional(Box<Segment>),
178 Wildcard,
180}
181
182impl Segment {
183 pub fn parse(s: &str) -> Self {
191 if s == "*" {
192 return Segment::Wildcard;
193 }
194
195 if let Some(rest) = s.strip_prefix(':') {
196 if let Some(pos) = rest.find('<') {
200 let name = rest[..pos].to_string();
201 let constraint_str = &rest[pos + 1..rest.len() - 1]; let constraint = Constraint::parse(constraint_str);
204
205 Segment::Param {
206 name,
207 constraint: Some(constraint),
208 }
209 } else {
210 Segment::Param {
211 name: rest.to_string(),
212 constraint: None,
213 }
214 }
215 } else {
216 Segment::Static(s.to_string())
218 }
219 }
220}
221
222#[derive(Debug, Clone, PartialEq)]
224pub enum Constraint {
225 Pattern(String),
227 Numeric,
229 Uuid,
231}
232
233impl Constraint {
234 fn parse(s: &str) -> Self {
236 match s {
237 "\\d+" => Constraint::Numeric,
238 "uuid" => Constraint::Uuid,
239 _ => Constraint::Pattern(s.to_string()),
240 }
241 }
242
243 pub fn validate(&self, value: &str) -> bool {
245 match self {
246 Constraint::Numeric => value.chars().all(|c| c.is_ascii_digit()),
247 Constraint::Uuid => {
248 let parts: Vec<&str> = value.split('-').collect();
250 if parts.len() != 5 {
251 return false;
252 }
253
254 parts[0].len() == 8
255 && parts[1].len() == 4
256 && parts[2].len() == 4
257 && parts[3].len() == 4
258 && parts[4].len() == 12
259 && parts
260 .iter()
261 .all(|p| p.chars().all(|c| c.is_ascii_hexdigit()))
262 }
263 Constraint::Pattern(_pattern) => {
264 true
267 }
268 }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_segment_parsing() {
278 assert_eq!(
279 Segment::parse("users"),
280 Segment::Static("users".to_string())
281 );
282 assert_eq!(
283 Segment::parse(":id"),
284 Segment::Param {
285 name: "id".to_string(),
286 constraint: None
287 }
288 );
289 assert_eq!(Segment::parse("*"), Segment::Wildcard);
290 }
291
292 #[test]
293 fn test_segment_parsing_with_constraint() {
294 let segment = Segment::parse(":id<\\d+>");
295 match segment {
296 Segment::Param { name, constraint } => {
297 assert_eq!(name, "id");
298 assert_eq!(constraint, Some(Constraint::Numeric));
299 }
300 _ => panic!("Expected Param segment"),
301 }
302 }
303
304 #[test]
305 fn test_priority_calculation() {
306 let pattern1 = RoutePattern::from_path("/users");
307 assert_eq!(pattern1.priority, 100); let pattern2 = RoutePattern::from_path("/users/:id");
310 assert_eq!(pattern2.priority, 90); let pattern3 = RoutePattern::from_path("/users/:id/posts/:postId");
313 assert_eq!(pattern3.priority, 80); let pattern4 = RoutePattern::from_path("/files/*");
316 assert_eq!(pattern4.priority, 0); }
318
319 #[test]
320 fn test_static_route_matching() {
321 let pattern = RoutePattern::from_path("/users");
322
323 assert!(pattern.matches("/users").is_some());
324 assert!(pattern.matches("/posts").is_none());
325 assert!(pattern.matches("/users/123").is_none());
326 }
327
328 #[test]
329 fn test_dynamic_route_matching() {
330 let pattern = RoutePattern::from_path("/users/:id");
331
332 let params = pattern.matches("/users/123");
333 assert!(params.is_some());
334 assert_eq!(params.unwrap().get("id"), Some(&"123".to_string()));
335
336 assert!(pattern.matches("/users").is_none());
337 assert!(pattern.matches("/users/123/posts").is_none());
338 }
339
340 #[test]
341 fn test_wildcard_matching() {
342 let pattern = RoutePattern::from_path("/files/*");
343
344 assert!(pattern.matches("/files/docs").is_some());
345 assert!(pattern.matches("/files/docs/report.pdf").is_some());
346 assert!(pattern.matches("/other").is_none());
347 }
348
349 #[test]
350 fn test_numeric_constraint() {
351 let constraint = Constraint::Numeric;
352
353 assert!(constraint.validate("123"));
354 assert!(constraint.validate("0"));
355 assert!(!constraint.validate("abc"));
356 assert!(!constraint.validate("12a"));
357 }
358
359 #[test]
360 fn test_uuid_constraint() {
361 let constraint = Constraint::Uuid;
362
363 assert!(constraint.validate("550e8400-e29b-41d4-a716-446655440000"));
364 assert!(!constraint.validate("not-a-uuid"));
365 assert!(!constraint.validate("550e8400-e29b-41d4-a716"));
366 }
367
368 #[test]
369 fn test_constrained_param_matching() {
370 let pattern = RoutePattern::from_path("/users/:id<\\d+>");
371
372 assert!(pattern.matches("/users/123").is_some());
373 assert!(pattern.matches("/users/abc").is_none());
374 }
375
376 #[test]
377 fn test_complex_pattern() {
378 let pattern = RoutePattern::from_path("/api/users/:userId/posts/:postId");
379
380 let params = pattern.matches("/api/users/42/posts/7");
381 assert!(params.is_some());
382
383 let params = params.unwrap();
384 assert_eq!(params.get("userId"), Some(&"42".to_string()));
385 assert_eq!(params.get("postId"), Some(&"7".to_string()));
386 }
387}