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::{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    pub vbr_engine: Option<Arc<dyn std::any::Any + Send + Sync>>,
29    /// Recorder database (optional, for saving/loading recorder state)
30    pub recorder: Option<Arc<dyn std::any::Any + Send + Sync>>,
31}
32
33/// Request to save a snapshot
34#[derive(Debug, Deserialize)]
35pub struct SaveSnapshotRequest {
36    /// Snapshot name
37    pub name: String,
38    /// Optional description
39    pub description: Option<String>,
40    /// Components to include
41    pub components: Option<SnapshotComponents>,
42}
43
44/// Request to load a snapshot
45#[derive(Debug, Deserialize)]
46pub struct LoadSnapshotRequest {
47    /// Components to restore (optional, defaults to all)
48    pub components: Option<SnapshotComponents>,
49}
50
51/// Query parameters for snapshot operations
52#[derive(Debug, Deserialize)]
53pub struct SnapshotQuery {
54    /// Workspace ID (defaults to "default" if not provided)
55    #[serde(default = "default_workspace")]
56    pub workspace: String,
57}
58
59fn default_workspace() -> String {
60    "default".to_string()
61}
62
63/// Extract VBR state from VBR engine if available
64async fn extract_vbr_state(vbr_engine: &Option<Arc<dyn std::any::Any + Send + Sync>>) -> Option<serde_json::Value> {
65    if let Some(engine) = vbr_engine {
66        // Try to downcast to VbrEngine and extract state
67        // Since we can't directly downcast to VbrEngine (it's in a different crate),
68        // we'll use a trait object approach or type_id checking
69        // For now, return None - this can be extended when VBR engine is integrated
70        debug!("VBR engine available, but state extraction not yet implemented");
71        None
72    } else {
73        None
74    }
75}
76
77/// Extract Recorder state from Recorder if available
78async fn extract_recorder_state(recorder: &Option<Arc<dyn std::any::Any + Send + Sync>>) -> Option<serde_json::Value> {
79    if let Some(rec) = recorder {
80        // Try to extract recorder state
81        // Since we can't directly downcast to RecorderDatabase (it's in a different crate),
82        // we'll use a trait object approach
83        // For now, return None - this can be extended when Recorder is integrated
84        debug!("Recorder available, but state extraction not yet implemented");
85        None
86    } else {
87        None
88    }
89}
90
91/// Save a snapshot
92///
93/// POST /api/v1/snapshots?workspace={workspace_id}
94pub async fn save_snapshot(
95    State(state): State<SnapshotState>,
96    Query(params): Query<SnapshotQuery>,
97    Json(request): Json<SaveSnapshotRequest>,
98) -> Result<Json<Value>, StatusCode> {
99    let components = request.components.unwrap_or_else(SnapshotComponents::all);
100
101    let consistency_engine = state.consistency_engine.as_deref();
102    let workspace_persistence = state.workspace_persistence.as_deref();
103
104    // Extract VBR state if VBR engine is available
105    let vbr_state = if components.vbr_state {
106        extract_vbr_state(&state.vbr_engine).await
107    } else {
108        None
109    };
110
111    // Extract Recorder state if Recorder is available
112    let recorder_state = if components.recorder_state {
113        extract_recorder_state(&state.recorder).await
114    } else {
115        None
116    };
117    let manifest = state
118        .manager
119        .save_snapshot(
120            request.name.clone(),
121            request.description,
122            params.workspace.clone(),
123            components,
124            consistency_engine,
125            workspace_persistence,
126            vbr_state,
127            recorder_state,
128        )
129        .await
130        .map_err(|e| {
131            error!("Failed to save snapshot: {}", e);
132            StatusCode::INTERNAL_SERVER_ERROR
133        })?;
134
135    info!("Saved snapshot '{}' for workspace '{}'", request.name, params.workspace);
136    Ok(Json(serde_json::json!({
137        "success": true,
138        "manifest": manifest,
139    })))
140}
141
142/// Load a snapshot
143///
144/// POST /api/v1/snapshots/{name}/load?workspace={workspace_id}
145pub async fn load_snapshot(
146    State(state): State<SnapshotState>,
147    Path(name): Path<String>,
148    Query(params): Query<SnapshotQuery>,
149    Json(request): Json<LoadSnapshotRequest>,
150) -> Result<Json<Value>, StatusCode> {
151    let consistency_engine = state.consistency_engine.as_deref();
152    let workspace_persistence = state.workspace_persistence.as_deref();
153    let (manifest, vbr_state, recorder_state) = state
154        .manager
155        .load_snapshot(
156            name.clone(),
157            params.workspace.clone(),
158            request.components,
159            consistency_engine,
160            workspace_persistence,
161        )
162        .await
163        .map_err(|e| {
164            error!("Failed to load snapshot: {}", e);
165            StatusCode::INTERNAL_SERVER_ERROR
166        })?;
167
168    info!("Loaded snapshot '{}' for workspace '{}'", name, params.workspace);
169    Ok(Json(serde_json::json!({
170        "success": true,
171        "manifest": manifest,
172        "vbr_state": vbr_state,
173        "recorder_state": recorder_state,
174    })))
175}
176
177/// List all snapshots
178///
179/// GET /api/v1/snapshots?workspace={workspace_id}
180pub async fn list_snapshots(
181    State(state): State<SnapshotState>,
182    Query(params): Query<SnapshotQuery>,
183) -> Result<Json<Value>, StatusCode> {
184    let snapshots = state.manager.list_snapshots(&params.workspace).await.map_err(|e| {
185        error!("Failed to list snapshots: {}", e);
186        StatusCode::INTERNAL_SERVER_ERROR
187    })?;
188
189    Ok(Json(serde_json::json!({
190        "workspace": params.workspace,
191        "snapshots": snapshots,
192        "count": snapshots.len(),
193    })))
194}
195
196/// Get snapshot information
197///
198/// GET /api/v1/snapshots/{name}?workspace={workspace_id}
199pub async fn get_snapshot_info(
200    State(state): State<SnapshotState>,
201    Path(name): Path<String>,
202    Query(params): Query<SnapshotQuery>,
203) -> Result<Json<Value>, StatusCode> {
204    let manifest = state
205        .manager
206        .get_snapshot_info(name.clone(), params.workspace.clone())
207        .await
208        .map_err(|e| {
209            error!("Failed to get snapshot info: {}", e);
210            StatusCode::NOT_FOUND
211        })?;
212
213    Ok(Json(serde_json::json!({
214        "success": true,
215        "manifest": manifest,
216    })))
217}
218
219/// Delete a snapshot
220///
221/// DELETE /api/v1/snapshots/{name}?workspace={workspace_id}
222pub async fn delete_snapshot(
223    State(state): State<SnapshotState>,
224    Path(name): Path<String>,
225    Query(params): Query<SnapshotQuery>,
226) -> Result<Json<Value>, StatusCode> {
227    state
228        .manager
229        .delete_snapshot(name.clone(), params.workspace.clone())
230        .await
231        .map_err(|e| {
232            error!("Failed to delete snapshot: {}", e);
233            StatusCode::INTERNAL_SERVER_ERROR
234        })?;
235
236    info!("Deleted snapshot '{}' for workspace '{}'", name, params.workspace);
237    Ok(Json(serde_json::json!({
238        "success": true,
239        "message": format!("Snapshot '{}' deleted successfully", name),
240    })))
241}
242
243/// Validate snapshot integrity
244///
245/// GET /api/v1/snapshots/{name}/validate?workspace={workspace_id}
246pub async fn validate_snapshot(
247    State(state): State<SnapshotState>,
248    Path(name): Path<String>,
249    Query(params): Query<SnapshotQuery>,
250) -> Result<Json<Value>, StatusCode> {
251    let is_valid = state
252        .manager
253        .validate_snapshot(name.clone(), params.workspace.clone())
254        .await
255        .map_err(|e| {
256            error!("Failed to validate snapshot: {}", e);
257            StatusCode::INTERNAL_SERVER_ERROR
258        })?;
259
260    Ok(Json(serde_json::json!({
261        "success": true,
262        "valid": is_valid,
263        "snapshot": name,
264        "workspace": params.workspace,
265    })))
266}
267
268/// Create snapshot router
269pub fn snapshot_router(state: SnapshotState) -> axum::Router {
270    use axum::routing::{get, post};
271
272    axum::Router::new()
273        .route("/api/v1/snapshots", get(list_snapshots).post(save_snapshot))
274        .route("/api/v1/snapshots/{name}", get(get_snapshot_info).delete(delete_snapshot))
275        .route("/api/v1/snapshots/{name}/load", post(load_snapshot))
276        .route("/api/v1/snapshots/{name}/validate", get(validate_snapshot))
277        .with_state(state)
278}