mockforge_http/
state_machine_api.rs

1//! State machine API handlers
2//!
3//! Provides REST endpoints for managing scenario state machines, including
4//! CRUD operations, execution, and import/export functionality.
5
6use axum::{
7    extract::{Path, Query, State},
8    http::StatusCode,
9    response::IntoResponse,
10    Json,
11};
12use mockforge_core::intelligent_behavior::{rules::StateMachine, visual_layout::VisualLayout};
13use mockforge_scenarios::{
14    state_machine::{ScenarioStateMachineManager, StateInstance},
15    ScenarioManifest,
16};
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use std::collections::HashMap;
20use std::sync::Arc;
21use tokio::sync::RwLock;
22use tracing::{debug, error, info, warn};
23
24// Re-export ManagementState for use in handlers
25use crate::management::ManagementState;
26
27// ===== Request/Response Types =====
28
29/// Request to create or update a state machine
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct StateMachineRequest {
32    /// State machine definition
33    pub state_machine: StateMachine,
34    /// Optional visual layout
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub visual_layout: Option<VisualLayout>,
37}
38
39/// Request to execute a state transition
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct TransitionRequest {
42    /// Resource ID to transition
43    pub resource_id: String,
44    /// Target state
45    pub to_state: String,
46    /// Optional context variables for condition evaluation
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub context: Option<HashMap<String, Value>>,
49}
50
51/// Request to create a state instance
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct CreateInstanceRequest {
54    /// Resource ID
55    pub resource_id: String,
56    /// Resource type (must match a state machine resource_type)
57    pub resource_type: String,
58}
59
60/// Response for state machine operations
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct StateMachineResponse {
63    /// State machine definition
64    pub state_machine: StateMachine,
65    /// Optional visual layout
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub visual_layout: Option<VisualLayout>,
68}
69
70/// Response for listing state machines
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct StateMachineListResponse {
73    /// List of state machines
74    pub state_machines: Vec<StateMachineInfo>,
75    /// Total count
76    pub total: usize,
77}
78
79/// Information about a state machine
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct StateMachineInfo {
82    /// Resource type
83    pub resource_type: String,
84    /// Number of states
85    pub state_count: usize,
86    /// Number of transitions
87    pub transition_count: usize,
88    /// Number of sub-scenarios
89    pub sub_scenario_count: usize,
90    /// Whether it has a visual layout
91    pub has_visual_layout: bool,
92}
93
94/// Response for state instance operations
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct StateInstanceResponse {
97    /// Resource ID
98    pub resource_id: String,
99    /// Current state
100    pub current_state: String,
101    /// Resource type
102    pub resource_type: String,
103    /// State history count
104    pub history_count: usize,
105    /// State data
106    pub state_data: HashMap<String, Value>,
107}
108
109/// Response for listing state instances
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct StateInstanceListResponse {
112    /// List of instances
113    pub instances: Vec<StateInstanceResponse>,
114    /// Total count
115    pub total: usize,
116}
117
118/// Response for next states query
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct NextStatesResponse {
121    /// List of possible next states
122    pub next_states: Vec<String>,
123}
124
125/// Response for import/export operations
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ImportExportResponse {
128    /// State machines
129    pub state_machines: Vec<StateMachine>,
130    /// Visual layouts by resource type
131    pub visual_layouts: HashMap<String, VisualLayout>,
132}
133
134// ===== Handlers =====
135
136/// List all state machines
137pub async fn list_state_machines(
138    State(state): State<ManagementState>,
139) -> Result<Json<StateMachineListResponse>, StatusCode> {
140    let manager = state.state_machine_manager.read().await;
141
142    // Get all state machines
143    let machines = manager.list_state_machines().await;
144
145    // Check visual layouts separately for each state machine
146    // We need to check if a visual layout exists for each state machine
147    let mut state_machine_list = Vec::new();
148    for (resource_type, sm) in machines.iter() {
149        let has_visual_layout = manager.get_visual_layout(resource_type).await.is_some();
150        state_machine_list.push(StateMachineInfo {
151            resource_type: resource_type.clone(),
152            state_count: sm.states.len(),
153            transition_count: sm.transitions.len(),
154            sub_scenario_count: sm.sub_scenarios.len(),
155            has_visual_layout,
156        });
157    }
158
159    Ok(Json(StateMachineListResponse {
160        state_machines: state_machine_list.clone(),
161        total: state_machine_list.len(),
162    }))
163}
164
165/// Get a state machine by resource type
166pub async fn get_state_machine(
167    State(state): State<ManagementState>,
168    Path(resource_type): Path<String>,
169) -> Result<Json<StateMachineResponse>, StatusCode> {
170    let manager = state.state_machine_manager.read().await;
171
172    let state_machine =
173        manager.get_state_machine(&resource_type).await.ok_or(StatusCode::NOT_FOUND)?;
174
175    let visual_layout = manager.get_visual_layout(&resource_type).await;
176
177    // Convert types from mockforge-scenarios' dependency version to local version
178    // by serializing and deserializing through JSON
179    let state_machine_json =
180        serde_json::to_value(&state_machine).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
181    let state_machine: StateMachine = serde_json::from_value(state_machine_json)
182        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
183
184    let visual_layout: Option<VisualLayout> = visual_layout
185        .map(|layout| {
186            let layout_json =
187                serde_json::to_value(&layout).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
188            serde_json::from_value(layout_json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
189        })
190        .transpose()
191        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
192
193    Ok(Json(StateMachineResponse {
194        state_machine,
195        visual_layout,
196    }))
197}
198
199/// Create or update a state machine
200pub async fn create_state_machine(
201    State(state): State<ManagementState>,
202    Json(request): Json<StateMachineRequest>,
203) -> Result<Json<StateMachineResponse>, StatusCode> {
204    let mut manager = state.state_machine_manager.write().await;
205
206    // Convert types from local version to mockforge-scenarios' dependency version
207    // by serializing and deserializing through JSON
208    // The ScenarioManifest uses types from mockforge-scenarios' mockforge-core dependency (0.2.9)
209    // We need to convert our local StateMachine to that version
210    let state_machine_json = serde_json::to_value(&request.state_machine)
211        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
212
213    // Create manifest with JSON values - serde will deserialize into the correct types
214    // We need to provide all required fields for ScenarioManifest
215    let mut manifest_json = serde_json::json!({
216        "manifest_version": "1.0",
217        "name": "api",
218        "version": "1.0.0",
219        "title": "API State Machine",
220        "description": "State machine created via API",
221        "author": "api",
222        "category": "other",
223        "compatibility": {
224            "min_version": "0.1.0",
225            "max_version": null
226        },
227        "files": [],
228        "state_machines": [state_machine_json],
229        "state_machine_graphs": {}
230    });
231
232    if let Some(layout) = &request.visual_layout {
233        let layout_json =
234            serde_json::to_value(layout).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
235        manifest_json["state_machine_graphs"][&request.state_machine.resource_type] = layout_json;
236    }
237
238    let manifest: ScenarioManifest =
239        serde_json::from_value(manifest_json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
240
241    // Validate the first state machine from manifest
242    if let Some(ref sm) = manifest.state_machines.first() {
243        if let Err(e) = manager.validate_state_machine(sm) {
244            error!("Invalid state machine: {}", e);
245            return Err(StatusCode::BAD_REQUEST);
246        }
247    }
248
249    if let Err(e) = manager.load_from_manifest(&manifest).await {
250        error!("Failed to load state machine: {}", e);
251        return Err(StatusCode::INTERNAL_SERVER_ERROR);
252    }
253
254    // Visual layout is already set in the manifest, no need to set separately
255
256    // Broadcast WebSocket event
257    if let Some(ref ws_tx) = state.ws_broadcast {
258        let event = crate::management_ws::MockEvent::state_machine_updated(
259            request.state_machine.resource_type.clone(),
260            request.state_machine.clone(),
261        );
262        let _ = ws_tx.send(event);
263    }
264
265    // Get state machine and layout back after loading (returns version from mockforge-scenarios' dependency)
266    let state_machine_from_manager = manager
267        .get_state_machine(&request.state_machine.resource_type)
268        .await
269        .ok_or(StatusCode::NOT_FOUND)?;
270    let visual_layout_from_manager =
271        manager.get_visual_layout(&request.state_machine.resource_type).await;
272
273    // Convert back to local types
274    let state_machine_json = serde_json::to_value(&state_machine_from_manager)
275        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
276    let state_machine: StateMachine = serde_json::from_value(state_machine_json)
277        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
278
279    let visual_layout: Option<VisualLayout> = visual_layout_from_manager
280        .map(|layout| {
281            let layout_json =
282                serde_json::to_value(&layout).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
283            serde_json::from_value(layout_json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
284        })
285        .transpose()
286        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
287
288    Ok(Json(StateMachineResponse {
289        state_machine,
290        visual_layout,
291    }))
292}
293
294/// Delete a state machine
295pub async fn delete_state_machine(
296    State(state): State<ManagementState>,
297    Path(resource_type): Path<String>,
298) -> Result<StatusCode, StatusCode> {
299    let mut manager = state.state_machine_manager.write().await;
300
301    // Delete the state machine
302    let deleted = manager.delete_state_machine(&resource_type).await;
303
304    if !deleted {
305        return Err(StatusCode::NOT_FOUND);
306    }
307
308    // Broadcast WebSocket event
309    if let Some(ref ws_tx) = state.ws_broadcast {
310        let event = crate::management_ws::MockEvent::state_machine_deleted(resource_type);
311        let _ = ws_tx.send(event);
312    }
313
314    Ok(StatusCode::NO_CONTENT)
315}
316
317/// List all state instances
318pub async fn list_instances(
319    State(state): State<ManagementState>,
320) -> Result<Json<StateInstanceListResponse>, StatusCode> {
321    let manager = state.state_machine_manager.read().await;
322
323    let instances = manager.list_instances().await;
324
325    let instance_responses: Vec<StateInstanceResponse> = instances
326        .iter()
327        .map(|i| StateInstanceResponse {
328            resource_id: i.resource_id.clone(),
329            current_state: i.current_state.clone(),
330            resource_type: i.resource_type.clone(),
331            history_count: i.state_history.len(),
332            state_data: i.state_data.clone(),
333        })
334        .collect();
335
336    Ok(Json(StateInstanceListResponse {
337        instances: instance_responses,
338        total: instances.len(),
339    }))
340}
341
342/// Get a state instance by resource ID
343pub async fn get_instance(
344    State(state): State<ManagementState>,
345    Path(resource_id): Path<String>,
346) -> Result<Json<StateInstanceResponse>, StatusCode> {
347    let manager = state.state_machine_manager.read().await;
348
349    let instance = manager.get_instance(&resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
350
351    Ok(Json(StateInstanceResponse {
352        resource_id: instance.resource_id,
353        current_state: instance.current_state,
354        resource_type: instance.resource_type,
355        history_count: instance.state_history.len(),
356        state_data: instance.state_data,
357    }))
358}
359
360/// Create a new state instance
361pub async fn create_instance(
362    State(state): State<ManagementState>,
363    Json(request): Json<CreateInstanceRequest>,
364) -> Result<Json<StateInstanceResponse>, StatusCode> {
365    let manager = state.state_machine_manager.write().await;
366
367    if let Err(e) = manager.create_instance(&request.resource_id, &request.resource_type).await {
368        error!("Failed to create instance: {}", e);
369        return Err(StatusCode::BAD_REQUEST);
370    }
371
372    let instance = manager
373        .get_instance(&request.resource_id)
374        .await
375        .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
376
377    // Broadcast WebSocket event
378    if let Some(ref ws_tx) = state.ws_broadcast {
379        let event = crate::management_ws::MockEvent::state_instance_created(
380            instance.resource_id.clone(),
381            instance.resource_type.clone(),
382            instance.current_state.clone(),
383        );
384        let _ = ws_tx.send(event);
385    }
386
387    Ok(Json(StateInstanceResponse {
388        resource_id: instance.resource_id,
389        current_state: instance.current_state,
390        resource_type: instance.resource_type,
391        history_count: instance.state_history.len(),
392        state_data: instance.state_data,
393    }))
394}
395
396/// Execute a state transition
397pub async fn execute_transition(
398    State(state): State<ManagementState>,
399    Json(request): Json<TransitionRequest>,
400) -> Result<Json<StateInstanceResponse>, StatusCode> {
401    let manager = state.state_machine_manager.write().await;
402
403    if let Err(e) = manager
404        .execute_transition(&request.resource_id, &request.to_state, request.context)
405        .await
406    {
407        error!("Failed to execute transition: {}", e);
408        return Err(StatusCode::BAD_REQUEST);
409    }
410
411    let instance = manager.get_instance(&request.resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
412
413    // Get the previous state from history if available
414    let from_state = instance
415        .state_history
416        .last()
417        .map(|h| h.from_state.clone())
418        .unwrap_or_else(|| instance.current_state.clone());
419
420    // Broadcast WebSocket event
421    if let Some(ref ws_tx) = state.ws_broadcast {
422        let event = crate::management_ws::MockEvent::state_transitioned(
423            instance.resource_id.clone(),
424            instance.resource_type.clone(),
425            from_state,
426            instance.current_state.clone(),
427            instance.state_data.clone(),
428        );
429        let _ = ws_tx.send(event);
430    }
431
432    Ok(Json(StateInstanceResponse {
433        resource_id: instance.resource_id,
434        current_state: instance.current_state,
435        resource_type: instance.resource_type,
436        history_count: instance.state_history.len(),
437        state_data: instance.state_data,
438    }))
439}
440
441/// Get next possible states for a resource
442pub async fn get_next_states(
443    State(state): State<ManagementState>,
444    Path(resource_id): Path<String>,
445) -> Result<Json<NextStatesResponse>, StatusCode> {
446    let manager = state.state_machine_manager.read().await;
447
448    let next_states =
449        manager.get_next_states(&resource_id).await.map_err(|_| StatusCode::NOT_FOUND)?;
450
451    Ok(Json(NextStatesResponse { next_states }))
452}
453
454/// Get current state of a resource
455pub async fn get_current_state(
456    State(state): State<ManagementState>,
457    Path(resource_id): Path<String>,
458) -> Result<Json<serde_json::Value>, StatusCode> {
459    let manager = state.state_machine_manager.read().await;
460
461    let current_state =
462        manager.get_current_state(&resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
463
464    Ok(Json(serde_json::json!({
465        "resource_id": resource_id,
466        "current_state": current_state
467    })))
468}
469
470/// Export state machines as JSON
471pub async fn export_state_machines(
472    State(state): State<ManagementState>,
473) -> Result<Json<ImportExportResponse>, StatusCode> {
474    let manager = state.state_machine_manager.read().await;
475
476    // Export all state machines and visual layouts (returns versions from mockforge-scenarios' dependency)
477    let (state_machines_from_manager, visual_layouts_from_manager) = manager.export_all().await;
478
479    // Convert to local types by serializing and deserializing
480    let state_machines: Vec<StateMachine> = state_machines_from_manager
481        .into_iter()
482        .map(|sm| {
483            let json = serde_json::to_value(&sm).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
484            serde_json::from_value(json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
485        })
486        .collect::<Result<Vec<_>, StatusCode>>()?;
487
488    let visual_layouts: HashMap<String, VisualLayout> = visual_layouts_from_manager
489        .into_iter()
490        .map(|(k, v)| {
491            let json = serde_json::to_value(&v).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
492            let layout: VisualLayout =
493                serde_json::from_value(json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
494            Ok((k, layout))
495        })
496        .collect::<Result<HashMap<_, _>, StatusCode>>()?;
497
498    Ok(Json(ImportExportResponse {
499        state_machines,
500        visual_layouts,
501    }))
502}
503
504/// Import state machines from JSON
505pub async fn import_state_machines(
506    State(state): State<ManagementState>,
507    Json(request): Json<ImportExportResponse>,
508) -> Result<StatusCode, StatusCode> {
509    let mut manager = state.state_machine_manager.write().await;
510
511    // Create manifest from JSON to let serde handle type conversion
512    // We need to provide all required fields for ScenarioManifest
513    let manifest_json = serde_json::json!({
514        "manifest_version": "1.0",
515        "name": "imported",
516        "version": "1.0.0",
517        "title": "Imported State Machines",
518        "description": "State machines imported via API",
519        "author": "api",
520        "category": "other",
521        "compatibility": {
522            "min_version": "0.1.0",
523            "max_version": null
524        },
525        "files": [],
526        "state_machines": request.state_machines,
527        "state_machine_graphs": request.visual_layouts
528    });
529
530    let manifest: ScenarioManifest =
531        serde_json::from_value(manifest_json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
532
533    if let Err(e) = manager.load_from_manifest(&manifest).await {
534        error!("Failed to import state machines: {}", e);
535        return Err(StatusCode::BAD_REQUEST);
536    }
537
538    // Visual layouts are already set in the manifest, no need to set separately
539
540    Ok(StatusCode::CREATED)
541}
542
543/// Create the state machine API router
544///
545/// This function creates routes that use ManagementState, so they can be
546/// nested within the management router.
547pub fn create_state_machine_routes() -> axum::Router<ManagementState> {
548    use axum::{
549        routing::{delete, get, post, put},
550        Router,
551    };
552
553    Router::new()
554        // State machine CRUD
555        .route("/", get(list_state_machines))
556        .route("/", post(create_state_machine))
557        .route("/{resource_type}", get(get_state_machine))
558        .route("/{resource_type}", put(create_state_machine))
559        .route("/{resource_type}", delete(delete_state_machine))
560
561        // State instance operations
562        .route("/instances", get(list_instances))
563        .route("/instances", post(create_instance))
564        .route("/instances/{resource_id}", get(get_instance))
565        .route("/instances/{resource_id}/state", get(get_current_state))
566        .route("/instances/{resource_id}/next-states", get(get_next_states))
567        .route("/instances/{resource_id}/transition", post(execute_transition))
568
569        // Import/Export
570        .route("/export", get(export_state_machines))
571        .route("/import", post(import_state_machines))
572}