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