mockforge_http/management/
proxy.rs1use 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#[derive(Debug, Deserialize, Serialize)]
13pub struct ProxyRuleRequest {
14 pub pattern: String,
16 #[serde(rename = "type")]
18 pub rule_type: String,
19 #[serde(default)]
21 pub status_codes: Vec<u16>,
22 pub body_transforms: Vec<BodyTransformRequest>,
24 #[serde(default = "default_true")]
26 pub enabled: bool,
27}
28
29#[derive(Debug, Deserialize, Serialize)]
31pub struct BodyTransformRequest {
32 pub path: String,
34 pub replace: String,
36 #[serde(default)]
38 pub operation: String,
39}
40
41#[derive(Debug, Serialize)]
43pub struct ProxyRuleResponse {
44 pub id: usize,
46 pub pattern: String,
48 #[serde(rename = "type")]
50 pub rule_type: String,
51 pub status_codes: Vec<u16>,
53 pub body_transforms: Vec<BodyTransformRequest>,
55 pub enabled: bool,
57}
58
59pub(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 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 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
122pub(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 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
188pub(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 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 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
252pub(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 config.request_replacements[rule_id] = updated_rule;
307 } else if rule_id < request_count + config.response_replacements.len() {
308 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
323pub(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 config.request_replacements.remove(rule_id);
352 } else if rule_id < request_count + config.response_replacements.len() {
353 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
368pub(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}