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")]
10pub enum MigrationMode {
11 Mock,
13 Shadow,
15 Real,
17 Auto,
19}
20
21impl Default for MigrationMode {
22 fn default() -> Self {
23 Self::Auto
24 }
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30pub struct ProxyConfig {
31 pub enabled: bool,
33 pub target_url: Option<String>,
35 pub timeout_seconds: u64,
37 pub follow_redirects: bool,
39 pub headers: HashMap<String, String>,
41 pub prefix: Option<String>,
43 pub passthrough_by_default: bool,
45 pub rules: Vec<ProxyRule>,
47 #[serde(default)]
49 pub migration_enabled: bool,
50 #[serde(default)]
53 pub migration_groups: HashMap<String, MigrationMode>,
54 #[serde(default)]
56 pub request_replacements: Vec<BodyTransformRule>,
57 #[serde(default)]
59 pub response_replacements: Vec<BodyTransformRule>,
60}
61
62#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
64#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
65pub struct ProxyRule {
66 pub path_pattern: String,
68 pub target_url: String,
70 pub enabled: bool,
72 pub pattern: String,
74 pub upstream_url: String,
76 #[serde(default)]
78 pub migration_mode: MigrationMode,
79 #[serde(default)]
81 pub migration_group: Option<String>,
82 #[serde(default)]
90 pub condition: Option<String>,
91}
92
93impl Default for ProxyRule {
94 fn default() -> Self {
95 Self {
96 path_pattern: "/".to_string(),
97 target_url: "http://localhost:9080".to_string(),
98 enabled: true,
99 pattern: "/".to_string(),
100 upstream_url: "http://localhost:9080".to_string(),
101 migration_mode: MigrationMode::Auto,
102 migration_group: None,
103 condition: None,
104 }
105 }
106}
107
108impl ProxyConfig {
109 pub fn new(upstream_url: String) -> Self {
111 Self {
112 enabled: true,
113 target_url: Some(upstream_url),
114 timeout_seconds: 30,
115 follow_redirects: true,
116 headers: HashMap::new(),
117 prefix: Some("/proxy/".to_string()),
118 passthrough_by_default: true,
119 rules: Vec::new(),
120 migration_enabled: false,
121 migration_groups: HashMap::new(),
122 request_replacements: Vec::new(),
123 response_replacements: Vec::new(),
124 }
125 }
126
127 pub fn get_effective_migration_mode(&self, path: &str) -> Option<MigrationMode> {
130 if !self.migration_enabled {
131 return None;
132 }
133
134 for rule in &self.rules {
136 if rule.enabled && self.path_matches_pattern(&rule.path_pattern, path) {
137 if let Some(ref group) = rule.migration_group {
139 if let Some(&group_mode) = self.migration_groups.get(group) {
140 return Some(group_mode);
141 }
142 }
143 return Some(rule.migration_mode);
145 }
146 }
147
148 None
149 }
150
151 pub fn should_proxy(&self, _method: &axum::http::Method, path: &str) -> bool {
155 if !self.enabled {
156 return false;
157 }
158
159 if self.migration_enabled {
161 if let Some(mode) = self.get_effective_migration_mode(path) {
162 match mode {
163 MigrationMode::Mock => return false, MigrationMode::Shadow => return true, MigrationMode::Real => return true, MigrationMode::Auto => {
167 }
169 }
170 }
171 }
172
173 for rule in &self.rules {
175 if rule.enabled && self.path_matches_pattern(&rule.path_pattern, path) {
176 if rule.condition.is_none() {
179 return true;
180 }
181 }
182 }
183
184 match &self.prefix {
186 None => true, Some(prefix) => path.starts_with(prefix),
188 }
189 }
190
191 pub fn should_proxy_with_condition(
194 &self,
195 method: &axum::http::Method,
196 uri: &axum::http::Uri,
197 headers: &axum::http::HeaderMap,
198 body: Option<&[u8]>,
199 ) -> bool {
200 use crate::proxy::conditional::find_matching_rule;
201
202 if !self.enabled {
203 return false;
204 }
205
206 let path = uri.path();
207
208 if self.migration_enabled {
210 if let Some(mode) = self.get_effective_migration_mode(path) {
211 match mode {
212 MigrationMode::Mock => return false, MigrationMode::Shadow => return true, MigrationMode::Real => return true, MigrationMode::Auto => {
216 }
218 }
219 }
220 }
221
222 if !self.rules.is_empty() {
224 if find_matching_rule(&self.rules, method, uri, headers, body, |pattern, path| {
225 self.path_matches_pattern(pattern, path)
226 })
227 .is_some()
228 {
229 return true;
230 }
231 }
232
233 let has_conditional_rules = self.rules.iter().any(|r| r.enabled && r.condition.is_some());
235 if !has_conditional_rules {
236 match &self.prefix {
237 None => true, Some(prefix) => path.starts_with(prefix),
239 }
240 } else {
241 false }
243 }
244
245 pub fn should_shadow(&self, path: &str) -> bool {
247 if !self.migration_enabled {
248 return false;
249 }
250
251 if let Some(mode) = self.get_effective_migration_mode(path) {
252 return mode == MigrationMode::Shadow;
253 }
254
255 false
256 }
257
258 pub fn get_upstream_url(&self, path: &str) -> String {
260 for rule in &self.rules {
262 if rule.enabled && self.path_matches_pattern(&rule.path_pattern, path) {
263 return rule.target_url.clone();
264 }
265 }
266
267 if let Some(base_url) = &self.target_url {
269 base_url.clone()
270 } else {
271 path.to_string()
272 }
273 }
274
275 pub fn strip_prefix(&self, path: &str) -> String {
277 match &self.prefix {
278 Some(prefix) => {
279 if path.starts_with(prefix) {
280 let stripped = path.strip_prefix(prefix).unwrap_or(path);
281 if stripped.starts_with('/') {
283 stripped.to_string()
284 } else {
285 format!("/{}", stripped)
286 }
287 } else {
288 path.to_string()
289 }
290 }
291 None => path.to_string(), }
293 }
294
295 fn path_matches_pattern(&self, pattern: &str, path: &str) -> bool {
297 if let Some(prefix) = pattern.strip_suffix("/*") {
298 path.starts_with(prefix)
299 } else {
300 path == pattern
301 }
302 }
303
304 pub fn update_rule_migration_mode(&mut self, pattern: &str, mode: MigrationMode) -> bool {
307 for rule in &mut self.rules {
308 if rule.path_pattern == pattern || rule.pattern == pattern {
309 rule.migration_mode = mode;
310 return true;
311 }
312 }
313 false
314 }
315
316 pub fn update_group_migration_mode(&mut self, group: &str, mode: MigrationMode) {
319 self.migration_groups.insert(group.to_string(), mode);
320 }
321
322 pub fn toggle_route_migration(&mut self, pattern: &str) -> Option<MigrationMode> {
325 for rule in &mut self.rules {
326 if rule.path_pattern == pattern || rule.pattern == pattern {
327 rule.migration_mode = match rule.migration_mode {
328 MigrationMode::Mock => MigrationMode::Shadow,
329 MigrationMode::Shadow => MigrationMode::Real,
330 MigrationMode::Real => MigrationMode::Mock,
331 MigrationMode::Auto => MigrationMode::Mock, };
333 return Some(rule.migration_mode);
334 }
335 }
336 None
337 }
338
339 pub fn toggle_group_migration(&mut self, group: &str) -> MigrationMode {
342 let current_mode = self.migration_groups.get(group).copied().unwrap_or(MigrationMode::Auto);
343 let new_mode = match current_mode {
344 MigrationMode::Mock => MigrationMode::Shadow,
345 MigrationMode::Shadow => MigrationMode::Real,
346 MigrationMode::Real => MigrationMode::Mock,
347 MigrationMode::Auto => MigrationMode::Mock, };
349 self.migration_groups.insert(group.to_string(), new_mode);
350 new_mode
351 }
352
353 pub fn get_migration_routes(&self) -> Vec<MigrationRouteInfo> {
355 self.rules
356 .iter()
357 .map(|rule| {
358 let effective_mode = if let Some(ref group) = rule.migration_group {
359 self.migration_groups.get(group).copied().unwrap_or(rule.migration_mode)
360 } else {
361 rule.migration_mode
362 };
363
364 MigrationRouteInfo {
365 pattern: rule.path_pattern.clone(),
366 upstream_url: rule.target_url.clone(),
367 migration_mode: effective_mode,
368 route_mode: rule.migration_mode,
369 migration_group: rule.migration_group.clone(),
370 enabled: rule.enabled,
371 }
372 })
373 .collect()
374 }
375
376 pub fn get_migration_groups(&self) -> HashMap<String, MigrationGroupInfo> {
378 let mut group_info: HashMap<String, MigrationGroupInfo> = HashMap::new();
379
380 for rule in &self.rules {
382 if let Some(ref group) = rule.migration_group {
383 let entry = group_info.entry(group.clone()).or_insert_with(|| MigrationGroupInfo {
384 name: group.clone(),
385 migration_mode: self
386 .migration_groups
387 .get(group)
388 .copied()
389 .unwrap_or(rule.migration_mode),
390 route_count: 0,
391 });
392 entry.route_count += 1;
393 }
394 }
395
396 for (group_name, &mode) in &self.migration_groups {
398 group_info.entry(group_name.clone()).or_insert_with(|| MigrationGroupInfo {
399 name: group_name.clone(),
400 migration_mode: mode,
401 route_count: 0,
402 });
403 }
404
405 group_info
406 }
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct MigrationRouteInfo {
412 pub pattern: String,
414 pub upstream_url: String,
416 pub migration_mode: MigrationMode,
418 pub route_mode: MigrationMode,
420 pub migration_group: Option<String>,
422 pub enabled: bool,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct MigrationGroupInfo {
429 pub name: String,
431 pub migration_mode: MigrationMode,
433 pub route_count: usize,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
440pub struct BodyTransformRule {
441 pub pattern: String,
443 #[serde(default)]
445 pub status_codes: Vec<u16>,
446 pub body_transforms: Vec<BodyTransform>,
448 #[serde(default = "default_true")]
450 pub enabled: bool,
451}
452
453fn default_true() -> bool {
454 true
455}
456
457impl BodyTransformRule {
458 pub fn matches_url(&self, url: &str) -> bool {
460 if !self.enabled {
461 return false;
462 }
463
464 if self.pattern.ends_with("/*") {
466 let prefix = &self.pattern[..self.pattern.len() - 2];
467 url.starts_with(prefix)
468 } else {
469 url == self.pattern || url.starts_with(&self.pattern)
470 }
471 }
472
473 pub fn matches_status_code(&self, status_code: u16) -> bool {
475 if self.status_codes.is_empty() {
476 true } else {
478 self.status_codes.contains(&status_code)
479 }
480 }
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
485#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
486pub struct BodyTransform {
487 pub path: String,
489 pub replace: String,
491 #[serde(default)]
493 pub operation: TransformOperation,
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
498#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
499#[serde(rename_all = "lowercase")]
500pub enum TransformOperation {
501 Replace,
503 Add,
505 Remove,
507}
508
509impl Default for TransformOperation {
510 fn default() -> Self {
511 Self::Replace
512 }
513}
514
515impl Default for ProxyConfig {
516 fn default() -> Self {
517 Self {
518 enabled: false,
519 target_url: None,
520 timeout_seconds: 30,
521 follow_redirects: true,
522 headers: HashMap::new(),
523 prefix: None,
524 passthrough_by_default: false,
525 rules: Vec::new(),
526 migration_enabled: false,
527 migration_groups: HashMap::new(),
528 request_replacements: Vec::new(),
529 response_replacements: Vec::new(),
530 }
531 }
532}