web_server_abstraction/
routing.rs1use std::collections::HashMap;
2
3#[derive(Debug, Clone)]
5pub struct Route {
6 pattern: String,
7 segments: Vec<RouteSegment>,
8}
9
10#[derive(Debug, Clone)]
11enum RouteSegment {
12 Static(String),
13 Parameter(String),
14 Wildcard,
15}
16
17impl Route {
18 pub fn new(pattern: impl Into<String>) -> Self {
25 let pattern = pattern.into();
26 let segments = Self::parse_pattern(&pattern);
27
28 Self { pattern, segments }
29 }
30
31 pub fn matches(&self, path: &str) -> Option<HashMap<String, String>> {
33 let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
34 let mut params = HashMap::new();
35
36 if path_segments.len() == 1
38 && path_segments[0].is_empty()
39 && (self.segments.is_empty()
40 || (self.segments.len() == 1
41 && matches!(self.segments[0], RouteSegment::Static(ref s) if s.is_empty())))
42 {
43 return Some(params);
44 }
45
46 let mut path_index = 0;
47 for segment in &self.segments {
48 match segment {
49 RouteSegment::Static(expected) => {
50 if expected.is_empty() {
51 continue; }
53 if path_index >= path_segments.len() || path_segments[path_index] != expected {
54 return None;
55 }
56 path_index += 1;
57 }
58 RouteSegment::Parameter(name) => {
59 if path_index >= path_segments.len() {
60 return None;
61 }
62 params.insert(name.clone(), path_segments[path_index].to_string());
63 path_index += 1;
64 }
65 RouteSegment::Wildcard => {
66 return Some(params);
68 }
69 }
70 }
71
72 if path_index == path_segments.len() {
74 Some(params)
75 } else {
76 None
77 }
78 }
79
80 fn parse_pattern(pattern: &str) -> Vec<RouteSegment> {
81 let mut segments = Vec::new();
82
83 for segment in pattern.split('/') {
84 if segment.is_empty() {
85 segments.push(RouteSegment::Static(String::new()));
86 continue;
87 }
88
89 if segment == "*" {
90 segments.push(RouteSegment::Wildcard);
91 } else if segment.starts_with('{') && segment.ends_with('}') {
92 let param_name = segment.trim_start_matches('{').trim_end_matches('}');
93 segments.push(RouteSegment::Parameter(param_name.to_string()));
94 } else {
95 segments.push(RouteSegment::Static(segment.to_string()));
96 }
97 }
98
99 segments
100 }
101
102 pub fn pattern(&self) -> &str {
104 &self.pattern
105 }
106}
107
108#[derive(Debug)]
110pub struct Router<T> {
111 routes: Vec<(Route, T)>,
112}
113
114impl<T> Router<T> {
115 pub fn new() -> Self {
117 Self { routes: Vec::new() }
118 }
119
120 pub fn add_route(&mut self, pattern: impl Into<String>, data: T) {
122 let route = Route::new(pattern);
123 self.routes.push((route, data));
124 }
125
126 pub fn match_route(&self, path: &str) -> Option<(&T, HashMap<String, String>)> {
128 for (route, data) in &self.routes {
129 if let Some(params) = route.matches(path) {
130 return Some((data, params));
131 }
132 }
133 None
134 }
135
136 pub fn routes(&self) -> &[(Route, T)] {
138 &self.routes
139 }
140}
141
142impl<T> Default for Router<T> {
143 fn default() -> Self {
144 Self::new()
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_static_route() {
154 let route = Route::new("/users");
155 assert!(route.matches("/users").is_some());
156 assert!(route.matches("/users/").is_none());
157 assert!(route.matches("/other").is_none());
158 }
159
160 #[test]
161 fn test_parameter_route() {
162 let route = Route::new("/users/{id}");
163
164 let params = route.matches("/users/123").unwrap();
165 assert_eq!(params.get("id"), Some(&"123".to_string()));
166
167 assert!(route.matches("/users").is_none());
168 assert!(route.matches("/users/123/posts").is_none());
169 }
170
171 #[test]
172 fn test_multiple_parameters() {
173 let route = Route::new("/users/{user_id}/posts/{post_id}");
174
175 let params = route.matches("/users/123/posts/456").unwrap();
176 assert_eq!(params.get("user_id"), Some(&"123".to_string()));
177 assert_eq!(params.get("post_id"), Some(&"456".to_string()));
178 }
179
180 #[test]
181 fn test_wildcard_route() {
182 let route = Route::new("/files/*");
183
184 assert!(route.matches("/files/any/path/here").is_some());
185 assert!(route.matches("/files/").is_some());
186 assert!(route.matches("/other/path").is_none());
187 }
188
189 #[test]
190 fn test_router() {
191 let mut router = Router::new();
192 router.add_route("/users/{id}", "user_handler");
193 router.add_route("/posts/{id}", "post_handler");
194
195 let (handler, params) = router.match_route("/users/123").unwrap();
196 assert_eq!(*handler, "user_handler");
197 assert_eq!(params.get("id"), Some(&"123".to_string()));
198
199 let (handler, params) = router.match_route("/posts/456").unwrap();
200 assert_eq!(*handler, "post_handler");
201 assert_eq!(params.get("id"), Some(&"456".to_string()));
202
203 assert!(router.match_route("/unknown").is_none());
204 }
205}