Skip to main content

mockforge_http/management/
proxy.rs

1use axum::{
2    extract::{Path, Query, State},
3    http::StatusCode,
4    response::Json,
5};
6use mockforge_core::proxy::config::{BodyTransform, BodyTransformRule, TransformOperation};
7use serde::{Deserialize, Serialize};
8
9use super::{default_true, ManagementState};
10
11/// Request body for creating/updating proxy replacement rules
12#[derive(Debug, Deserialize, Serialize)]
13pub struct ProxyRuleRequest {
14    /// URL pattern to match (supports wildcards like "/api/users/*")
15    pub pattern: String,
16    /// Rule type: "request" or "response"
17    #[serde(rename = "type")]
18    pub rule_type: String,
19    /// Optional status code filter for response rules
20    #[serde(default)]
21    pub status_codes: Vec<u16>,
22    /// Body transformations to apply
23    pub body_transforms: Vec<BodyTransformRequest>,
24    /// Whether this rule is enabled
25    #[serde(default = "default_true")]
26    pub enabled: bool,
27}
28
29/// Request body for individual body transformations
30#[derive(Debug, Deserialize, Serialize)]
31pub struct BodyTransformRequest {
32    /// JSONPath expression to target (e.g., "$.userId", "$.email")
33    pub path: String,
34    /// Replacement value (supports template expansion like "{{uuid}}", "{{faker.email}}")
35    pub replace: String,
36    /// Operation to perform: "replace", "add", or "remove"
37    #[serde(default)]
38    pub operation: String,
39}
40
41/// Response format for proxy rules
42#[derive(Debug, Serialize)]
43pub struct ProxyRuleResponse {
44    /// Rule ID (index in the array)
45    pub id: usize,
46    /// URL pattern
47    pub pattern: String,
48    /// Rule type
49    #[serde(rename = "type")]
50    pub rule_type: String,
51    /// Status codes (for response rules)
52    pub status_codes: Vec<u16>,
53    /// Body transformations
54    pub body_transforms: Vec<BodyTransformRequest>,
55    /// Whether enabled
56    pub enabled: bool,
57}
58
59/// List all proxy replacement rules
60pub(crate) async fn list_proxy_rules(
61    State(state): State<ManagementState>,
62) -> Result<Json<serde_json::Value>, StatusCode> {
63    let proxy_config = match &state.proxy_config {
64        Some(config) => config,
65        None => {
66            return Ok(Json(serde_json::json!({
67                "error": "Proxy not configured. Proxy config not available."
68            })));
69        }
70    };
71
72    let config = proxy_config.read().await;
73
74    let mut rules: Vec<ProxyRuleResponse> = Vec::new();
75
76    // Add request replacement rules
77    for (idx, rule) in config.request_replacements.iter().enumerate() {
78        rules.push(ProxyRuleResponse {
79            id: idx,
80            pattern: rule.pattern.clone(),
81            rule_type: "request".to_string(),
82            status_codes: Vec::new(),
83            body_transforms: rule
84                .body_transforms
85                .iter()
86                .map(|t| BodyTransformRequest {
87                    path: t.path.clone(),
88                    replace: t.replace.clone(),
89                    operation: format!("{:?}", t.operation).to_lowercase(),
90                })
91                .collect(),
92            enabled: rule.enabled,
93        });
94    }
95
96    // Add response replacement rules
97    let request_count = config.request_replacements.len();
98    for (idx, rule) in config.response_replacements.iter().enumerate() {
99        rules.push(ProxyRuleResponse {
100            id: request_count + idx,
101            pattern: rule.pattern.clone(),
102            rule_type: "response".to_string(),
103            status_codes: rule.status_codes.clone(),
104            body_transforms: rule
105                .body_transforms
106                .iter()
107                .map(|t| BodyTransformRequest {
108                    path: t.path.clone(),
109                    replace: t.replace.clone(),
110                    operation: format!("{:?}", t.operation).to_lowercase(),
111                })
112                .collect(),
113            enabled: rule.enabled,
114        });
115    }
116
117    Ok(Json(serde_json::json!({
118        "rules": rules
119    })))
120}
121
122/// Create a new proxy replacement rule
123pub(crate) async fn create_proxy_rule(
124    State(state): State<ManagementState>,
125    Json(request): Json<ProxyRuleRequest>,
126) -> Result<Json<serde_json::Value>, StatusCode> {
127    let proxy_config = match &state.proxy_config {
128        Some(config) => config,
129        None => {
130            return Ok(Json(serde_json::json!({
131                "error": "Proxy not configured. Proxy config not available."
132            })));
133        }
134    };
135
136    // Validate request
137    if request.body_transforms.is_empty() {
138        return Ok(Json(serde_json::json!({
139            "error": "At least one body transform is required"
140        })));
141    }
142
143    let body_transforms: Vec<BodyTransform> = request
144        .body_transforms
145        .iter()
146        .map(|t| {
147            let op = match t.operation.as_str() {
148                "replace" => TransformOperation::Replace,
149                "add" => TransformOperation::Add,
150                "remove" => TransformOperation::Remove,
151                _ => TransformOperation::Replace,
152            };
153            BodyTransform {
154                path: t.path.clone(),
155                replace: t.replace.clone(),
156                operation: op,
157            }
158        })
159        .collect();
160
161    let new_rule = BodyTransformRule {
162        pattern: request.pattern.clone(),
163        status_codes: request.status_codes.clone(),
164        body_transforms,
165        enabled: request.enabled,
166    };
167
168    let mut config = proxy_config.write().await;
169
170    let rule_id = if request.rule_type == "request" {
171        config.request_replacements.push(new_rule);
172        config.request_replacements.len() - 1
173    } else if request.rule_type == "response" {
174        config.response_replacements.push(new_rule);
175        config.request_replacements.len() + config.response_replacements.len() - 1
176    } else {
177        return Ok(Json(serde_json::json!({
178            "error": format!("Invalid rule type: {}. Must be 'request' or 'response'", request.rule_type)
179        })));
180    };
181
182    Ok(Json(serde_json::json!({
183        "id": rule_id,
184        "message": "Rule created successfully"
185    })))
186}
187
188/// Get a specific proxy replacement rule
189pub(crate) async fn get_proxy_rule(
190    State(state): State<ManagementState>,
191    Path(id): Path<String>,
192) -> Result<Json<serde_json::Value>, StatusCode> {
193    let proxy_config = match &state.proxy_config {
194        Some(config) => config,
195        None => {
196            return Ok(Json(serde_json::json!({
197                "error": "Proxy not configured. Proxy config not available."
198            })));
199        }
200    };
201
202    let config = proxy_config.read().await;
203    let rule_id: usize = match id.parse() {
204        Ok(id) => id,
205        Err(_) => {
206            return Ok(Json(serde_json::json!({
207                "error": format!("Invalid rule ID: {}", id)
208            })));
209        }
210    };
211
212    let request_count = config.request_replacements.len();
213
214    if rule_id < request_count {
215        // Request rule
216        let rule = &config.request_replacements[rule_id];
217        Ok(Json(serde_json::json!({
218            "id": rule_id,
219            "pattern": rule.pattern,
220            "type": "request",
221            "status_codes": [],
222            "body_transforms": rule.body_transforms.iter().map(|t| serde_json::json!({
223                "path": t.path,
224                "replace": t.replace,
225                "operation": format!("{:?}", t.operation).to_lowercase()
226            })).collect::<Vec<_>>(),
227            "enabled": rule.enabled
228        })))
229    } else if rule_id < request_count + config.response_replacements.len() {
230        // Response rule
231        let response_idx = rule_id - request_count;
232        let rule = &config.response_replacements[response_idx];
233        Ok(Json(serde_json::json!({
234            "id": rule_id,
235            "pattern": rule.pattern,
236            "type": "response",
237            "status_codes": rule.status_codes,
238            "body_transforms": rule.body_transforms.iter().map(|t| serde_json::json!({
239                "path": t.path,
240                "replace": t.replace,
241                "operation": format!("{:?}", t.operation).to_lowercase()
242            })).collect::<Vec<_>>(),
243            "enabled": rule.enabled
244        })))
245    } else {
246        Ok(Json(serde_json::json!({
247            "error": format!("Rule ID {} not found", rule_id)
248        })))
249    }
250}
251
252/// Update a proxy replacement rule
253pub(crate) async fn update_proxy_rule(
254    State(state): State<ManagementState>,
255    Path(id): Path<String>,
256    Json(request): Json<ProxyRuleRequest>,
257) -> Result<Json<serde_json::Value>, StatusCode> {
258    let proxy_config = match &state.proxy_config {
259        Some(config) => config,
260        None => {
261            return Ok(Json(serde_json::json!({
262                "error": "Proxy not configured. Proxy config not available."
263            })));
264        }
265    };
266
267    let mut config = proxy_config.write().await;
268    let rule_id: usize = match id.parse() {
269        Ok(id) => id,
270        Err(_) => {
271            return Ok(Json(serde_json::json!({
272                "error": format!("Invalid rule ID: {}", id)
273            })));
274        }
275    };
276
277    let body_transforms: Vec<BodyTransform> = request
278        .body_transforms
279        .iter()
280        .map(|t| {
281            let op = match t.operation.as_str() {
282                "replace" => TransformOperation::Replace,
283                "add" => TransformOperation::Add,
284                "remove" => TransformOperation::Remove,
285                _ => TransformOperation::Replace,
286            };
287            BodyTransform {
288                path: t.path.clone(),
289                replace: t.replace.clone(),
290                operation: op,
291            }
292        })
293        .collect();
294
295    let updated_rule = BodyTransformRule {
296        pattern: request.pattern.clone(),
297        status_codes: request.status_codes.clone(),
298        body_transforms,
299        enabled: request.enabled,
300    };
301
302    let request_count = config.request_replacements.len();
303
304    if rule_id < request_count {
305        // Update request rule
306        config.request_replacements[rule_id] = updated_rule;
307    } else if rule_id < request_count + config.response_replacements.len() {
308        // Update response rule
309        let response_idx = rule_id - request_count;
310        config.response_replacements[response_idx] = updated_rule;
311    } else {
312        return Ok(Json(serde_json::json!({
313            "error": format!("Rule ID {} not found", rule_id)
314        })));
315    }
316
317    Ok(Json(serde_json::json!({
318        "id": rule_id,
319        "message": "Rule updated successfully"
320    })))
321}
322
323/// Delete a proxy replacement rule
324pub(crate) async fn delete_proxy_rule(
325    State(state): State<ManagementState>,
326    Path(id): Path<String>,
327) -> Result<Json<serde_json::Value>, StatusCode> {
328    let proxy_config = match &state.proxy_config {
329        Some(config) => config,
330        None => {
331            return Ok(Json(serde_json::json!({
332                "error": "Proxy not configured. Proxy config not available."
333            })));
334        }
335    };
336
337    let mut config = proxy_config.write().await;
338    let rule_id: usize = match id.parse() {
339        Ok(id) => id,
340        Err(_) => {
341            return Ok(Json(serde_json::json!({
342                "error": format!("Invalid rule ID: {}", id)
343            })));
344        }
345    };
346
347    let request_count = config.request_replacements.len();
348
349    if rule_id < request_count {
350        // Delete request rule
351        config.request_replacements.remove(rule_id);
352    } else if rule_id < request_count + config.response_replacements.len() {
353        // Delete response rule
354        let response_idx = rule_id - request_count;
355        config.response_replacements.remove(response_idx);
356    } else {
357        return Ok(Json(serde_json::json!({
358            "error": format!("Rule ID {} not found", rule_id)
359        })));
360    }
361
362    Ok(Json(serde_json::json!({
363        "id": rule_id,
364        "message": "Rule deleted successfully"
365    })))
366}
367
368/// Get proxy rules and transformation configuration for inspection
369pub(crate) async fn get_proxy_inspect(
370    State(state): State<ManagementState>,
371    Query(params): Query<std::collections::HashMap<String, String>>,
372) -> Result<Json<serde_json::Value>, StatusCode> {
373    let limit: usize = params.get("limit").and_then(|s| s.parse().ok()).unwrap_or(50);
374    let offset: usize = params.get("offset").and_then(|s| s.parse().ok()).unwrap_or(0);
375
376    let proxy_config = match &state.proxy_config {
377        Some(config) => config.read().await,
378        None => {
379            return Ok(Json(serde_json::json!({
380                "error": "Proxy not configured. Proxy config not available."
381            })));
382        }
383    };
384
385    let mut rules = Vec::new();
386    for (idx, rule) in proxy_config.request_replacements.iter().enumerate() {
387        rules.push(serde_json::json!({
388            "id": idx,
389            "kind": "request",
390            "pattern": rule.pattern,
391            "enabled": rule.enabled,
392            "status_codes": rule.status_codes,
393            "transform_count": rule.body_transforms.len(),
394            "transforms": rule.body_transforms.iter().map(|t| serde_json::json!({
395                "path": t.path,
396                "operation": t.operation,
397                "replace": t.replace
398            })).collect::<Vec<_>>()
399        }));
400    }
401    let request_rule_count = rules.len();
402    for (idx, rule) in proxy_config.response_replacements.iter().enumerate() {
403        rules.push(serde_json::json!({
404            "id": request_rule_count + idx,
405            "kind": "response",
406            "pattern": rule.pattern,
407            "enabled": rule.enabled,
408            "status_codes": rule.status_codes,
409            "transform_count": rule.body_transforms.len(),
410            "transforms": rule.body_transforms.iter().map(|t| serde_json::json!({
411                "path": t.path,
412                "operation": t.operation,
413                "replace": t.replace
414            })).collect::<Vec<_>>()
415        }));
416    }
417
418    let total = rules.len();
419    let paged_rules: Vec<_> = rules.into_iter().skip(offset).take(limit).collect();
420
421    Ok(Json(serde_json::json!({
422        "enabled": proxy_config.enabled,
423        "target_url": proxy_config.target_url,
424        "prefix": proxy_config.prefix,
425        "timeout_seconds": proxy_config.timeout_seconds,
426        "follow_redirects": proxy_config.follow_redirects,
427        "passthrough_by_default": proxy_config.passthrough_by_default,
428        "rules": paged_rules,
429        "request_rule_count": request_rule_count,
430        "response_rule_count": total.saturating_sub(request_rule_count),
431        "limit": limit,
432        "offset": offset,
433        "total": total
434    })))
435}