mockforge_core/proxy/
routing.rs1use crate::{
4 routing::{HttpMethod, Route},
5 Result,
6};
7use std::collections::HashMap;
8
9pub struct ProxyRouter {
11 proxy_routes: HashMap<HttpMethod, Vec<Route>>,
13}
14
15impl ProxyRouter {
16 pub fn new() -> Self {
18 Self {
19 proxy_routes: HashMap::new(),
20 }
21 }
22
23 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 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 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 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 fn matches_path(&self, route_path: &str, request_path: &str) -> bool {
59 if route_path == request_path {
60 return true;
61 }
62
63 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 fn rewrite_path(&self, pattern: &str, path: &str) -> String {
85 if pattern == path {
86 return path.to_string();
87 }
88
89 if let Some(prefix) = pattern.strip_suffix("/*") {
91 if path.starts_with(prefix) && path.len() > prefix.len() {
93 let remaining = &path[prefix.len()..];
94 if remaining.starts_with('/') {
96 return remaining.to_string();
97 } else {
98 return format!("/{}", remaining);
99 }
100 }
101 }
102
103 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}