mockforge_core/proxy/
routing.rs

1//! Proxy routing logic
2
3use crate::{
4    routing::{HttpMethod, Route},
5    Result,
6};
7use std::collections::HashMap;
8
9/// Proxy router for determining if a request should be proxied
10pub struct ProxyRouter {
11    /// Routes that should be proxied
12    proxy_routes: HashMap<HttpMethod, Vec<Route>>,
13}
14
15impl ProxyRouter {
16    /// Create a new proxy router
17    pub fn new() -> Self {
18        Self {
19            proxy_routes: HashMap::new(),
20        }
21    }
22
23    /// Add a route that should be proxied
24    pub fn add_proxy_route(&mut self, route: Route) -> Result<()> {
25        self.proxy_routes.entry(route.method.clone()).or_default().push(route);
26        Ok(())
27    }
28
29    /// Check if a request should be proxied
30    pub fn should_proxy(&self, method: &HttpMethod, path: &str) -> bool {
31        if let Some(routes) = self.proxy_routes.get(method) {
32            routes.iter().any(|route| self.matches_path(&route.path, path))
33        } else {
34            false
35        }
36    }
37
38    /// Get the target URL for a proxied request
39    pub fn get_target_url(
40        &self,
41        method: &HttpMethod,
42        path: &str,
43        base_url: &str,
44    ) -> Option<String> {
45        if let Some(routes) = self.proxy_routes.get(method) {
46            for route in routes {
47                if self.matches_path(&route.path, path) {
48                    // Perform URL rewriting based on the route pattern
49                    let target_path = self.rewrite_path(&route.path, path);
50                    return Some(format!("{}{}", base_url.trim_end_matches('/'), target_path));
51                }
52            }
53        }
54        None
55    }
56
57    /// Simple path matching with wildcard support (* matches any segment)
58    fn matches_path(&self, route_path: &str, request_path: &str) -> bool {
59        if route_path == request_path {
60            return true;
61        }
62
63        // Support wildcard matching (* matches any segment)
64        if route_path.contains('*') {
65            let pattern_parts: Vec<&str> = route_path.split('/').collect();
66            let path_parts: Vec<&str> = request_path.split('/').collect();
67
68            if pattern_parts.len() != path_parts.len() {
69                return false;
70            }
71
72            for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
73                if *pattern_part != "*" && *pattern_part != *path_part {
74                    return false;
75                }
76            }
77            return true;
78        }
79
80        false
81    }
82
83    /// Rewrite the request path based on the route pattern
84    fn rewrite_path(&self, pattern: &str, path: &str) -> String {
85        if pattern == path {
86            return path.to_string();
87        }
88
89        // For wildcard patterns like "/api/*", strip the prefix and keep the rest
90        if let Some(prefix) = pattern.strip_suffix("/*") {
91            // Remove "/*"
92            if path.starts_with(prefix) && path.len() > prefix.len() {
93                let remaining = &path[prefix.len()..];
94                // Ensure we don't have double slashes
95                if remaining.starts_with('/') {
96                    return remaining.to_string();
97                } else {
98                    return format!("/{}", remaining);
99                }
100            }
101        }
102
103        // For exact matches or other patterns, return the path as-is
104        path.to_string()
105    }
106}
107
108impl Default for ProxyRouter {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::routing::HttpMethod;
118
119    #[test]
120    fn test_matches_path_exact() {
121        let router = ProxyRouter::new();
122        assert!(router.matches_path("/api/users", "/api/users"));
123        assert!(!router.matches_path("/api/users", "/api/posts"));
124    }
125
126    #[test]
127    fn test_matches_path_wildcard() {
128        let router = ProxyRouter::new();
129        assert!(router.matches_path("/api/*", "/api/users"));
130        assert!(router.matches_path("/api/*", "/api/posts"));
131        assert!(!router.matches_path("/api/*", "/admin/users"));
132        assert!(!router.matches_path("/api/*", "/api/users/profile"));
133    }
134
135    #[test]
136    fn test_rewrite_path_exact() {
137        let router = ProxyRouter::new();
138        assert_eq!(router.rewrite_path("/api/users", "/api/users"), "/api/users");
139    }
140
141    #[test]
142    fn test_rewrite_path_wildcard() {
143        let router = ProxyRouter::new();
144        assert_eq!(router.rewrite_path("/api/*", "/api/users"), "/users");
145        assert_eq!(router.rewrite_path("/proxy/*", "/proxy/api/v1/users"), "/api/v1/users");
146        assert_eq!(router.rewrite_path("/v1/*", "/v1/api/users"), "/api/users");
147    }
148
149    #[test]
150    fn test_get_target_url() {
151        let mut router = ProxyRouter::new();
152        let route = crate::routing::Route::new(HttpMethod::GET, "/api/*".to_string());
153        router.add_proxy_route(route).unwrap();
154
155        let base_url = "http://backend:9080";
156        assert_eq!(
157            router.get_target_url(&HttpMethod::GET, "/api/users", base_url),
158            Some("http://backend:9080/users".to_string())
159        );
160        assert_eq!(
161            router.get_target_url(&HttpMethod::GET, "/api/posts", base_url),
162            Some("http://backend:9080/posts".to_string())
163        );
164        assert_eq!(router.get_target_url(&HttpMethod::GET, "/admin/users", base_url), None);
165    }
166}