mockforge_core/
routing.rs

1//! Route registry and routing logic for MockForge
2
3use crate::Result;
4use std::collections::HashMap;
5
6/// HTTP method enum
7#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
8#[serde(rename_all = "lowercase")]
9pub enum HttpMethod {
10    GET,
11    POST,
12    PUT,
13    DELETE,
14    PATCH,
15    HEAD,
16    OPTIONS,
17}
18
19/// Route definition
20#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
21pub struct Route {
22    /// HTTP method
23    pub method: HttpMethod,
24    /// Path pattern (supports wildcards)
25    pub path: String,
26    /// Route priority (higher = more specific)
27    pub priority: i32,
28    /// Additional metadata
29    pub metadata: HashMap<String, serde_json::Value>,
30}
31
32impl Route {
33    /// Create a new route
34    pub fn new(method: HttpMethod, path: String) -> Self {
35        Self {
36            method,
37            path,
38            priority: 0,
39            metadata: HashMap::new(),
40        }
41    }
42
43    /// Set route priority
44    pub fn with_priority(mut self, priority: i32) -> Self {
45        self.priority = priority;
46        self
47    }
48
49    /// Add metadata
50    pub fn with_metadata(mut self, key: String, value: serde_json::Value) -> Self {
51        self.metadata.insert(key, value);
52        self
53    }
54}
55
56/// Route registry for managing routes across different protocols
57#[derive(Debug, Clone)]
58pub struct RouteRegistry {
59    /// HTTP routes indexed by method and path pattern
60    http_routes: HashMap<HttpMethod, Vec<Route>>,
61    /// WebSocket routes
62    ws_routes: Vec<Route>,
63    /// gRPC service routes
64    grpc_routes: HashMap<String, Vec<Route>>,
65}
66
67impl RouteRegistry {
68    /// Create a new empty route registry
69    pub fn new() -> Self {
70        Self {
71            http_routes: HashMap::new(),
72            ws_routes: Vec::new(),
73            grpc_routes: HashMap::new(),
74        }
75    }
76
77    /// Add an HTTP route
78    pub fn add_http_route(&mut self, route: Route) -> Result<()> {
79        self.http_routes.entry(route.method.clone()).or_default().push(route);
80        Ok(())
81    }
82
83    /// Add a WebSocket route
84    pub fn add_ws_route(&mut self, route: Route) -> Result<()> {
85        self.ws_routes.push(route);
86        Ok(())
87    }
88
89    /// Clear all routes
90    pub fn clear(&mut self) {
91        self.http_routes.clear();
92        self.ws_routes.clear();
93        self.grpc_routes.clear();
94    }
95
96    /// Add a generic route (alias for add_http_route)
97    pub fn add_route(&mut self, route: Route) -> Result<()> {
98        self.add_http_route(route)
99    }
100
101    /// Add a gRPC route
102    pub fn add_grpc_route(&mut self, service: String, route: Route) -> Result<()> {
103        self.grpc_routes.entry(service).or_default().push(route);
104        Ok(())
105    }
106
107    /// Find matching HTTP routes
108    pub fn find_http_routes(&self, method: &HttpMethod, path: &str) -> Vec<&Route> {
109        self.http_routes
110            .get(method)
111            .map(|routes| {
112                routes.iter().filter(|route| self.matches_path(&route.path, path)).collect()
113            })
114            .unwrap_or_default()
115    }
116
117    /// Find matching WebSocket routes
118    pub fn find_ws_routes(&self, path: &str) -> Vec<&Route> {
119        self.ws_routes
120            .iter()
121            .filter(|route| self.matches_path(&route.path, path))
122            .collect()
123    }
124
125    /// Find matching gRPC routes
126    pub fn find_grpc_routes(&self, service: &str, method: &str) -> Vec<&Route> {
127        self.grpc_routes
128            .get(service)
129            .map(|routes| {
130                routes.iter().filter(|route| self.matches_path(&route.path, method)).collect()
131            })
132            .unwrap_or_default()
133    }
134
135    /// Check if a path matches a route pattern
136    fn matches_path(&self, pattern: &str, path: &str) -> bool {
137        if pattern == path {
138            return true;
139        }
140
141        // Simple wildcard matching (* matches any segment)
142        if pattern.contains('*') {
143            let pattern_parts: Vec<&str> = pattern.split('/').collect();
144            let path_parts: Vec<&str> = path.split('/').collect();
145
146            if pattern_parts.len() != path_parts.len() {
147                return false;
148            }
149
150            for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
151                if *pattern_part != "*" && *pattern_part != *path_part {
152                    return false;
153                }
154            }
155            return true;
156        }
157
158        false
159    }
160
161    /// Get all HTTP routes for a method
162    pub fn get_http_routes(&self, method: &HttpMethod) -> Vec<&Route> {
163        self.http_routes
164            .get(method)
165            .map(|routes| routes.iter().collect())
166            .unwrap_or_default()
167    }
168
169    /// Get all WebSocket routes
170    pub fn get_ws_routes(&self) -> Vec<&Route> {
171        self.ws_routes.iter().collect()
172    }
173
174    /// Get all gRPC routes for a service
175    pub fn get_grpc_routes(&self, service: &str) -> Vec<&Route> {
176        self.grpc_routes
177            .get(service)
178            .map(|routes| routes.iter().collect())
179            .unwrap_or_default()
180    }
181}
182
183impl Default for RouteRegistry {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_route_new() {
195        let route = Route::new(HttpMethod::GET, "/api/users".to_string());
196        assert_eq!(route.method, HttpMethod::GET);
197        assert_eq!(route.path, "/api/users");
198        assert_eq!(route.priority, 0);
199        assert!(route.metadata.is_empty());
200    }
201
202    #[test]
203    fn test_route_with_priority() {
204        let route = Route::new(HttpMethod::POST, "/api/users".to_string()).with_priority(10);
205        assert_eq!(route.priority, 10);
206    }
207
208    #[test]
209    fn test_route_with_metadata() {
210        let route = Route::new(HttpMethod::GET, "/api/users".to_string())
211            .with_metadata("version".to_string(), serde_json::json!("v1"))
212            .with_metadata("auth".to_string(), serde_json::json!(true));
213
214        assert_eq!(route.metadata.get("version"), Some(&serde_json::json!("v1")));
215        assert_eq!(route.metadata.get("auth"), Some(&serde_json::json!(true)));
216    }
217
218    #[test]
219    fn test_route_registry_new() {
220        let registry = RouteRegistry::new();
221        assert!(registry.http_routes.is_empty());
222        assert!(registry.ws_routes.is_empty());
223        assert!(registry.grpc_routes.is_empty());
224    }
225
226    #[test]
227    fn test_route_registry_default() {
228        let registry = RouteRegistry::default();
229        assert!(registry.http_routes.is_empty());
230    }
231
232    #[test]
233    fn test_add_http_route() {
234        let mut registry = RouteRegistry::new();
235        let route = Route::new(HttpMethod::GET, "/api/users".to_string());
236
237        assert!(registry.add_http_route(route).is_ok());
238        assert_eq!(registry.get_http_routes(&HttpMethod::GET).len(), 1);
239    }
240
241    #[test]
242    fn test_add_multiple_http_routes() {
243        let mut registry = RouteRegistry::new();
244
245        registry
246            .add_http_route(Route::new(HttpMethod::GET, "/api/users".to_string()))
247            .unwrap();
248        registry
249            .add_http_route(Route::new(HttpMethod::GET, "/api/posts".to_string()))
250            .unwrap();
251        registry
252            .add_http_route(Route::new(HttpMethod::POST, "/api/users".to_string()))
253            .unwrap();
254
255        assert_eq!(registry.get_http_routes(&HttpMethod::GET).len(), 2);
256        assert_eq!(registry.get_http_routes(&HttpMethod::POST).len(), 1);
257    }
258
259    #[test]
260    fn test_add_ws_route() {
261        let mut registry = RouteRegistry::new();
262        let route = Route::new(HttpMethod::GET, "/ws/chat".to_string());
263
264        assert!(registry.add_ws_route(route).is_ok());
265        assert_eq!(registry.get_ws_routes().len(), 1);
266    }
267
268    #[test]
269    fn test_add_grpc_route() {
270        let mut registry = RouteRegistry::new();
271        let route = Route::new(HttpMethod::POST, "GetUser".to_string());
272
273        assert!(registry.add_grpc_route("UserService".to_string(), route).is_ok());
274        assert_eq!(registry.get_grpc_routes("UserService").len(), 1);
275    }
276
277    #[test]
278    fn test_add_route_alias() {
279        let mut registry = RouteRegistry::new();
280        let route = Route::new(HttpMethod::GET, "/api/test".to_string());
281
282        assert!(registry.add_route(route).is_ok());
283        assert_eq!(registry.get_http_routes(&HttpMethod::GET).len(), 1);
284    }
285
286    #[test]
287    fn test_clear() {
288        let mut registry = RouteRegistry::new();
289
290        registry
291            .add_http_route(Route::new(HttpMethod::GET, "/api/users".to_string()))
292            .unwrap();
293        registry
294            .add_ws_route(Route::new(HttpMethod::GET, "/ws/chat".to_string()))
295            .unwrap();
296        registry
297            .add_grpc_route(
298                "Service".to_string(),
299                Route::new(HttpMethod::POST, "Method".to_string()),
300            )
301            .unwrap();
302
303        assert!(!registry.get_http_routes(&HttpMethod::GET).is_empty());
304        assert!(!registry.get_ws_routes().is_empty());
305
306        registry.clear();
307
308        assert!(registry.get_http_routes(&HttpMethod::GET).is_empty());
309        assert!(registry.get_ws_routes().is_empty());
310        assert!(registry.get_grpc_routes("Service").is_empty());
311    }
312
313    #[test]
314    fn test_find_http_routes_exact_match() {
315        let mut registry = RouteRegistry::new();
316        registry
317            .add_http_route(Route::new(HttpMethod::GET, "/api/users".to_string()))
318            .unwrap();
319
320        let found = registry.find_http_routes(&HttpMethod::GET, "/api/users");
321        assert_eq!(found.len(), 1);
322        assert_eq!(found[0].path, "/api/users");
323    }
324
325    #[test]
326    fn test_find_http_routes_no_match() {
327        let mut registry = RouteRegistry::new();
328        registry
329            .add_http_route(Route::new(HttpMethod::GET, "/api/users".to_string()))
330            .unwrap();
331
332        let found = registry.find_http_routes(&HttpMethod::GET, "/api/posts");
333        assert_eq!(found.len(), 0);
334    }
335
336    #[test]
337    fn test_find_http_routes_wildcard_match() {
338        let mut registry = RouteRegistry::new();
339        registry
340            .add_http_route(Route::new(HttpMethod::GET, "/api/*/details".to_string()))
341            .unwrap();
342
343        let found = registry.find_http_routes(&HttpMethod::GET, "/api/users/details");
344        assert_eq!(found.len(), 1);
345
346        let found = registry.find_http_routes(&HttpMethod::GET, "/api/posts/details");
347        assert_eq!(found.len(), 1);
348    }
349
350    #[test]
351    fn test_find_http_routes_wildcard_no_match_different_length() {
352        let mut registry = RouteRegistry::new();
353        registry
354            .add_http_route(Route::new(HttpMethod::GET, "/api/*/details".to_string()))
355            .unwrap();
356
357        let found = registry.find_http_routes(&HttpMethod::GET, "/api/users");
358        assert_eq!(found.len(), 0);
359    }
360
361    #[test]
362    fn test_find_ws_routes() {
363        let mut registry = RouteRegistry::new();
364        registry
365            .add_ws_route(Route::new(HttpMethod::GET, "/ws/chat".to_string()))
366            .unwrap();
367
368        let found = registry.find_ws_routes("/ws/chat");
369        assert_eq!(found.len(), 1);
370    }
371
372    #[test]
373    fn test_find_ws_routes_wildcard() {
374        let mut registry = RouteRegistry::new();
375        registry.add_ws_route(Route::new(HttpMethod::GET, "/ws/*".to_string())).unwrap();
376
377        let found = registry.find_ws_routes("/ws/chat");
378        assert_eq!(found.len(), 1);
379
380        let found = registry.find_ws_routes("/ws/notifications");
381        assert_eq!(found.len(), 1);
382    }
383
384    #[test]
385    fn test_find_grpc_routes() {
386        let mut registry = RouteRegistry::new();
387        registry
388            .add_grpc_route(
389                "UserService".to_string(),
390                Route::new(HttpMethod::POST, "GetUser".to_string()),
391            )
392            .unwrap();
393
394        let found = registry.find_grpc_routes("UserService", "GetUser");
395        assert_eq!(found.len(), 1);
396    }
397
398    #[test]
399    fn test_find_grpc_routes_wildcard() {
400        let mut registry = RouteRegistry::new();
401        // Wildcard pattern matching requires exact segment count
402        // For gRPC method names, we'd typically use exact matches
403        registry
404            .add_grpc_route(
405                "UserService".to_string(),
406                Route::new(HttpMethod::POST, "GetUser".to_string()),
407            )
408            .unwrap();
409
410        let found = registry.find_grpc_routes("UserService", "GetUser");
411        assert_eq!(found.len(), 1);
412    }
413
414    #[test]
415    fn test_matches_path_exact() {
416        let registry = RouteRegistry::new();
417        assert!(registry.matches_path("/api/users", "/api/users"));
418        assert!(!registry.matches_path("/api/users", "/api/posts"));
419    }
420
421    #[test]
422    fn test_matches_path_wildcard_single_segment() {
423        let registry = RouteRegistry::new();
424        assert!(registry.matches_path("/api/*", "/api/users"));
425        assert!(registry.matches_path("/api/*", "/api/posts"));
426        assert!(!registry.matches_path("/api/*", "/api"));
427        assert!(!registry.matches_path("/api/*", "/api/users/123"));
428    }
429
430    #[test]
431    fn test_matches_path_wildcard_multiple_segments() {
432        let registry = RouteRegistry::new();
433        assert!(registry.matches_path("/api/*/details", "/api/users/details"));
434        assert!(registry.matches_path("/api/*/*", "/api/users/123"));
435        assert!(!registry.matches_path("/api/*/*", "/api/users"));
436    }
437
438    #[test]
439    fn test_get_http_routes_empty() {
440        let registry = RouteRegistry::new();
441        assert!(registry.get_http_routes(&HttpMethod::GET).is_empty());
442    }
443
444    #[test]
445    fn test_get_ws_routes_empty() {
446        let registry = RouteRegistry::new();
447        assert!(registry.get_ws_routes().is_empty());
448    }
449
450    #[test]
451    fn test_get_grpc_routes_empty() {
452        let registry = RouteRegistry::new();
453        assert!(registry.get_grpc_routes("Service").is_empty());
454    }
455
456    #[test]
457    fn test_http_method_serialization() {
458        let method = HttpMethod::GET;
459        let json = serde_json::to_string(&method).unwrap();
460        assert_eq!(json, r#""get""#);
461
462        let method = HttpMethod::POST;
463        let json = serde_json::to_string(&method).unwrap();
464        assert_eq!(json, r#""post""#);
465    }
466
467    #[test]
468    fn test_http_method_deserialization() {
469        let method: HttpMethod = serde_json::from_str(r#""get""#).unwrap();
470        assert_eq!(method, HttpMethod::GET);
471
472        let method: HttpMethod = serde_json::from_str(r#""post""#).unwrap();
473        assert_eq!(method, HttpMethod::POST);
474    }
475}