mockforge_ui/handlers/
migration.rs

1//! Migration pipeline handlers
2//!
3//! Handlers for managing the mock-to-real migration pipeline,
4//! including route toggling, group management, and status reporting.
5//!
6//! These handlers proxy requests to the HTTP server's management API.
7
8use axum::{
9    extract::{Path, State},
10    http::StatusCode,
11    Json,
12};
13use serde::{Deserialize, Serialize};
14use serde_json::{json, Value};
15
16use crate::handlers::AdminState;
17use crate::models::ApiResponse;
18
19// Use percent encoding for URL path segments
20// Note: We encode patterns that may contain special characters like / and *
21fn encode_path_segment(s: &str) -> String {
22    // Simple encoding for common special characters in URL paths
23    s.replace('/', "%2F")
24        .replace('*', "%2A")
25        .replace('?', "%3F")
26        .replace('#', "%23")
27        .replace('[', "%5B")
28        .replace(']', "%5D")
29}
30
31/// Helper function to proxy requests to the HTTP server's management API
32async fn proxy_to_http_server(
33    state: &AdminState,
34    path: &str,
35    body: Option<Value>,
36    method: &str,
37) -> Result<Json<ApiResponse<Value>>, StatusCode> {
38    let http_addr = match state.http_server_addr {
39        Some(addr) => addr,
40        None => {
41            return Ok(Json(ApiResponse::error("HTTP server not available".to_string())));
42        }
43    };
44
45    let url = format!("http://{}/__mockforge/api{}", http_addr, path);
46
47    let client = reqwest::Client::new();
48    let mut request = match (method, body.is_some()) {
49        ("PUT", _) => client.put(&url),
50        ("POST", _) => client.post(&url),
51        _ => client.get(&url),
52    };
53
54    if let Some(body_value) = body {
55        request = request.json(&body_value);
56    }
57
58    match request.send().await {
59        Ok(response) => {
60            let status = response.status();
61            match response.json::<Value>().await {
62                Ok(data) => {
63                    if status.is_success() {
64                        Ok(Json(ApiResponse::success(data)))
65                    } else {
66                        Ok(Json(ApiResponse::error(
67                            data.get("error")
68                                .and_then(|v| v.as_str())
69                                .unwrap_or("Request failed")
70                                .to_string(),
71                        )))
72                    }
73                }
74                Err(e) => {
75                    tracing::error!("Failed to parse response: {}", e);
76                    Ok(Json(ApiResponse::error(format!("Failed to parse response: {}", e))))
77                }
78            }
79        }
80        Err(e) => {
81            tracing::error!("Failed to proxy request: {}", e);
82            Ok(Json(ApiResponse::error(format!("Failed to connect to HTTP server: {}", e))))
83        }
84    }
85}
86
87/// Request to set migration mode for a route
88#[derive(Debug, Deserialize)]
89pub struct SetRouteMigrationRequest {
90    /// Migration mode: mock, shadow, real, or auto
91    pub mode: String,
92}
93
94/// Request to set migration mode for a group
95#[derive(Debug, Deserialize)]
96pub struct SetGroupMigrationRequest {
97    /// Migration mode: mock, shadow, real, or auto
98    pub mode: String,
99}
100
101/// Migration status response
102#[derive(Debug, Serialize)]
103pub struct MigrationStatus {
104    /// Total number of routes
105    pub total_routes: usize,
106    /// Number of routes in mock mode
107    pub mock_routes: usize,
108    /// Number of routes in shadow mode
109    pub shadow_routes: usize,
110    /// Number of routes in real mode
111    pub real_routes: usize,
112    /// Number of routes in auto mode
113    pub auto_routes: usize,
114    /// Total number of groups
115    pub total_groups: usize,
116    /// Migration enabled flag
117    pub migration_enabled: bool,
118}
119
120/// Get all migration routes with their status
121pub async fn get_migration_routes(
122    State(state): State<AdminState>,
123) -> Result<Json<ApiResponse<Value>>, StatusCode> {
124    proxy_to_http_server(&state, "/migration/routes", None, "GET").await
125}
126
127/// Toggle a route's migration mode through stages: mock → shadow → real → mock
128pub async fn toggle_route_migration(
129    State(state): State<AdminState>,
130    Path(pattern): Path<String>,
131) -> Result<Json<ApiResponse<Value>>, StatusCode> {
132    let encoded_pattern = encode_path_segment(&pattern);
133    proxy_to_http_server(
134        &state,
135        &format!("/migration/routes/{}/toggle", encoded_pattern),
136        None,
137        "POST",
138    )
139    .await
140}
141
142/// Set a route's migration mode explicitly
143pub async fn set_route_migration_mode(
144    State(state): State<AdminState>,
145    Path(pattern): Path<String>,
146    Json(request): Json<SetRouteMigrationRequest>,
147) -> Result<Json<ApiResponse<Value>>, StatusCode> {
148    let encoded_pattern = encode_path_segment(&pattern);
149    let body = json!({ "mode": request.mode });
150    proxy_to_http_server(
151        &state,
152        &format!("/migration/routes/{}", encoded_pattern),
153        Some(body),
154        "PUT",
155    )
156    .await
157}
158
159/// Toggle a group's migration mode through stages: mock → shadow → real → mock
160pub async fn toggle_group_migration(
161    State(state): State<AdminState>,
162    Path(group): Path<String>,
163) -> Result<Json<ApiResponse<Value>>, StatusCode> {
164    let encoded_group = encode_path_segment(&group);
165    proxy_to_http_server(
166        &state,
167        &format!("/migration/groups/{}/toggle", encoded_group),
168        None,
169        "POST",
170    )
171    .await
172}
173
174/// Set a group's migration mode explicitly
175pub async fn set_group_migration_mode(
176    State(state): State<AdminState>,
177    Path(group): Path<String>,
178    Json(request): Json<SetGroupMigrationRequest>,
179) -> Result<Json<ApiResponse<Value>>, StatusCode> {
180    let encoded_group = encode_path_segment(&group);
181    let body = json!({ "mode": request.mode });
182    proxy_to_http_server(&state, &format!("/migration/groups/{}", encoded_group), Some(body), "PUT")
183        .await
184}
185
186/// Get all migration groups with their status
187pub async fn get_migration_groups(
188    State(state): State<AdminState>,
189) -> Result<Json<ApiResponse<Value>>, StatusCode> {
190    proxy_to_http_server(&state, "/migration/groups", None, "GET").await
191}
192
193/// Get overall migration status
194pub async fn get_migration_status(
195    State(state): State<AdminState>,
196) -> Result<Json<ApiResponse<Value>>, StatusCode> {
197    proxy_to_http_server(&state, "/migration/status", None, "GET").await
198}