Skip to main content

mockforge_core/
routing.rs

1//! Route registry and routing logic for MockForge
2//!
3//! Uses [`matchit`] for O(path-length) route matching instead of linear scan.
4
5use crate::Result;
6use std::collections::HashMap;
7
8/// HTTP method enum representing standard HTTP request methods
9#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
10#[serde(rename_all = "lowercase")]
11pub enum HttpMethod {
12    /// GET method - retrieve data from server
13    GET,
14    /// POST method - submit data to server
15    POST,
16    /// PUT method - update/replace resource
17    PUT,
18    /// DELETE method - remove resource
19    DELETE,
20    /// PATCH method - partial resource update
21    PATCH,
22    /// HEAD method - retrieve headers only
23    HEAD,
24    /// OPTIONS method - get allowed methods/headers
25    OPTIONS,
26}
27
28/// Route definition
29#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
30pub struct Route {
31    /// HTTP method
32    pub method: HttpMethod,
33    /// Path pattern (supports wildcards)
34    pub path: String,
35    /// Route priority (higher = more specific)
36    pub priority: i32,
37    /// Additional metadata
38    pub metadata: HashMap<String, serde_json::Value>,
39}
40
41impl Route {
42    /// Create a new route
43    pub fn new(method: HttpMethod, path: String) -> Self {
44        Self {
45            method,
46            path,
47            priority: 0,
48            metadata: HashMap::new(),
49        }
50    }
51
52    /// Set route priority
53    pub fn with_priority(mut self, priority: i32) -> Self {
54        self.priority = priority;
55        self
56    }
57
58    /// Add metadata
59    pub fn with_metadata(mut self, key: String, value: serde_json::Value) -> Self {
60        self.metadata.insert(key, value);
61        self
62    }
63}
64
65/// Convert a route pattern with `*` wildcards to matchit's `:param` syntax.
66///
67/// Each `*` segment becomes `:__wild_N` where N is the segment index,
68/// ensuring unique parameter names within the same path.
69fn to_matchit_pattern(pattern: &str) -> String {
70    if !pattern.contains('*') {
71        return pattern.to_string();
72    }
73
74    pattern
75        .split('/')
76        .enumerate()
77        .map(|(i, seg)| {
78            if seg == "*" {
79                format!("{{w{i}}}")
80            } else {
81                seg.to_string()
82            }
83        })
84        .collect::<Vec<_>>()
85        .join("/")
86}
87
88/// Per-method route index backed by [`matchit::Router`].
89///
90/// Each path maps to a list of route indices (to handle overlapping
91/// patterns that matchit would reject, e.g. exact + wildcard on same path).
92#[derive(Clone)]
93struct MethodIndex {
94    /// Fast trie-based router: path → indices into `routes`
95    router: matchit::Router<Vec<usize>>,
96    /// All routes for this method (preserves insertion order)
97    routes: Vec<Route>,
98}
99
100impl std::fmt::Debug for MethodIndex {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        f.debug_struct("MethodIndex").field("routes", &self.routes).finish()
103    }
104}
105
106impl MethodIndex {
107    fn new() -> Self {
108        Self {
109            router: matchit::Router::new(),
110            routes: Vec::new(),
111        }
112    }
113
114    fn insert(&mut self, route: Route) {
115        let idx = self.routes.len();
116        let matchit_path = to_matchit_pattern(&route.path);
117        self.routes.push(route);
118
119        // Try to insert into the trie. If the pattern conflicts with an
120        // existing entry (e.g. duplicate path), append to its index list.
121        match self.router.insert(matchit_path.clone(), vec![idx]) {
122            Ok(()) => {}
123            Err(_) => {
124                // Pattern already registered — append index to existing entry
125                if let Ok(matched) = self.router.at_mut(&matchit_path) {
126                    matched.value.push(idx);
127                }
128            }
129        }
130    }
131
132    fn find(&self, path: &str) -> Vec<&Route> {
133        match self.router.at(path) {
134            Ok(matched) => matched.value.iter().map(|&i| &self.routes[i]).collect(),
135            Err(_) => Vec::new(),
136        }
137    }
138
139    fn all(&self) -> Vec<&Route> {
140        self.routes.iter().collect()
141    }
142}
143
144/// Route registry for managing routes across different protocols.
145///
146/// Uses [`matchit`] for O(path-length) HTTP route matching. WebSocket and
147/// gRPC routes fall back to linear scan (typically few routes).
148#[derive(Debug, Clone)]
149pub struct RouteRegistry {
150    /// HTTP routes indexed by method with trie-based matching
151    http_routes: HashMap<HttpMethod, MethodIndex>,
152    /// WebSocket routes
153    ws_routes: Vec<Route>,
154    /// gRPC service routes
155    grpc_routes: HashMap<String, Vec<Route>>,
156}
157
158impl RouteRegistry {
159    /// Create a new empty route registry
160    pub fn new() -> Self {
161        Self {
162            http_routes: HashMap::new(),
163            ws_routes: Vec::new(),
164            grpc_routes: HashMap::new(),
165        }
166    }
167
168    /// Add an HTTP route
169    pub fn add_http_route(&mut self, route: Route) -> Result<()> {
170        self.http_routes
171            .entry(route.method.clone())
172            .or_insert_with(MethodIndex::new)
173            .insert(route);
174        Ok(())
175    }
176
177    /// Add a WebSocket route
178    pub fn add_ws_route(&mut self, route: Route) -> Result<()> {
179        self.ws_routes.push(route);
180        Ok(())
181    }
182
183    /// Clear all routes
184    pub fn clear(&mut self) {
185        self.http_routes.clear();
186        self.ws_routes.clear();
187        self.grpc_routes.clear();
188    }
189
190    /// Add a generic route (alias for add_http_route)
191    pub fn add_route(&mut self, route: Route) -> Result<()> {
192        self.add_http_route(route)
193    }
194
195    /// Add a gRPC route
196    pub fn add_grpc_route(&mut self, service: String, route: Route) -> Result<()> {
197        self.grpc_routes.entry(service).or_default().push(route);
198        Ok(())
199    }
200
201    /// Find matching HTTP routes (O(path-length) via matchit trie)
202    pub fn find_http_routes(&self, method: &HttpMethod, path: &str) -> Vec<&Route> {
203        self.http_routes.get(method).map(|index| index.find(path)).unwrap_or_default()
204    }
205
206    /// Find matching WebSocket routes
207    pub fn find_ws_routes(&self, path: &str) -> Vec<&Route> {
208        self.ws_routes
209            .iter()
210            .filter(|route| self.matches_path(&route.path, path))
211            .collect()
212    }
213
214    /// Find matching gRPC routes
215    pub fn find_grpc_routes(&self, service: &str, method: &str) -> Vec<&Route> {
216        self.grpc_routes
217            .get(service)
218            .map(|routes| {
219                routes.iter().filter(|route| self.matches_path(&route.path, method)).collect()
220            })
221            .unwrap_or_default()
222    }
223
224    /// Check if a path matches a route pattern (used for WS/gRPC linear scan)
225    fn matches_path(&self, pattern: &str, path: &str) -> bool {
226        if pattern == path {
227            return true;
228        }
229
230        // Simple wildcard matching (* matches any segment)
231        if pattern.contains('*') {
232            let pattern_parts: Vec<&str> = pattern.split('/').collect();
233            let path_parts: Vec<&str> = path.split('/').collect();
234
235            if pattern_parts.len() != path_parts.len() {
236                return false;
237            }
238
239            for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
240                if *pattern_part != "*" && *pattern_part != *path_part {
241                    return false;
242                }
243            }
244            return true;
245        }
246
247        false
248    }
249
250    /// Get all HTTP routes for a method
251    pub fn get_http_routes(&self, method: &HttpMethod) -> Vec<&Route> {
252        self.http_routes.get(method).map(|index| index.all()).unwrap_or_default()
253    }
254
255    /// Get all WebSocket routes
256    pub fn get_ws_routes(&self) -> Vec<&Route> {
257        self.ws_routes.iter().collect()
258    }
259
260    /// Get all gRPC routes for a service
261    pub fn get_grpc_routes(&self, service: &str) -> Vec<&Route> {
262        self.grpc_routes
263            .get(service)
264            .map(|routes| routes.iter().collect())
265            .unwrap_or_default()
266    }
267}
268
269impl Default for RouteRegistry {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_route_new() {
281        let route = Route::new(HttpMethod::GET, "/api/users".to_string());
282        assert_eq!(route.method, HttpMethod::GET);
283        assert_eq!(route.path, "/api/users");
284        assert_eq!(route.priority, 0);
285        assert!(route.metadata.is_empty());
286    }
287
288    #[test]
289    fn test_route_with_priority() {
290        let route = Route::new(HttpMethod::POST, "/api/users".to_string()).with_priority(10);
291        assert_eq!(route.priority, 10);
292    }
293
294    #[test]
295    fn test_route_with_metadata() {
296        let route = Route::new(HttpMethod::GET, "/api/users".to_string())
297            .with_metadata("version".to_string(), serde_json::json!("v1"))
298            .with_metadata("auth".to_string(), serde_json::json!(true));
299
300        assert_eq!(route.metadata.get("version"), Some(&serde_json::json!("v1")));
301        assert_eq!(route.metadata.get("auth"), Some(&serde_json::json!(true)));
302    }
303
304    #[test]
305    fn test_route_registry_new() {
306        let registry = RouteRegistry::new();
307        assert!(registry.http_routes.is_empty());
308        assert!(registry.ws_routes.is_empty());
309        assert!(registry.grpc_routes.is_empty());
310    }
311
312    #[test]
313    fn test_route_registry_default() {
314        let registry = RouteRegistry::default();
315        assert!(registry.http_routes.is_empty());
316    }
317
318    #[test]
319    fn test_add_http_route() {
320        let mut registry = RouteRegistry::new();
321        let route = Route::new(HttpMethod::GET, "/api/users".to_string());
322
323        assert!(registry.add_http_route(route).is_ok());
324        assert_eq!(registry.get_http_routes(&HttpMethod::GET).len(), 1);
325    }
326
327    #[test]
328    fn test_add_multiple_http_routes() {
329        let mut registry = RouteRegistry::new();
330
331        registry
332            .add_http_route(Route::new(HttpMethod::GET, "/api/users".to_string()))
333            .unwrap();
334        registry
335            .add_http_route(Route::new(HttpMethod::GET, "/api/posts".to_string()))
336            .unwrap();
337        registry
338            .add_http_route(Route::new(HttpMethod::POST, "/api/users".to_string()))
339            .unwrap();
340
341        assert_eq!(registry.get_http_routes(&HttpMethod::GET).len(), 2);
342        assert_eq!(registry.get_http_routes(&HttpMethod::POST).len(), 1);
343    }
344
345    #[test]
346    fn test_add_ws_route() {
347        let mut registry = RouteRegistry::new();
348        let route = Route::new(HttpMethod::GET, "/ws/chat".to_string());
349
350        assert!(registry.add_ws_route(route).is_ok());
351        assert_eq!(registry.get_ws_routes().len(), 1);
352    }
353
354    #[test]
355    fn test_add_grpc_route() {
356        let mut registry = RouteRegistry::new();
357        let route = Route::new(HttpMethod::POST, "GetUser".to_string());
358
359        assert!(registry.add_grpc_route("UserService".to_string(), route).is_ok());
360        assert_eq!(registry.get_grpc_routes("UserService").len(), 1);
361    }
362
363    #[test]
364    fn test_add_route_alias() {
365        let mut registry = RouteRegistry::new();
366        let route = Route::new(HttpMethod::GET, "/api/test".to_string());
367
368        assert!(registry.add_route(route).is_ok());
369        assert_eq!(registry.get_http_routes(&HttpMethod::GET).len(), 1);
370    }
371
372    #[test]
373    fn test_clear() {
374        let mut registry = RouteRegistry::new();
375
376        registry
377            .add_http_route(Route::new(HttpMethod::GET, "/api/users".to_string()))
378            .unwrap();
379        registry
380            .add_ws_route(Route::new(HttpMethod::GET, "/ws/chat".to_string()))
381            .unwrap();
382        registry
383            .add_grpc_route(
384                "Service".to_string(),
385                Route::new(HttpMethod::POST, "Method".to_string()),
386            )
387            .unwrap();
388
389        assert!(!registry.get_http_routes(&HttpMethod::GET).is_empty());
390        assert!(!registry.get_ws_routes().is_empty());
391
392        registry.clear();
393
394        assert!(registry.get_http_routes(&HttpMethod::GET).is_empty());
395        assert!(registry.get_ws_routes().is_empty());
396        assert!(registry.get_grpc_routes("Service").is_empty());
397    }
398
399    #[test]
400    fn test_find_http_routes_exact_match() {
401        let mut registry = RouteRegistry::new();
402        registry
403            .add_http_route(Route::new(HttpMethod::GET, "/api/users".to_string()))
404            .unwrap();
405
406        let found = registry.find_http_routes(&HttpMethod::GET, "/api/users");
407        assert_eq!(found.len(), 1);
408        assert_eq!(found[0].path, "/api/users");
409    }
410
411    #[test]
412    fn test_find_http_routes_no_match() {
413        let mut registry = RouteRegistry::new();
414        registry
415            .add_http_route(Route::new(HttpMethod::GET, "/api/users".to_string()))
416            .unwrap();
417
418        let found = registry.find_http_routes(&HttpMethod::GET, "/api/posts");
419        assert_eq!(found.len(), 0);
420    }
421
422    #[test]
423    fn test_find_http_routes_wildcard_match() {
424        let mut registry = RouteRegistry::new();
425        registry
426            .add_http_route(Route::new(HttpMethod::GET, "/api/*/details".to_string()))
427            .unwrap();
428
429        let found = registry.find_http_routes(&HttpMethod::GET, "/api/users/details");
430        assert_eq!(found.len(), 1);
431
432        let found = registry.find_http_routes(&HttpMethod::GET, "/api/posts/details");
433        assert_eq!(found.len(), 1);
434    }
435
436    #[test]
437    fn test_find_http_routes_wildcard_no_match_different_length() {
438        let mut registry = RouteRegistry::new();
439        registry
440            .add_http_route(Route::new(HttpMethod::GET, "/api/*/details".to_string()))
441            .unwrap();
442
443        let found = registry.find_http_routes(&HttpMethod::GET, "/api/users");
444        assert_eq!(found.len(), 0);
445    }
446
447    #[test]
448    fn test_find_ws_routes() {
449        let mut registry = RouteRegistry::new();
450        registry
451            .add_ws_route(Route::new(HttpMethod::GET, "/ws/chat".to_string()))
452            .unwrap();
453
454        let found = registry.find_ws_routes("/ws/chat");
455        assert_eq!(found.len(), 1);
456    }
457
458    #[test]
459    fn test_find_ws_routes_wildcard() {
460        let mut registry = RouteRegistry::new();
461        registry.add_ws_route(Route::new(HttpMethod::GET, "/ws/*".to_string())).unwrap();
462
463        let found = registry.find_ws_routes("/ws/chat");
464        assert_eq!(found.len(), 1);
465
466        let found = registry.find_ws_routes("/ws/notifications");
467        assert_eq!(found.len(), 1);
468    }
469
470    #[test]
471    fn test_find_grpc_routes() {
472        let mut registry = RouteRegistry::new();
473        registry
474            .add_grpc_route(
475                "UserService".to_string(),
476                Route::new(HttpMethod::POST, "GetUser".to_string()),
477            )
478            .unwrap();
479
480        let found = registry.find_grpc_routes("UserService", "GetUser");
481        assert_eq!(found.len(), 1);
482    }
483
484    #[test]
485    fn test_find_grpc_routes_wildcard() {
486        let mut registry = RouteRegistry::new();
487        registry
488            .add_grpc_route(
489                "UserService".to_string(),
490                Route::new(HttpMethod::POST, "GetUser".to_string()),
491            )
492            .unwrap();
493
494        let found = registry.find_grpc_routes("UserService", "GetUser");
495        assert_eq!(found.len(), 1);
496    }
497
498    #[test]
499    fn test_matches_path_exact() {
500        let registry = RouteRegistry::new();
501        assert!(registry.matches_path("/api/users", "/api/users"));
502        assert!(!registry.matches_path("/api/users", "/api/posts"));
503    }
504
505    #[test]
506    fn test_matches_path_wildcard_single_segment() {
507        let registry = RouteRegistry::new();
508        assert!(registry.matches_path("/api/*", "/api/users"));
509        assert!(registry.matches_path("/api/*", "/api/posts"));
510        assert!(!registry.matches_path("/api/*", "/api"));
511        assert!(!registry.matches_path("/api/*", "/api/users/123"));
512    }
513
514    #[test]
515    fn test_matches_path_wildcard_multiple_segments() {
516        let registry = RouteRegistry::new();
517        assert!(registry.matches_path("/api/*/details", "/api/users/details"));
518        assert!(registry.matches_path("/api/*/*", "/api/users/123"));
519        assert!(!registry.matches_path("/api/*/*", "/api/users"));
520    }
521
522    #[test]
523    fn test_get_http_routes_empty() {
524        let registry = RouteRegistry::new();
525        assert!(registry.get_http_routes(&HttpMethod::GET).is_empty());
526    }
527
528    #[test]
529    fn test_get_ws_routes_empty() {
530        let registry = RouteRegistry::new();
531        assert!(registry.get_ws_routes().is_empty());
532    }
533
534    #[test]
535    fn test_get_grpc_routes_empty() {
536        let registry = RouteRegistry::new();
537        assert!(registry.get_grpc_routes("Service").is_empty());
538    }
539
540    #[test]
541    fn test_http_method_serialization() {
542        let method = HttpMethod::GET;
543        let json = serde_json::to_string(&method).unwrap();
544        assert_eq!(json, r#""get""#);
545
546        let method = HttpMethod::POST;
547        let json = serde_json::to_string(&method).unwrap();
548        assert_eq!(json, r#""post""#);
549    }
550
551    #[test]
552    fn test_http_method_deserialization() {
553        let method: HttpMethod = serde_json::from_str(r#""get""#).unwrap();
554        assert_eq!(method, HttpMethod::GET);
555
556        let method: HttpMethod = serde_json::from_str(r#""post""#).unwrap();
557        assert_eq!(method, HttpMethod::POST);
558    }
559
560    #[test]
561    fn test_to_matchit_pattern() {
562        assert_eq!(to_matchit_pattern("/api/users"), "/api/users");
563        assert_eq!(to_matchit_pattern("/api/*/details"), "/api/{w2}/details");
564        assert_eq!(to_matchit_pattern("/api/*/*"), "/api/{w2}/{w3}");
565        assert_eq!(to_matchit_pattern("/*"), "/{w1}");
566    }
567
568    #[test]
569    fn test_matchit_many_routes_performance() {
570        let mut registry = RouteRegistry::new();
571
572        // Add 200 distinct routes
573        for i in 0..200 {
574            registry
575                .add_http_route(Route::new(HttpMethod::GET, format!("/api/v1/resource{i}")))
576                .unwrap();
577        }
578
579        // Matching the last route should still be fast (trie, not linear)
580        let found = registry.find_http_routes(&HttpMethod::GET, "/api/v1/resource199");
581        assert_eq!(found.len(), 1);
582        assert_eq!(found[0].path, "/api/v1/resource199");
583
584        // Non-existent route
585        let found = registry.find_http_routes(&HttpMethod::GET, "/api/v1/resource999");
586        assert_eq!(found.len(), 0);
587    }
588}