1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
9#[serde(rename_all = "lowercase")]
10#[derive(Default)]
11pub enum MigrationMode {
12 Mock,
14 Shadow,
16 Real,
18 #[default]
20 Auto,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26pub struct ProxyConfig {
27 pub enabled: bool,
29 pub target_url: Option<String>,
31 pub timeout_seconds: u64,
33 pub follow_redirects: bool,
35 pub headers: HashMap<String, String>,
37 pub prefix: Option<String>,
39 pub passthrough_by_default: bool,
41 pub rules: Vec<ProxyRule>,
43 #[serde(default)]
45 pub migration_enabled: bool,
46 #[serde(default)]
49 pub migration_groups: HashMap<String, MigrationMode>,
50 #[serde(default)]
52 pub request_replacements: Vec<BodyTransformRule>,
53 #[serde(default)]
55 pub response_replacements: Vec<BodyTransformRule>,
56}
57
58#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
60#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
61pub struct ProxyRule {
62 pub path_pattern: String,
64 pub target_url: String,
66 pub enabled: bool,
68 pub pattern: String,
70 pub upstream_url: String,
72 #[serde(default)]
74 pub migration_mode: MigrationMode,
75 #[serde(default)]
77 pub migration_group: Option<String>,
78 #[serde(default)]
86 pub condition: Option<String>,
87}
88
89impl Default for ProxyRule {
90 fn default() -> Self {
91 Self {
92 path_pattern: "/".to_string(),
93 target_url: "http://localhost:9080".to_string(),
94 enabled: true,
95 pattern: "/".to_string(),
96 upstream_url: "http://localhost:9080".to_string(),
97 migration_mode: MigrationMode::Auto,
98 migration_group: None,
99 condition: None,
100 }
101 }
102}
103
104impl ProxyConfig {
105 pub fn new(upstream_url: String) -> Self {
107 Self {
108 enabled: true,
109 target_url: Some(upstream_url),
110 timeout_seconds: 30,
111 follow_redirects: true,
112 headers: HashMap::new(),
113 prefix: Some("/proxy/".to_string()),
114 passthrough_by_default: true,
115 rules: Vec::new(),
116 migration_enabled: false,
117 migration_groups: HashMap::new(),
118 request_replacements: Vec::new(),
119 response_replacements: Vec::new(),
120 }
121 }
122
123 pub fn get_effective_migration_mode(&self, path: &str) -> Option<MigrationMode> {
126 if !self.migration_enabled {
127 return None;
128 }
129
130 for rule in &self.rules {
132 if rule.enabled && self.path_matches_pattern(&rule.path_pattern, path) {
133 if let Some(ref group) = rule.migration_group {
135 if let Some(&group_mode) = self.migration_groups.get(group) {
136 return Some(group_mode);
137 }
138 }
139 return Some(rule.migration_mode);
141 }
142 }
143
144 None
145 }
146
147 pub fn should_proxy(&self, _method: &axum::http::Method, path: &str) -> bool {
151 if !self.enabled {
152 return false;
153 }
154
155 if self.migration_enabled {
157 if let Some(mode) = self.get_effective_migration_mode(path) {
158 match mode {
159 MigrationMode::Mock => return false, MigrationMode::Shadow => return true, MigrationMode::Real => return true, MigrationMode::Auto => {
163 }
165 }
166 }
167 }
168
169 for rule in &self.rules {
171 if rule.enabled && self.path_matches_pattern(&rule.path_pattern, path) {
172 if rule.condition.is_none() {
175 return true;
176 }
177 }
178 }
179
180 match &self.prefix {
182 None => true, Some(prefix) => path.starts_with(prefix),
184 }
185 }
186
187 pub fn should_proxy_with_condition(
190 &self,
191 method: &axum::http::Method,
192 uri: &axum::http::Uri,
193 headers: &axum::http::HeaderMap,
194 body: Option<&[u8]>,
195 ) -> bool {
196 use crate::proxy::conditional::find_matching_rule;
197
198 if !self.enabled {
199 return false;
200 }
201
202 let path = uri.path();
203
204 if self.migration_enabled {
206 if let Some(mode) = self.get_effective_migration_mode(path) {
207 match mode {
208 MigrationMode::Mock => return false, MigrationMode::Shadow => return true, MigrationMode::Real => return true, MigrationMode::Auto => {
212 }
214 }
215 }
216 }
217
218 if !self.rules.is_empty()
220 && find_matching_rule(&self.rules, method, uri, headers, body, |pattern, path| {
221 self.path_matches_pattern(pattern, path)
222 })
223 .is_some()
224 {
225 return true;
226 }
227
228 let has_conditional_rules = self.rules.iter().any(|r| r.enabled && r.condition.is_some());
230 if !has_conditional_rules {
231 match &self.prefix {
232 None => true, Some(prefix) => path.starts_with(prefix),
234 }
235 } else {
236 false }
238 }
239
240 pub fn should_shadow(&self, path: &str) -> bool {
242 if !self.migration_enabled {
243 return false;
244 }
245
246 if let Some(mode) = self.get_effective_migration_mode(path) {
247 return mode == MigrationMode::Shadow;
248 }
249
250 false
251 }
252
253 pub fn get_upstream_url(&self, path: &str) -> String {
255 for rule in &self.rules {
257 if rule.enabled && self.path_matches_pattern(&rule.path_pattern, path) {
258 return rule.target_url.clone();
259 }
260 }
261
262 if let Some(base_url) = &self.target_url {
264 base_url.clone()
265 } else {
266 path.to_string()
267 }
268 }
269
270 pub fn strip_prefix(&self, path: &str) -> String {
272 match &self.prefix {
273 Some(prefix) => {
274 if path.starts_with(prefix) {
275 let stripped = path.strip_prefix(prefix).unwrap_or(path);
276 if stripped.starts_with('/') {
278 stripped.to_string()
279 } else {
280 format!("/{}", stripped)
281 }
282 } else {
283 path.to_string()
284 }
285 }
286 None => path.to_string(), }
288 }
289
290 fn path_matches_pattern(&self, pattern: &str, path: &str) -> bool {
292 if let Some(prefix) = pattern.strip_suffix("/*") {
293 path.starts_with(prefix)
294 } else {
295 path == pattern
296 }
297 }
298
299 pub fn update_rule_migration_mode(&mut self, pattern: &str, mode: MigrationMode) -> bool {
302 for rule in &mut self.rules {
303 if rule.path_pattern == pattern || rule.pattern == pattern {
304 rule.migration_mode = mode;
305 return true;
306 }
307 }
308 false
309 }
310
311 pub fn update_group_migration_mode(&mut self, group: &str, mode: MigrationMode) {
314 self.migration_groups.insert(group.to_string(), mode);
315 }
316
317 pub fn toggle_route_migration(&mut self, pattern: &str) -> Option<MigrationMode> {
320 for rule in &mut self.rules {
321 if rule.path_pattern == pattern || rule.pattern == pattern {
322 rule.migration_mode = match rule.migration_mode {
323 MigrationMode::Mock => MigrationMode::Shadow,
324 MigrationMode::Shadow => MigrationMode::Real,
325 MigrationMode::Real => MigrationMode::Mock,
326 MigrationMode::Auto => MigrationMode::Mock, };
328 return Some(rule.migration_mode);
329 }
330 }
331 None
332 }
333
334 pub fn toggle_group_migration(&mut self, group: &str) -> MigrationMode {
337 let current_mode = self.migration_groups.get(group).copied().unwrap_or(MigrationMode::Auto);
338 let new_mode = match current_mode {
339 MigrationMode::Mock => MigrationMode::Shadow,
340 MigrationMode::Shadow => MigrationMode::Real,
341 MigrationMode::Real => MigrationMode::Mock,
342 MigrationMode::Auto => MigrationMode::Mock, };
344 self.migration_groups.insert(group.to_string(), new_mode);
345 new_mode
346 }
347
348 pub fn get_migration_routes(&self) -> Vec<MigrationRouteInfo> {
350 self.rules
351 .iter()
352 .map(|rule| {
353 let effective_mode = if let Some(ref group) = rule.migration_group {
354 self.migration_groups.get(group).copied().unwrap_or(rule.migration_mode)
355 } else {
356 rule.migration_mode
357 };
358
359 MigrationRouteInfo {
360 pattern: rule.path_pattern.clone(),
361 upstream_url: rule.target_url.clone(),
362 migration_mode: effective_mode,
363 route_mode: rule.migration_mode,
364 migration_group: rule.migration_group.clone(),
365 enabled: rule.enabled,
366 }
367 })
368 .collect()
369 }
370
371 pub fn get_migration_groups(&self) -> HashMap<String, MigrationGroupInfo> {
373 let mut group_info: HashMap<String, MigrationGroupInfo> = HashMap::new();
374
375 for rule in &self.rules {
377 if let Some(ref group) = rule.migration_group {
378 let entry = group_info.entry(group.clone()).or_insert_with(|| MigrationGroupInfo {
379 name: group.clone(),
380 migration_mode: self
381 .migration_groups
382 .get(group)
383 .copied()
384 .unwrap_or(rule.migration_mode),
385 route_count: 0,
386 });
387 entry.route_count += 1;
388 }
389 }
390
391 for (group_name, &mode) in &self.migration_groups {
393 group_info.entry(group_name.clone()).or_insert_with(|| MigrationGroupInfo {
394 name: group_name.clone(),
395 migration_mode: mode,
396 route_count: 0,
397 });
398 }
399
400 group_info
401 }
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct MigrationRouteInfo {
407 pub pattern: String,
409 pub upstream_url: String,
411 pub migration_mode: MigrationMode,
413 pub route_mode: MigrationMode,
415 pub migration_group: Option<String>,
417 pub enabled: bool,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct MigrationGroupInfo {
424 pub name: String,
426 pub migration_mode: MigrationMode,
428 pub route_count: usize,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
434#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
435pub struct BodyTransformRule {
436 pub pattern: String,
438 #[serde(default)]
440 pub status_codes: Vec<u16>,
441 pub body_transforms: Vec<BodyTransform>,
443 #[serde(default = "default_true")]
445 pub enabled: bool,
446}
447
448fn default_true() -> bool {
449 true
450}
451
452impl BodyTransformRule {
453 pub fn matches_url(&self, url: &str) -> bool {
455 if !self.enabled {
456 return false;
457 }
458
459 if self.pattern.ends_with("/*") {
461 let prefix = &self.pattern[..self.pattern.len() - 2];
462 url.starts_with(prefix)
463 } else {
464 url == self.pattern || url.starts_with(&self.pattern)
465 }
466 }
467
468 pub fn matches_status_code(&self, status_code: u16) -> bool {
470 if self.status_codes.is_empty() {
471 true } else {
473 self.status_codes.contains(&status_code)
474 }
475 }
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
481pub struct BodyTransform {
482 pub path: String,
484 pub replace: String,
486 #[serde(default)]
488 pub operation: TransformOperation,
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
493#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
494#[serde(rename_all = "lowercase")]
495#[derive(Default)]
496pub enum TransformOperation {
497 #[default]
499 Replace,
500 Add,
502 Remove,
504}
505
506impl Default for ProxyConfig {
507 fn default() -> Self {
508 Self {
509 enabled: false,
510 target_url: None,
511 timeout_seconds: 30,
512 follow_redirects: true,
513 headers: HashMap::new(),
514 prefix: None,
515 passthrough_by_default: false,
516 rules: Vec::new(),
517 migration_enabled: false,
518 migration_groups: HashMap::new(),
519 request_replacements: Vec::new(),
520 response_replacements: Vec::new(),
521 }
522 }
523}