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    let state_machine_list: Vec<_> = machines
146        .iter()
147        .map(|(resource_type, sm)| StateMachineInfo {
148            resource_type: resource_type.clone(),
149            state_count: sm.states.len(),
150            transition_count: sm.transitions.len(),
151            sub_scenario_count: sm.sub_scenarios.len(),
152            has_visual_layout: sm.visual_layout.is_some(),
153        })
154        .collect();
155
156    Ok(Json(StateMachineListResponse {
157        state_machines: state_machine_list.clone(),
158        total: state_machine_list.len(),
159    }))
160}
161
162/// Get a state machine by resource type
163pub async fn get_state_machine(
164    State(state): State<ManagementState>,
165    Path(resource_type): Path<String>,
166) -> Result<Json<StateMachineResponse>, StatusCode> {
167    let manager = state.state_machine_manager.read().await;
168
169    let state_machine =
170        manager.get_state_machine(&resource_type).await.ok_or(StatusCode::NOT_FOUND)?;
171
172    let visual_layout = manager.get_visual_layout(&resource_type).await;
173
174    Ok(Json(StateMachineResponse {
175        state_machine,
176        visual_layout,
177    }))
178}
179
180/// Create or update a state machine
181pub async fn create_state_machine(
182    State(state): State<ManagementState>,
183    Json(request): Json<StateMachineRequest>,
184) -> Result<Json<StateMachineResponse>, StatusCode> {
185    let mut manager = state.state_machine_manager.write().await;
186
187    // Validate state machine
188    if let Err(e) = manager.validate_state_machine(&request.state_machine) {
189        error!("Invalid state machine: {}", e);
190        return Err(StatusCode::BAD_REQUEST);
191    }
192
193    // Store state machine (we'd need to add a method to store directly)
194    // For now, we'll create a minimal manifest and load it
195    let mut manifest = ScenarioManifest::new(
196        "api".to_string(),
197        "1.0.0".to_string(),
198        "API State Machine".to_string(),
199        "State machine created via API".to_string(),
200    );
201    manifest.state_machines.push(request.state_machine.clone());
202
203    if let Some(layout) = &request.visual_layout {
204        manifest
205            .state_machine_graphs
206            .insert(request.state_machine.resource_type.clone(), layout.clone());
207    }
208
209    if let Err(e) = manager.load_from_manifest(&manifest).await {
210        error!("Failed to load state machine: {}", e);
211        return Err(StatusCode::INTERNAL_SERVER_ERROR);
212    }
213
214    // Set visual layout if provided
215    let visual_layout = request.visual_layout.clone();
216    if let Some(layout) = &visual_layout {
217        manager
218            .set_visual_layout(&request.state_machine.resource_type, layout.clone())
219            .await;
220    }
221
222    // Broadcast WebSocket event
223    if let Some(ref ws_tx) = state.ws_broadcast {
224        let event = crate::management_ws::MockEvent::state_machine_updated(
225            request.state_machine.resource_type.clone(),
226            request.state_machine.clone(),
227        );
228        let _ = ws_tx.send(event);
229    }
230
231    Ok(Json(StateMachineResponse {
232        state_machine: request.state_machine,
233        visual_layout,
234    }))
235}
236
237/// Delete a state machine
238pub async fn delete_state_machine(
239    State(state): State<ManagementState>,
240    Path(resource_type): Path<String>,
241) -> Result<StatusCode, StatusCode> {
242    let mut manager = state.state_machine_manager.write().await;
243
244    // Delete the state machine
245    let deleted = manager.delete_state_machine(&resource_type).await;
246
247    if !deleted {
248        return Err(StatusCode::NOT_FOUND);
249    }
250
251    // Broadcast WebSocket event
252    if let Some(ref ws_tx) = state.ws_broadcast {
253        let event = crate::management_ws::MockEvent::state_machine_deleted(resource_type);
254        let _ = ws_tx.send(event);
255    }
256
257    Ok(StatusCode::NO_CONTENT)
258}
259
260/// List all state instances
261pub async fn list_instances(
262    State(state): State<ManagementState>,
263) -> Result<Json<StateInstanceListResponse>, StatusCode> {
264    let manager = state.state_machine_manager.read().await;
265
266    let instances = manager.list_instances().await;
267
268    let instance_responses: Vec<StateInstanceResponse> = instances
269        .iter()
270        .map(|i| StateInstanceResponse {
271            resource_id: i.resource_id.clone(),
272            current_state: i.current_state.clone(),
273            resource_type: i.resource_type.clone(),
274            history_count: i.state_history.len(),
275            state_data: i.state_data.clone(),
276        })
277        .collect();
278
279    Ok(Json(StateInstanceListResponse {
280        instances: instance_responses,
281        total: instances.len(),
282    }))
283}
284
285/// Get a state instance by resource ID
286pub async fn get_instance(
287    State(state): State<ManagementState>,
288    Path(resource_id): Path<String>,
289) -> Result<Json<StateInstanceResponse>, StatusCode> {
290    let manager = state.state_machine_manager.read().await;
291
292    let instance = manager.get_instance(&resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
293
294    Ok(Json(StateInstanceResponse {
295        resource_id: instance.resource_id,
296        current_state: instance.current_state,
297        resource_type: instance.resource_type,
298        history_count: instance.state_history.len(),
299        state_data: instance.state_data,
300    }))
301}
302
303/// Create a new state instance
304pub async fn create_instance(
305    State(state): State<ManagementState>,
306    Json(request): Json<CreateInstanceRequest>,
307) -> Result<Json<StateInstanceResponse>, StatusCode> {
308    let manager = state.state_machine_manager.write().await;
309
310    if let Err(e) = manager.create_instance(&request.resource_id, &request.resource_type).await {
311        error!("Failed to create instance: {}", e);
312        return Err(StatusCode::BAD_REQUEST);
313    }
314
315    let instance = manager
316        .get_instance(&request.resource_id)
317        .await
318        .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
319
320    // Broadcast WebSocket event
321    if let Some(ref ws_tx) = state.ws_broadcast {
322        let event = crate::management_ws::MockEvent::state_instance_created(
323            instance.resource_id.clone(),
324            instance.resource_type.clone(),
325            instance.current_state.clone(),
326        );
327        let _ = ws_tx.send(event);
328    }
329
330    Ok(Json(StateInstanceResponse {
331        resource_id: instance.resource_id,
332        current_state: instance.current_state,
333        resource_type: instance.resource_type,
334        history_count: instance.state_history.len(),
335        state_data: instance.state_data,
336    }))
337}
338
339/// Execute a state transition
340pub async fn execute_transition(
341    State(state): State<ManagementState>,
342    Json(request): Json<TransitionRequest>,
343) -> Result<Json<StateInstanceResponse>, StatusCode> {
344    let manager = state.state_machine_manager.write().await;
345
346    if let Err(e) = manager
347        .execute_transition(&request.resource_id, &request.to_state, request.context)
348        .await
349    {
350        error!("Failed to execute transition: {}", e);
351        return Err(StatusCode::BAD_REQUEST);
352    }
353
354    let instance = manager.get_instance(&request.resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
355
356    // Get the previous state from history if available
357    let from_state = instance
358        .state_history
359        .last()
360        .map(|h| h.from_state.clone())
361        .unwrap_or_else(|| instance.current_state.clone());
362
363    // Broadcast WebSocket event
364    if let Some(ref ws_tx) = state.ws_broadcast {
365        let event = crate::management_ws::MockEvent::state_transitioned(
366            instance.resource_id.clone(),
367            instance.resource_type.clone(),
368            from_state,
369            instance.current_state.clone(),
370            instance.state_data.clone(),
371        );
372        let _ = ws_tx.send(event);
373    }
374
375    Ok(Json(StateInstanceResponse {
376        resource_id: instance.resource_id,
377        current_state: instance.current_state,
378        resource_type: instance.resource_type,
379        history_count: instance.state_history.len(),
380        state_data: instance.state_data,
381    }))
382}
383
384/// Get next possible states for a resource
385pub async fn get_next_states(
386    State(state): State<ManagementState>,
387    Path(resource_id): Path<String>,
388) -> Result<Json<NextStatesResponse>, StatusCode> {
389    let manager = state.state_machine_manager.read().await;
390
391    let next_states =
392        manager.get_next_states(&resource_id).await.map_err(|_| StatusCode::NOT_FOUND)?;
393
394    Ok(Json(NextStatesResponse { next_states }))
395}
396
397/// Get current state of a resource
398pub async fn get_current_state(
399    State(state): State<ManagementState>,
400    Path(resource_id): Path<String>,
401) -> Result<Json<serde_json::Value>, StatusCode> {
402    let manager = state.state_machine_manager.read().await;
403
404    let current_state =
405        manager.get_current_state(&resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
406
407    Ok(Json(serde_json::json!({
408        "resource_id": resource_id,
409        "current_state": current_state
410    })))
411}
412
413/// Export state machines as JSON
414pub async fn export_state_machines(
415    State(state): State<ManagementState>,
416) -> Result<Json<ImportExportResponse>, StatusCode> {
417    let manager = state.state_machine_manager.read().await;
418
419    // Export all state machines and visual layouts
420    let (state_machines, visual_layouts) = manager.export_all().await;
421
422    Ok(Json(ImportExportResponse {
423        state_machines,
424        visual_layouts,
425    }))
426}
427
428/// Import state machines from JSON
429pub async fn import_state_machines(
430    State(state): State<ManagementState>,
431    Json(request): Json<ImportExportResponse>,
432) -> Result<StatusCode, StatusCode> {
433    let mut manager = state.state_machine_manager.write().await;
434
435    // Create a manifest from imported data
436    let mut manifest = ScenarioManifest::new(
437        "imported".to_string(),
438        "1.0.0".to_string(),
439        "Imported State Machines".to_string(),
440        "State machines imported via API".to_string(),
441    );
442    manifest.state_machines = request.state_machines.clone();
443    manifest.state_machine_graphs = request.visual_layouts.clone();
444
445    if let Err(e) = manager.load_from_manifest(&manifest).await {
446        error!("Failed to import state machines: {}", e);
447        return Err(StatusCode::BAD_REQUEST);
448    }
449
450    // Set visual layouts
451    for (resource_type, layout) in request.visual_layouts {
452        manager.set_visual_layout(&resource_type, layout).await;
453    }
454
455    Ok(StatusCode::CREATED)
456}
457
458/// Create the state machine API router
459///
460/// This function creates routes that use ManagementState, so they can be
461/// nested within the management router.
462pub fn create_state_machine_routes() -> axum::Router<ManagementState> {
463    use axum::routing::{delete, get, post, put};
464
465    axum::Router::new()
466        // State machine CRUD
467        .route("/", get(list_state_machines))
468        .route("/", post(create_state_machine))
469        .route("/:resource_type", get(get_state_machine))
470        .route("/:resource_type", put(create_state_machine))
471        .route("/:resource_type", delete(delete_state_machine))
472
473        // State instance operations
474        .route("/instances", get(list_instances))
475        .route("/instances", post(create_instance))
476        .route("/instances/:resource_id", get(get_instance))
477        .route("/instances/:resource_id/state", get(get_current_state))
478        .route("/instances/:resource_id/next-states", get(get_next_states))
479        .route("/instances/:resource_id/transition", post(execute_transition))
480
481        // Import/Export
482        .route("/export", get(export_state_machines))
483        .route("/import", post(import_state_machines))
484}