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