web_server_abstraction/
routing.rs

1use std::collections::HashMap;
2
3/// Route pattern matching for extracting path parameters
4#[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    /// Create a new route pattern
19    ///
20    /// Supports patterns like:
21    /// - `/users/{id}` - captures `id` parameter
22    /// - `/users/{id}/posts/{post_id}` - captures multiple parameters
23    /// - `/files/*` - wildcard matching
24    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    /// Check if a path matches this route and extract parameters
32    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        // Handle empty path
37        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; // Skip empty segments (from leading /)
52                    }
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                    // Wildcard matches everything remaining
67                    return Some(params);
68                }
69            }
70        }
71
72        // All segments must be consumed
73        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    /// Get the original pattern
103    pub fn pattern(&self) -> &str {
104        &self.pattern
105    }
106}
107
108/// Simple router that matches routes and extracts parameters
109#[derive(Debug)]
110pub struct Router<T> {
111    routes: Vec<(Route, T)>,
112}
113
114impl<T> Router<T> {
115    /// Create a new router
116    pub fn new() -> Self {
117        Self { routes: Vec::new() }
118    }
119
120    /// Add a route with associated data
121    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    /// Find a matching route and return the data and extracted parameters
127    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    /// Get all routes
137    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}