1use axum::{
9 extract::{Path, Query, State},
10 http::StatusCode,
11 response::Json,
12 routing::{get, post},
13 Router,
14};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::sync::Arc;
18
19use crate::management::{ManagementState, MockConfig};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct MockSnapshot {
24 pub id: String,
26 pub timestamp: i64,
28 pub environment_id: Option<String>,
30 pub persona_id: Option<String>,
32 pub scenario_id: Option<String>,
34 pub reality_level: Option<f64>,
36 pub mocks: Vec<MockSnapshotItem>,
38 pub metadata: HashMap<String, serde_json::Value>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct MockSnapshotItem {
45 pub id: String,
47 pub method: String,
49 pub path: String,
51 pub status_code: u16,
53 pub response_body: serde_json::Value,
55 pub response_headers: Option<HashMap<String, String>>,
57 pub config: serde_json::Value,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct SnapshotDiff {
64 pub left: MockSnapshot,
66 pub right: MockSnapshot,
68 pub differences: Vec<Difference>,
70 pub summary: DiffSummary,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct Difference {
77 pub diff_type: DifferenceType,
79 pub mock_id: Option<String>,
81 pub path: String,
83 pub method: String,
85 pub description: String,
87 pub left_value: Option<serde_json::Value>,
89 pub right_value: Option<serde_json::Value>,
91 pub field_path: Option<String>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum DifferenceType {
99 MissingInRight,
101 MissingInLeft,
103 StatusCodeMismatch,
105 BodyMismatch,
107 HeadersMismatch,
109 ConfigMismatch,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct DiffSummary {
116 pub left_total: usize,
118 pub right_total: usize,
120 pub differences_count: usize,
122 pub only_in_left: usize,
124 pub only_in_right: usize,
126 pub mocks_with_differences: usize,
128}
129
130#[derive(Debug, Deserialize)]
132pub struct CreateSnapshotRequest {
133 pub environment_id: Option<String>,
135 pub persona_id: Option<String>,
137 pub scenario_id: Option<String>,
139 pub reality_level: Option<f64>,
141 #[serde(default)]
143 pub metadata: HashMap<String, serde_json::Value>,
144}
145
146#[derive(Debug, Deserialize)]
148pub struct CompareSnapshotsRequest {
149 pub left_snapshot_id: Option<String>,
151 pub right_snapshot_id: Option<String>,
153 pub left_environment_id: Option<String>,
155 pub right_environment_id: Option<String>,
157 pub left_persona_id: Option<String>,
159 pub right_persona_id: Option<String>,
161 pub left_scenario_id: Option<String>,
163 pub right_scenario_id: Option<String>,
165 pub left_reality_level: Option<f64>,
167 pub right_reality_level: Option<f64>,
169}
170
171type SnapshotStorage = Arc<tokio::sync::RwLock<HashMap<String, MockSnapshot>>>;
173
174async fn create_snapshot(
176 State(state): State<ManagementState>,
177 Json(request): Json<CreateSnapshotRequest>,
178) -> Result<Json<MockSnapshot>, StatusCode> {
179 let mocks = state.mocks.read().await.clone();
181
182 let snapshot_items: Vec<MockSnapshotItem> = mocks
184 .iter()
185 .map(|mock| MockSnapshotItem {
186 id: mock.id.clone(),
187 method: mock.method.clone(),
188 path: mock.path.clone(),
189 status_code: mock.status_code.unwrap_or(200),
190 response_body: mock.response.body.clone(),
191 response_headers: mock.response.headers.clone(),
192 config: serde_json::to_value(mock).unwrap_or_default(),
193 })
194 .collect();
195
196 let snapshot = MockSnapshot {
198 id: uuid::Uuid::new_v4().to_string(),
199 timestamp: chrono::Utc::now().timestamp(),
200 environment_id: request.environment_id,
201 persona_id: request.persona_id,
202 scenario_id: request.scenario_id,
203 reality_level: request.reality_level,
204 mocks: snapshot_items,
205 metadata: request.metadata,
206 };
207
208 Ok(Json(snapshot))
213}
214
215async fn get_snapshot(
217 Path(_snapshot_id): Path<String>,
218 State(_state): State<ManagementState>,
219) -> Result<Json<MockSnapshot>, StatusCode> {
220 Err(StatusCode::NOT_IMPLEMENTED)
223}
224
225async fn list_snapshots(
227 Query(params): Query<HashMap<String, String>>,
228 State(_state): State<ManagementState>,
229) -> Result<Json<Vec<MockSnapshot>>, StatusCode> {
230 Ok(Json(vec![]))
233}
234
235async fn compare_snapshots(
237 State(state): State<ManagementState>,
238 Json(request): Json<CompareSnapshotsRequest>,
239) -> Result<Json<SnapshotDiff>, StatusCode> {
240 let current_mocks = state.mocks.read().await.clone();
242
243 let left_snapshot = create_snapshot_from_mocks(
247 ¤t_mocks,
248 request.left_environment_id.clone(),
249 request.left_persona_id.clone(),
250 request.left_scenario_id.clone(),
251 request.left_reality_level,
252 );
253
254 let right_snapshot = create_snapshot_from_mocks(
255 ¤t_mocks,
256 request.right_environment_id.clone(),
257 request.right_persona_id.clone(),
258 request.right_scenario_id.clone(),
259 request.right_reality_level,
260 );
261
262 let diff = compare_snapshot_objects(&left_snapshot, &right_snapshot);
264
265 Ok(Json(diff))
266}
267
268fn create_snapshot_from_mocks(
270 mocks: &[MockConfig],
271 environment_id: Option<String>,
272 persona_id: Option<String>,
273 scenario_id: Option<String>,
274 reality_level: Option<f64>,
275) -> MockSnapshot {
276 let snapshot_items: Vec<MockSnapshotItem> = mocks
277 .iter()
278 .map(|mock| MockSnapshotItem {
279 id: mock.id.clone(),
280 method: mock.method.clone(),
281 path: mock.path.clone(),
282 status_code: mock.status_code.unwrap_or(200),
283 response_body: mock.response.body.clone(),
284 response_headers: mock.response.headers.clone(),
285 config: serde_json::to_value(mock).unwrap_or_default(),
286 })
287 .collect();
288
289 MockSnapshot {
290 id: uuid::Uuid::new_v4().to_string(),
291 timestamp: chrono::Utc::now().timestamp(),
292 environment_id,
293 persona_id,
294 scenario_id,
295 reality_level,
296 mocks: snapshot_items,
297 metadata: HashMap::new(),
298 }
299}
300
301fn compare_snapshot_objects(left: &MockSnapshot, right: &MockSnapshot) -> SnapshotDiff {
303 let mut differences = Vec::new();
304
305 let left_map: HashMap<String, &MockSnapshotItem> =
307 left.mocks.iter().map(|m| (format!("{}:{}", m.method, m.path), m)).collect();
308
309 let right_map: HashMap<String, &MockSnapshotItem> =
310 right.mocks.iter().map(|m| (format!("{}:{}", m.method, m.path), m)).collect();
311
312 for (key, left_mock) in &left_map {
314 if !right_map.contains_key(key) {
315 differences.push(Difference {
316 diff_type: DifferenceType::MissingInRight,
317 mock_id: Some(left_mock.id.clone()),
318 path: left_mock.path.clone(),
319 method: left_mock.method.clone(),
320 description: format!(
321 "Mock {} {} exists in left but not in right",
322 left_mock.method, left_mock.path
323 ),
324 left_value: Some(serde_json::to_value(left_mock).unwrap_or_default()),
325 right_value: None,
326 field_path: None,
327 });
328 }
329 }
330
331 for (key, right_mock) in &right_map {
333 if !left_map.contains_key(key) {
334 differences.push(Difference {
335 diff_type: DifferenceType::MissingInLeft,
336 mock_id: Some(right_mock.id.clone()),
337 path: right_mock.path.clone(),
338 method: right_mock.method.clone(),
339 description: format!(
340 "Mock {} {} exists in right but not in left",
341 right_mock.method, right_mock.path
342 ),
343 left_value: None,
344 right_value: Some(serde_json::to_value(right_mock).unwrap_or_default()),
345 field_path: None,
346 });
347 }
348 }
349
350 for (key, left_mock) in &left_map {
352 if let Some(right_mock) = right_map.get(key) {
353 if left_mock.status_code != right_mock.status_code {
355 differences.push(Difference {
356 diff_type: DifferenceType::StatusCodeMismatch,
357 mock_id: Some(left_mock.id.clone()),
358 path: left_mock.path.clone(),
359 method: left_mock.method.clone(),
360 description: format!(
361 "Status code differs: {} vs {}",
362 left_mock.status_code, right_mock.status_code
363 ),
364 left_value: Some(serde_json::json!(left_mock.status_code)),
365 right_value: Some(serde_json::json!(right_mock.status_code)),
366 field_path: Some("status_code".to_string()),
367 });
368 }
369
370 if left_mock.response_body != right_mock.response_body {
372 differences.push(Difference {
373 diff_type: DifferenceType::BodyMismatch,
374 mock_id: Some(left_mock.id.clone()),
375 path: left_mock.path.clone(),
376 method: left_mock.method.clone(),
377 description: format!(
378 "Response body differs for {} {}",
379 left_mock.method, left_mock.path
380 ),
381 left_value: Some(left_mock.response_body.clone()),
382 right_value: Some(right_mock.response_body.clone()),
383 field_path: Some("response_body".to_string()),
384 });
385 }
386
387 if left_mock.response_headers != right_mock.response_headers {
389 differences.push(Difference {
390 diff_type: DifferenceType::HeadersMismatch,
391 mock_id: Some(left_mock.id.clone()),
392 path: left_mock.path.clone(),
393 method: left_mock.method.clone(),
394 description: format!(
395 "Response headers differ for {} {}",
396 left_mock.method, left_mock.path
397 ),
398 left_value: left_mock
399 .response_headers
400 .as_ref()
401 .map(|h| serde_json::to_value(h).unwrap_or_default()),
402 right_value: right_mock
403 .response_headers
404 .as_ref()
405 .map(|h| serde_json::to_value(h).unwrap_or_default()),
406 field_path: Some("response_headers".to_string()),
407 });
408 }
409 }
410 }
411
412 let only_in_left = differences
414 .iter()
415 .filter(|d| matches!(d.diff_type, DifferenceType::MissingInRight))
416 .count();
417 let only_in_right = differences
418 .iter()
419 .filter(|d| matches!(d.diff_type, DifferenceType::MissingInLeft))
420 .count();
421 let mocks_with_differences: std::collections::HashSet<String> =
422 differences.iter().filter_map(|d| d.mock_id.clone()).collect();
423
424 let summary = DiffSummary {
425 left_total: left.mocks.len(),
426 right_total: right.mocks.len(),
427 differences_count: differences.len(),
428 only_in_left,
429 only_in_right,
430 mocks_with_differences: mocks_with_differences.len(),
431 };
432
433 SnapshotDiff {
434 left: left.clone(),
435 right: right.clone(),
436 differences,
437 summary,
438 }
439}
440
441pub fn snapshot_diff_router(state: ManagementState) -> Router<ManagementState> {
443 Router::new()
444 .route("/snapshots", post(create_snapshot))
445 .route("/snapshots", get(list_snapshots))
446 .route("/snapshots/{id}", get(get_snapshot))
447 .route("/snapshots/compare", post(compare_snapshots))
448 .with_state(state)
449}