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 && 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 let has_conditional_rules = self.rules.iter().any(|r| r.enabled && r.condition.is_some());
234 if !has_conditional_rules {
235 match &self.prefix {
236 None => true, Some(prefix) => path.starts_with(prefix),
238 }
239 } else {
240 false }
242 }
243
244 pub fn should_shadow(&self, path: &str) -> bool {
246 if !self.migration_enabled {
247 return false;
248 }
249
250 if let Some(mode) = self.get_effective_migration_mode(path) {
251 return mode == MigrationMode::Shadow;
252 }
253
254 false
255 }
256
257 pub fn get_upstream_url(&self, path: &str) -> String {
259 for rule in &self.rules {
261 if rule.enabled && self.path_matches_pattern(&rule.path_pattern, path) {
262 return rule.target_url.clone();
263 }
264 }
265
266 if let Some(base_url) = &self.target_url {
268 base_url.clone()
269 } else {
270 path.to_string()
271 }
272 }
273
274 pub fn strip_prefix(&self, path: &str) -> String {
276 match &self.prefix {
277 Some(prefix) => {
278 if path.starts_with(prefix) {
279 let stripped = path.strip_prefix(prefix).unwrap_or(path);
280 if stripped.starts_with('/') {
282 stripped.to_string()
283 } else {
284 format!("/{}", stripped)
285 }
286 } else {
287 path.to_string()
288 }
289 }
290 None => path.to_string(), }
292 }
293
294 fn path_matches_pattern(&self, pattern: &str, path: &str) -> bool {
296 if let Some(prefix) = pattern.strip_suffix("/*") {
297 path.starts_with(prefix)
298 } else {
299 path == pattern
300 }
301 }
302
303 pub fn update_rule_migration_mode(&mut self, pattern: &str, mode: MigrationMode) -> bool {
306 for rule in &mut self.rules {
307 if rule.path_pattern == pattern || rule.pattern == pattern {
308 rule.migration_mode = mode;
309 return true;
310 }
311 }
312 false
313 }
314
315 pub fn update_group_migration_mode(&mut self, group: &str, mode: MigrationMode) {
318 self.migration_groups.insert(group.to_string(), mode);
319 }
320
321 pub fn toggle_route_migration(&mut self, pattern: &str) -> Option<MigrationMode> {
324 for rule in &mut self.rules {
325 if rule.path_pattern == pattern || rule.pattern == pattern {
326 rule.migration_mode = match rule.migration_mode {
327 MigrationMode::Mock => MigrationMode::Shadow,
328 MigrationMode::Shadow => MigrationMode::Real,
329 MigrationMode::Real => MigrationMode::Mock,
330 MigrationMode::Auto => MigrationMode::Mock, };
332 return Some(rule.migration_mode);
333 }
334 }
335 None
336 }
337
338 pub fn toggle_group_migration(&mut self, group: &str) -> MigrationMode {
341 let current_mode = self.migration_groups.get(group).copied().unwrap_or(MigrationMode::Auto);
342 let new_mode = match current_mode {
343 MigrationMode::Mock => MigrationMode::Shadow,
344 MigrationMode::Shadow => MigrationMode::Real,
345 MigrationMode::Real => MigrationMode::Mock,
346 MigrationMode::Auto => MigrationMode::Mock, };
348 self.migration_groups.insert(group.to_string(), new_mode);
349 new_mode
350 }
351
352 pub fn get_migration_routes(&self) -> Vec<MigrationRouteInfo> {
354 self.rules
355 .iter()
356 .map(|rule| {
357 let effective_mode = if let Some(ref group) = rule.migration_group {
358 self.migration_groups.get(group).copied().unwrap_or(rule.migration_mode)
359 } else {
360 rule.migration_mode
361 };
362
363 MigrationRouteInfo {
364 pattern: rule.path_pattern.clone(),
365 upstream_url: rule.target_url.clone(),
366 migration_mode: effective_mode,
367 route_mode: rule.migration_mode,
368 migration_group: rule.migration_group.clone(),
369 enabled: rule.enabled,
370 }
371 })
372 .collect()
373 }
374
375 pub fn get_migration_groups(&self) -> HashMap<String, MigrationGroupInfo> {
377 let mut group_info: HashMap<String, MigrationGroupInfo> = HashMap::new();
378
379 for rule in &self.rules {
381 if let Some(ref group) = rule.migration_group {
382 let entry = group_info.entry(group.clone()).or_insert_with(|| MigrationGroupInfo {
383 name: group.clone(),
384 migration_mode: self
385 .migration_groups
386 .get(group)
387 .copied()
388 .unwrap_or(rule.migration_mode),
389 route_count: 0,
390 });
391 entry.route_count += 1;
392 }
393 }
394
395 for (group_name, &mode) in &self.migration_groups {
397 group_info.entry(group_name.clone()).or_insert_with(|| MigrationGroupInfo {
398 name: group_name.clone(),
399 migration_mode: mode,
400 route_count: 0,
401 });
402 }
403
404 group_info
405 }
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct MigrationRouteInfo {
411 pub pattern: String,
413 pub upstream_url: String,
415 pub migration_mode: MigrationMode,
417 pub route_mode: MigrationMode,
419 pub migration_group: Option<String>,
421 pub enabled: bool,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct MigrationGroupInfo {
428 pub name: String,
430 pub migration_mode: MigrationMode,
432 pub route_count: usize,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
439pub struct BodyTransformRule {
440 pub pattern: String,
442 #[serde(default)]
444 pub status_codes: Vec<u16>,
445 pub body_transforms: Vec<BodyTransform>,
447 #[serde(default = "default_true")]
449 pub enabled: bool,
450}
451
452fn default_true() -> bool {
453 true
454}
455
456impl BodyTransformRule {
457 pub fn matches_url(&self, url: &str) -> bool {
459 if !self.enabled {
460 return false;
461 }
462
463 if self.pattern.ends_with("/*") {
465 let prefix = &self.pattern[..self.pattern.len() - 2];
466 url.starts_with(prefix)
467 } else {
468 url == self.pattern || url.starts_with(&self.pattern)
469 }
470 }
471
472 pub fn matches_status_code(&self, status_code: u16) -> bool {
474 if self.status_codes.is_empty() {
475 true } else {
477 self.status_codes.contains(&status_code)
478 }
479 }
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
485pub struct BodyTransform {
486 pub path: String,
488 pub replace: String,
490 #[serde(default)]
492 pub operation: TransformOperation,
493}
494
495#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
497#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
498#[serde(rename_all = "lowercase")]
499pub enum TransformOperation {
500 Replace,
502 Add,
504 Remove,
506}
507
508impl Default for TransformOperation {
509 fn default() -> Self {
510 Self::Replace
511 }
512}
513
514impl Default for ProxyConfig {
515 fn default() -> Self {
516 Self {
517 enabled: false,
518 target_url: None,
519 timeout_seconds: 30,
520 follow_redirects: true,
521 headers: HashMap::new(),
522 prefix: None,
523 passthrough_by_default: false,
524 rules: Vec::new(),
525 migration_enabled: false,
526 migration_groups: HashMap::new(),
527 request_replacements: Vec::new(),
528 response_replacements: Vec::new(),
529 }
530 }
531}