mockforge_http/handlers/
snapshots.rs

1//! Snapshot management API handlers
2//!
3//! This module provides HTTP handlers for managing snapshots via REST API.
4
5use axum::{
6    extract::{Path, Query, State},
7    http::StatusCode,
8    response::Json,
9};
10use mockforge_core::consistency::ConsistencyEngine;
11use mockforge_core::snapshots::{ProtocolStateExporter, SnapshotComponents, SnapshotManager};
12use mockforge_core::workspace_persistence::WorkspacePersistence;
13use serde::Deserialize;
14use serde_json::Value;
15use std::sync::Arc;
16use tracing::{debug, error, info, warn};
17
18/// State for snapshot handlers
19#[derive(Clone)]
20pub struct SnapshotState {
21    /// Snapshot manager
22    pub manager: Arc<SnapshotManager>,
23    /// Consistency engine (optional, for saving/loading unified state)
24    pub consistency_engine: Option<Arc<ConsistencyEngine>>,
25    /// Workspace persistence (optional, for saving/loading workspace config)
26    pub workspace_persistence: Option<Arc<WorkspacePersistence>>,
27    /// VBR engine (optional, for saving/loading VBR state)
28    /// Now uses ProtocolStateExporter trait for proper state extraction
29    pub vbr_engine: Option<Arc<dyn ProtocolStateExporter>>,
30    /// Recorder database (optional, for saving/loading recorder state)
31    /// Now uses ProtocolStateExporter trait for proper state extraction
32    pub recorder: Option<Arc<dyn ProtocolStateExporter>>,
33}
34
35/// Request to save a snapshot
36#[derive(Debug, Deserialize)]
37pub struct SaveSnapshotRequest {
38    /// Snapshot name
39    pub name: String,
40    /// Optional description
41    pub description: Option<String>,
42    /// Components to include
43    pub components: Option<SnapshotComponents>,
44}
45
46/// Request to load a snapshot
47#[derive(Debug, Deserialize)]
48pub struct LoadSnapshotRequest {
49    /// Components to restore (optional, defaults to all)
50    pub components: Option<SnapshotComponents>,
51}
52
53/// Query parameters for snapshot operations
54#[derive(Debug, Deserialize)]
55pub struct SnapshotQuery {
56    /// Workspace ID (defaults to "default" if not provided)
57    #[serde(default = "default_workspace")]
58    pub workspace: String,
59}
60
61fn default_workspace() -> String {
62    "default".to_string()
63}
64
65/// Extract VBR state from VBR engine if available
66async fn extract_vbr_state(
67    vbr_engine: &Option<Arc<dyn ProtocolStateExporter>>,
68) -> Option<serde_json::Value> {
69    if let Some(engine) = vbr_engine {
70        match engine.export_state().await {
71            Ok(state) => {
72                let summary = engine.state_summary().await;
73                info!("Extracted VBR state from {} engine: {}", engine.protocol_name(), summary);
74                Some(state)
75            }
76            Err(e) => {
77                warn!("Failed to extract VBR state: {}", e);
78                None
79            }
80        }
81    } else {
82        debug!("No VBR engine available for state extraction");
83        None
84    }
85}
86
87/// Extract Recorder state from Recorder if available
88async fn extract_recorder_state(
89    recorder: &Option<Arc<dyn ProtocolStateExporter>>,
90) -> Option<serde_json::Value> {
91    if let Some(rec) = recorder {
92        match rec.export_state().await {
93            Ok(state) => {
94                let summary = rec.state_summary().await;
95                info!("Extracted Recorder state from {} engine: {}", rec.protocol_name(), summary);
96                Some(state)
97            }
98            Err(e) => {
99                warn!("Failed to extract Recorder state: {}", e);
100                None
101            }
102        }
103    } else {
104        debug!("No Recorder available for state extraction");
105        None
106    }
107}
108
109/// Save a snapshot
110///
111/// POST /api/v1/snapshots?workspace={workspace_id}
112pub async fn save_snapshot(
113    State(state): State<SnapshotState>,
114    Query(params): Query<SnapshotQuery>,
115    Json(request): Json<SaveSnapshotRequest>,
116) -> Result<Json<Value>, StatusCode> {
117    let components = request.components.unwrap_or_else(SnapshotComponents::all);
118
119    let consistency_engine = state.consistency_engine.as_deref();
120    let workspace_persistence = state.workspace_persistence.as_deref();
121
122    // Extract VBR state if VBR engine is available
123    let vbr_state = if components.vbr_state {
124        extract_vbr_state(&state.vbr_engine).await
125    } else {
126        None
127    };
128
129    // Extract Recorder state if Recorder is available
130    let recorder_state = if components.recorder_state {
131        extract_recorder_state(&state.recorder).await
132    } else {
133        None
134    };
135    let manifest = state
136        .manager
137        .save_snapshot(
138            request.name.clone(),
139            request.description,
140            params.workspace.clone(),
141            components,
142            consistency_engine,
143            workspace_persistence,
144            vbr_state,
145            recorder_state,
146        )
147        .await
148        .map_err(|e| {
149            error!("Failed to save snapshot: {}", e);
150            StatusCode::INTERNAL_SERVER_ERROR
151        })?;
152
153    info!("Saved snapshot '{}' for workspace '{}'", request.name, params.workspace);
154    Ok(Json(serde_json::json!({
155        "success": true,
156        "manifest": manifest,
157    })))
158}
159
160/// Load a snapshot
161///
162/// POST /api/v1/snapshots/{name}/load?workspace={workspace_id}
163pub async fn load_snapshot(
164    State(state): State<SnapshotState>,
165    Path(name): Path<String>,
166    Query(params): Query<SnapshotQuery>,
167    Json(request): Json<LoadSnapshotRequest>,
168) -> Result<Json<Value>, StatusCode> {
169    let consistency_engine = state.consistency_engine.as_deref();
170    let workspace_persistence = state.workspace_persistence.as_deref();
171    let (manifest, vbr_state, recorder_state) = state
172        .manager
173        .load_snapshot(
174            name.clone(),
175            params.workspace.clone(),
176            request.components,
177            consistency_engine,
178            workspace_persistence,
179        )
180        .await
181        .map_err(|e| {
182            error!("Failed to load snapshot: {}", e);
183            StatusCode::INTERNAL_SERVER_ERROR
184        })?;
185
186    info!("Loaded snapshot '{}' for workspace '{}'", name, params.workspace);
187    Ok(Json(serde_json::json!({
188        "success": true,
189        "manifest": manifest,
190        "vbr_state": vbr_state,
191        "recorder_state": recorder_state,
192    })))
193}
194
195/// List all snapshots
196///
197/// GET /api/v1/snapshots?workspace={workspace_id}
198pub async fn list_snapshots(
199    State(state): State<SnapshotState>,
200    Query(params): Query<SnapshotQuery>,
201) -> Result<Json<Value>, StatusCode> {
202    let snapshots = state.manager.list_snapshots(&params.workspace).await.map_err(|e| {
203        error!("Failed to list snapshots: {}", e);
204        StatusCode::INTERNAL_SERVER_ERROR
205    })?;
206
207    Ok(Json(serde_json::json!({
208        "workspace": params.workspace,
209        "snapshots": snapshots,
210        "count": snapshots.len(),
211    })))
212}
213
214/// Get snapshot information
215///
216/// GET /api/v1/snapshots/{name}?workspace={workspace_id}
217pub async fn get_snapshot_info(
218    State(state): State<SnapshotState>,
219    Path(name): Path<String>,
220    Query(params): Query<SnapshotQuery>,
221) -> Result<Json<Value>, StatusCode> {
222    let manifest = state
223        .manager
224        .get_snapshot_info(name.clone(), params.workspace.clone())
225        .await
226        .map_err(|e| {
227            error!("Failed to get snapshot info: {}", e);
228            StatusCode::NOT_FOUND
229        })?;
230
231    Ok(Json(serde_json::json!({
232        "success": true,
233        "manifest": manifest,
234    })))
235}
236
237/// Delete a snapshot
238///
239/// DELETE /api/v1/snapshots/{name}?workspace={workspace_id}
240pub async fn delete_snapshot(
241    State(state): State<SnapshotState>,
242    Path(name): Path<String>,
243    Query(params): Query<SnapshotQuery>,
244) -> Result<Json<Value>, StatusCode> {
245    state
246        .manager
247        .delete_snapshot(name.clone(), params.workspace.clone())
248        .await
249        .map_err(|e| {
250            error!("Failed to delete snapshot: {}", e);
251            StatusCode::INTERNAL_SERVER_ERROR
252        })?;
253
254    info!("Deleted snapshot '{}' for workspace '{}'", name, params.workspace);
255    Ok(Json(serde_json::json!({
256        "success": true,
257        "message": format!("Snapshot '{}' deleted successfully", name),
258    })))
259}
260
261/// Validate snapshot integrity
262///
263/// GET /api/v1/snapshots/{name}/validate?workspace={workspace_id}
264pub async fn validate_snapshot(
265    State(state): State<SnapshotState>,
266    Path(name): Path<String>,
267    Query(params): Query<SnapshotQuery>,
268) -> Result<Json<Value>, StatusCode> {
269    let is_valid = state
270        .manager
271        .validate_snapshot(name.clone(), params.workspace.clone())
272        .await
273        .map_err(|e| {
274            error!("Failed to validate snapshot: {}", e);
275            StatusCode::INTERNAL_SERVER_ERROR
276        })?;
277
278    Ok(Json(serde_json::json!({
279        "success": true,
280        "valid": is_valid,
281        "snapshot": name,
282        "workspace": params.workspace,
283    })))
284}
285
286/// Create snapshot router
287pub fn snapshot_router(state: SnapshotState) -> axum::Router {
288    use axum::routing::{get, post};
289
290    axum::Router::new()
291        .route("/api/v1/snapshots", get(list_snapshots).post(save_snapshot))
292        .route("/api/v1/snapshots/{name}", get(get_snapshot_info).delete(delete_snapshot))
293        .route("/api/v1/snapshots/{name}/load", post(load_snapshot))
294        .route("/api/v1/snapshots/{name}/validate", get(validate_snapshot))
295        .with_state(state)
296}