Skip to main content

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, State},
8    http::StatusCode,
9    Json,
10};
11use mockforge_core::intelligent_behavior::{rules::StateMachine, visual_layout::VisualLayout};
12use mockforge_scenarios::ScenarioManifest;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::HashMap;
16use tracing::error;
17
18// Re-export ManagementState for use in handlers
19use crate::management::ManagementState;
20
21// ===== Request/Response Types =====
22
23/// Request to create or update a state machine
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct StateMachineRequest {
26    /// State machine definition
27    pub state_machine: StateMachine,
28    /// Optional visual layout
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub visual_layout: Option<VisualLayout>,
31}
32
33/// Request to execute a state transition
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct TransitionRequest {
36    /// Resource ID to transition
37    pub resource_id: String,
38    /// Target state
39    pub to_state: String,
40    /// Optional context variables for condition evaluation
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub context: Option<HashMap<String, Value>>,
43}
44
45/// Request to create a state instance
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct CreateInstanceRequest {
48    /// Resource ID
49    pub resource_id: String,
50    /// Resource type (must match a state machine resource_type)
51    pub resource_type: String,
52}
53
54/// Response for state machine operations
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct StateMachineResponse {
57    /// State machine definition
58    pub state_machine: StateMachine,
59    /// Optional visual layout
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub visual_layout: Option<VisualLayout>,
62}
63
64/// Response for listing state machines
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct StateMachineListResponse {
67    /// List of state machines
68    pub state_machines: Vec<StateMachineInfo>,
69    /// Total count
70    pub total: usize,
71}
72
73/// Information about a state machine
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct StateMachineInfo {
76    /// Resource type
77    pub resource_type: String,
78    /// Number of states
79    pub state_count: usize,
80    /// Number of transitions
81    pub transition_count: usize,
82    /// Number of sub-scenarios
83    pub sub_scenario_count: usize,
84    /// Whether it has a visual layout
85    pub has_visual_layout: bool,
86}
87
88/// Response for state instance operations
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct StateInstanceResponse {
91    /// Resource ID
92    pub resource_id: String,
93    /// Current state
94    pub current_state: String,
95    /// Resource type
96    pub resource_type: String,
97    /// State history count
98    pub history_count: usize,
99    /// State data
100    pub state_data: HashMap<String, Value>,
101}
102
103/// Response for listing state instances
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct StateInstanceListResponse {
106    /// List of instances
107    pub instances: Vec<StateInstanceResponse>,
108    /// Total count
109    pub total: usize,
110}
111
112/// Response for next states query
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct NextStatesResponse {
115    /// List of possible next states
116    pub next_states: Vec<String>,
117}
118
119/// Response for import/export operations
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ImportExportResponse {
122    /// State machines
123    pub state_machines: Vec<StateMachine>,
124    /// Visual layouts by resource type
125    pub visual_layouts: HashMap<String, VisualLayout>,
126}
127
128// ===== Handlers =====
129
130/// List all state machines
131pub async fn list_state_machines(
132    State(state): State<ManagementState>,
133) -> Result<Json<StateMachineListResponse>, StatusCode> {
134    let manager = state.state_machine_manager.read().await;
135
136    // Get all state machines
137    let machines = manager.list_state_machines().await;
138
139    // Check visual layouts separately for each state machine
140    // We need to check if a visual layout exists for each state machine
141    let mut state_machine_list = Vec::new();
142    for (resource_type, sm) in machines.iter() {
143        let has_visual_layout = manager.get_visual_layout(resource_type).await.is_some();
144        state_machine_list.push(StateMachineInfo {
145            resource_type: resource_type.clone(),
146            state_count: sm.states.len(),
147            transition_count: sm.transitions.len(),
148            sub_scenario_count: sm.sub_scenarios.len(),
149            has_visual_layout,
150        });
151    }
152
153    Ok(Json(StateMachineListResponse {
154        state_machines: state_machine_list.clone(),
155        total: state_machine_list.len(),
156    }))
157}
158
159/// Get a state machine by resource type
160pub async fn get_state_machine(
161    State(state): State<ManagementState>,
162    Path(resource_type): Path<String>,
163) -> Result<Json<StateMachineResponse>, StatusCode> {
164    let manager = state.state_machine_manager.read().await;
165
166    let state_machine =
167        manager.get_state_machine(&resource_type).await.ok_or(StatusCode::NOT_FOUND)?;
168
169    let visual_layout = manager.get_visual_layout(&resource_type).await;
170
171    // Convert types from mockforge-scenarios' dependency version to local version
172    // by serializing and deserializing through JSON
173    let state_machine_json = serde_json::to_value(&state_machine).map_err(|e| {
174        tracing::error!("JSON serialization error: {}", e);
175        StatusCode::INTERNAL_SERVER_ERROR
176    })?;
177    let state_machine: StateMachine = serde_json::from_value(state_machine_json).map_err(|e| {
178        tracing::error!("JSON serialization error: {}", e);
179        StatusCode::INTERNAL_SERVER_ERROR
180    })?;
181
182    let visual_layout: Option<VisualLayout> = visual_layout
183        .map(|layout| {
184            let layout_json = serde_json::to_value(&layout).map_err(|e| {
185                tracing::error!("JSON serialization error: {}", e);
186                StatusCode::INTERNAL_SERVER_ERROR
187            })?;
188            serde_json::from_value(layout_json).map_err(|e| {
189                tracing::error!("JSON deserialization error: {}", e);
190                StatusCode::INTERNAL_SERVER_ERROR
191            })
192        })
193        .transpose()
194        .map_err(|e| {
195            tracing::error!("JSON serialization error: {}", e);
196            StatusCode::INTERNAL_SERVER_ERROR
197        })?;
198
199    Ok(Json(StateMachineResponse {
200        state_machine,
201        visual_layout,
202    }))
203}
204
205/// Create or update a state machine
206pub async fn create_state_machine(
207    State(state): State<ManagementState>,
208    Json(request): Json<StateMachineRequest>,
209) -> Result<Json<StateMachineResponse>, StatusCode> {
210    let manager = state.state_machine_manager.write().await;
211
212    // Convert types from local version to mockforge-scenarios' dependency version
213    // by serializing and deserializing through JSON
214    // The ScenarioManifest uses types from mockforge-scenarios' mockforge-core dependency (0.2.9)
215    // We need to convert our local StateMachine to that version
216    let state_machine_json = serde_json::to_value(&request.state_machine).map_err(|e| {
217        tracing::error!("JSON serialization error: {}", e);
218        StatusCode::INTERNAL_SERVER_ERROR
219    })?;
220
221    // Create manifest with JSON values - serde will deserialize into the correct types
222    // We need to provide all required fields for ScenarioManifest
223    let mut manifest_json = serde_json::json!({
224        "manifest_version": "1.0",
225        "name": "api",
226        "version": "1.0.0",
227        "title": "API State Machine",
228        "description": "State machine created via API",
229        "author": "api",
230        "category": "other",
231        "compatibility": {
232            "min_version": "0.1.0",
233            "max_version": null
234        },
235        "files": [],
236        "state_machines": [state_machine_json],
237        "state_machine_graphs": {}
238    });
239
240    if let Some(layout) = &request.visual_layout {
241        let layout_json = serde_json::to_value(layout).map_err(|e| {
242            tracing::error!("JSON serialization error: {}", e);
243            StatusCode::INTERNAL_SERVER_ERROR
244        })?;
245        manifest_json["state_machine_graphs"][&request.state_machine.resource_type] = layout_json;
246    }
247
248    let manifest: ScenarioManifest = serde_json::from_value(manifest_json).map_err(|e| {
249        tracing::error!("JSON serialization error: {}", e);
250        StatusCode::INTERNAL_SERVER_ERROR
251    })?;
252
253    // Validate the first state machine from manifest
254    if let Some(sm) = manifest.state_machines.first() {
255        if let Err(e) = manager.validate_state_machine(sm) {
256            error!("Invalid state machine: {}", e);
257            return Err(StatusCode::BAD_REQUEST);
258        }
259    }
260
261    if let Err(e) = manager.load_from_manifest(&manifest).await {
262        error!("Failed to load state machine: {}", e);
263        return Err(StatusCode::INTERNAL_SERVER_ERROR);
264    }
265
266    // Visual layout is already set in the manifest, no need to set separately
267
268    // Broadcast WebSocket event
269    if let Some(ref ws_tx) = state.ws_broadcast {
270        let event = crate::management_ws::MockEvent::state_machine_updated(
271            request.state_machine.resource_type.clone(),
272            request.state_machine.clone(),
273        );
274        let _ = ws_tx.send(event);
275    }
276
277    // Get state machine and layout back after loading (returns version from mockforge-scenarios' dependency)
278    let state_machine_from_manager = manager
279        .get_state_machine(&request.state_machine.resource_type)
280        .await
281        .ok_or(StatusCode::NOT_FOUND)?;
282    let visual_layout_from_manager =
283        manager.get_visual_layout(&request.state_machine.resource_type).await;
284
285    // Convert back to local types
286    let state_machine_json = serde_json::to_value(&state_machine_from_manager).map_err(|e| {
287        tracing::error!("JSON serialization error: {}", e);
288        StatusCode::INTERNAL_SERVER_ERROR
289    })?;
290    let state_machine: StateMachine = serde_json::from_value(state_machine_json).map_err(|e| {
291        tracing::error!("JSON serialization error: {}", e);
292        StatusCode::INTERNAL_SERVER_ERROR
293    })?;
294
295    let visual_layout: Option<VisualLayout> = visual_layout_from_manager
296        .map(|layout| {
297            let layout_json = serde_json::to_value(&layout).map_err(|e| {
298                tracing::error!("JSON serialization error: {}", e);
299                StatusCode::INTERNAL_SERVER_ERROR
300            })?;
301            serde_json::from_value(layout_json).map_err(|e| {
302                tracing::error!("JSON deserialization error: {}", e);
303                StatusCode::INTERNAL_SERVER_ERROR
304            })
305        })
306        .transpose()
307        .map_err(|e| {
308            tracing::error!("JSON serialization error: {}", e);
309            StatusCode::INTERNAL_SERVER_ERROR
310        })?;
311
312    Ok(Json(StateMachineResponse {
313        state_machine,
314        visual_layout,
315    }))
316}
317
318/// Delete a state machine
319pub async fn delete_state_machine(
320    State(state): State<ManagementState>,
321    Path(resource_type): Path<String>,
322) -> Result<StatusCode, StatusCode> {
323    let manager = state.state_machine_manager.write().await;
324
325    // Delete the state machine
326    let deleted = manager.delete_state_machine(&resource_type).await;
327
328    if !deleted {
329        return Err(StatusCode::NOT_FOUND);
330    }
331
332    // Broadcast WebSocket event
333    if let Some(ref ws_tx) = state.ws_broadcast {
334        let event = crate::management_ws::MockEvent::state_machine_deleted(resource_type);
335        let _ = ws_tx.send(event);
336    }
337
338    Ok(StatusCode::NO_CONTENT)
339}
340
341/// List all state instances
342pub async fn list_instances(
343    State(state): State<ManagementState>,
344) -> Result<Json<StateInstanceListResponse>, StatusCode> {
345    let manager = state.state_machine_manager.read().await;
346
347    let instances = manager.list_instances().await;
348
349    let instance_responses: Vec<StateInstanceResponse> = instances
350        .iter()
351        .map(|i| StateInstanceResponse {
352            resource_id: i.resource_id.clone(),
353            current_state: i.current_state.clone(),
354            resource_type: i.resource_type.clone(),
355            history_count: i.state_history.len(),
356            state_data: i.state_data.clone(),
357        })
358        .collect();
359
360    Ok(Json(StateInstanceListResponse {
361        instances: instance_responses,
362        total: instances.len(),
363    }))
364}
365
366/// Get a state instance by resource ID
367pub async fn get_instance(
368    State(state): State<ManagementState>,
369    Path(resource_id): Path<String>,
370) -> Result<Json<StateInstanceResponse>, StatusCode> {
371    let manager = state.state_machine_manager.read().await;
372
373    let instance = manager.get_instance(&resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
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/// Create a new state instance
385pub async fn create_instance(
386    State(state): State<ManagementState>,
387    Json(request): Json<CreateInstanceRequest>,
388) -> Result<Json<StateInstanceResponse>, StatusCode> {
389    let manager = state.state_machine_manager.write().await;
390
391    if let Err(e) = manager.create_instance(&request.resource_id, &request.resource_type).await {
392        error!("Failed to create instance: {}", e);
393        return Err(StatusCode::BAD_REQUEST);
394    }
395
396    let instance = manager
397        .get_instance(&request.resource_id)
398        .await
399        .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
400
401    // Broadcast WebSocket event
402    if let Some(ref ws_tx) = state.ws_broadcast {
403        let event = crate::management_ws::MockEvent::state_instance_created(
404            instance.resource_id.clone(),
405            instance.resource_type.clone(),
406            instance.current_state.clone(),
407        );
408        let _ = ws_tx.send(event);
409    }
410
411    Ok(Json(StateInstanceResponse {
412        resource_id: instance.resource_id,
413        current_state: instance.current_state,
414        resource_type: instance.resource_type,
415        history_count: instance.state_history.len(),
416        state_data: instance.state_data,
417    }))
418}
419
420/// Execute a state transition
421pub async fn execute_transition(
422    State(state): State<ManagementState>,
423    Json(request): Json<TransitionRequest>,
424) -> Result<Json<StateInstanceResponse>, StatusCode> {
425    let manager = state.state_machine_manager.write().await;
426
427    if let Err(e) = manager
428        .execute_transition(&request.resource_id, &request.to_state, request.context)
429        .await
430    {
431        error!("Failed to execute transition: {}", e);
432        return Err(StatusCode::BAD_REQUEST);
433    }
434
435    let instance = manager.get_instance(&request.resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
436
437    // Get the previous state from history if available
438    let from_state = instance
439        .state_history
440        .last()
441        .map(|h| h.from_state.clone())
442        .unwrap_or_else(|| instance.current_state.clone());
443
444    // Broadcast WebSocket event
445    if let Some(ref ws_tx) = state.ws_broadcast {
446        let event = crate::management_ws::MockEvent::state_transitioned(
447            instance.resource_id.clone(),
448            instance.resource_type.clone(),
449            from_state,
450            instance.current_state.clone(),
451            instance.state_data.clone(),
452        );
453        let _ = ws_tx.send(event);
454    }
455
456    Ok(Json(StateInstanceResponse {
457        resource_id: instance.resource_id,
458        current_state: instance.current_state,
459        resource_type: instance.resource_type,
460        history_count: instance.state_history.len(),
461        state_data: instance.state_data,
462    }))
463}
464
465/// Get next possible states for a resource
466pub async fn get_next_states(
467    State(state): State<ManagementState>,
468    Path(resource_id): Path<String>,
469) -> Result<Json<NextStatesResponse>, StatusCode> {
470    let manager = state.state_machine_manager.read().await;
471
472    let next_states = manager.get_next_states(&resource_id).await.map_err(|e| {
473        tracing::warn!("Failed to get next states for {}: {}", resource_id, e);
474        StatusCode::NOT_FOUND
475    })?;
476
477    Ok(Json(NextStatesResponse { next_states }))
478}
479
480/// Get current state of a resource
481pub async fn get_current_state(
482    State(state): State<ManagementState>,
483    Path(resource_id): Path<String>,
484) -> Result<Json<Value>, StatusCode> {
485    let manager = state.state_machine_manager.read().await;
486
487    let current_state =
488        manager.get_current_state(&resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
489
490    Ok(Json(serde_json::json!({
491        "resource_id": resource_id,
492        "current_state": current_state
493    })))
494}
495
496/// Export state machines as JSON
497pub async fn export_state_machines(
498    State(state): State<ManagementState>,
499) -> Result<Json<ImportExportResponse>, StatusCode> {
500    let manager = state.state_machine_manager.read().await;
501
502    // Export all state machines and visual layouts (returns versions from mockforge-scenarios' dependency)
503    let (state_machines_from_manager, visual_layouts_from_manager) = manager.export_all().await;
504
505    // Convert to local types by serializing and deserializing
506    let state_machines: Vec<StateMachine> = state_machines_from_manager
507        .into_iter()
508        .map(|sm| {
509            let json = serde_json::to_value(&sm).map_err(|e| {
510                tracing::error!("JSON serialization error: {}", e);
511                StatusCode::INTERNAL_SERVER_ERROR
512            })?;
513            serde_json::from_value(json).map_err(|e| {
514                tracing::error!("JSON deserialization error: {}", e);
515                StatusCode::INTERNAL_SERVER_ERROR
516            })
517        })
518        .collect::<Result<Vec<_>, StatusCode>>()?;
519
520    let visual_layouts: HashMap<String, VisualLayout> = visual_layouts_from_manager
521        .into_iter()
522        .map(|(k, v)| {
523            let json = serde_json::to_value(&v).map_err(|e| {
524                tracing::error!("JSON serialization error: {}", e);
525                StatusCode::INTERNAL_SERVER_ERROR
526            })?;
527            let layout: VisualLayout = serde_json::from_value(json).map_err(|e| {
528                tracing::error!("JSON serialization error: {}", e);
529                StatusCode::INTERNAL_SERVER_ERROR
530            })?;
531            Ok((k, layout))
532        })
533        .collect::<Result<HashMap<_, _>, StatusCode>>()?;
534
535    Ok(Json(ImportExportResponse {
536        state_machines,
537        visual_layouts,
538    }))
539}
540
541/// Import state machines from JSON
542pub async fn import_state_machines(
543    State(state): State<ManagementState>,
544    Json(request): Json<ImportExportResponse>,
545) -> Result<StatusCode, StatusCode> {
546    let manager = state.state_machine_manager.write().await;
547
548    // Create manifest from JSON to let serde handle type conversion
549    // We need to provide all required fields for ScenarioManifest
550    let manifest_json = serde_json::json!({
551        "manifest_version": "1.0",
552        "name": "imported",
553        "version": "1.0.0",
554        "title": "Imported State Machines",
555        "description": "State machines imported via API",
556        "author": "api",
557        "category": "other",
558        "compatibility": {
559            "min_version": "0.1.0",
560            "max_version": null
561        },
562        "files": [],
563        "state_machines": request.state_machines,
564        "state_machine_graphs": request.visual_layouts
565    });
566
567    let manifest: ScenarioManifest = serde_json::from_value(manifest_json).map_err(|e| {
568        tracing::error!("JSON serialization error: {}", e);
569        StatusCode::INTERNAL_SERVER_ERROR
570    })?;
571
572    if let Err(e) = manager.load_from_manifest(&manifest).await {
573        error!("Failed to import state machines: {}", e);
574        return Err(StatusCode::BAD_REQUEST);
575    }
576
577    // Visual layouts are already set in the manifest, no need to set separately
578
579    Ok(StatusCode::CREATED)
580}
581
582/// Create the state machine API router
583///
584/// This function creates routes that use ManagementState, so they can be
585/// nested within the management router.
586pub fn create_state_machine_routes() -> axum::Router<ManagementState> {
587    use axum::{
588        routing::{delete, get, post, put},
589        Router,
590    };
591
592    Router::new()
593        // State machine CRUD
594        .route("/", get(list_state_machines))
595        .route("/", post(create_state_machine))
596        .route("/{resource_type}", get(get_state_machine))
597        .route("/{resource_type}", put(create_state_machine))
598        .route("/{resource_type}", delete(delete_state_machine))
599
600        // State instance operations
601        .route("/instances", get(list_instances))
602        .route("/instances", post(create_instance))
603        .route("/instances/{resource_id}", get(get_instance))
604        .route("/instances/{resource_id}/state", get(get_current_state))
605        .route("/instances/{resource_id}/next-states", get(get_next_states))
606        .route("/instances/{resource_id}/transition", post(execute_transition))
607
608        // Import/Export
609        .route("/export", get(export_state_machines))
610        .route("/import", post(import_state_machines))
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616    use std::collections::HashMap;
617
618    fn create_test_state_machine() -> StateMachine {
619        StateMachine::new(
620            "test-resource",
621            vec!["pending".to_string(), "active".to_string()],
622            "pending",
623        )
624    }
625
626    // StateMachineRequest tests
627    #[test]
628    fn test_state_machine_request_serialize() {
629        let request = StateMachineRequest {
630            state_machine: create_test_state_machine(),
631            visual_layout: None,
632        };
633        let json = serde_json::to_string(&request);
634        assert!(json.is_ok());
635    }
636
637    #[test]
638    fn test_state_machine_request_deserialize() {
639        let json = r#"{"state_machine":{"resource_type":"test","states":["a","b"],"initial_state":"a","transitions":[],"sub_scenarios":[]}}"#;
640        let result: Result<StateMachineRequest, _> = serde_json::from_str(json);
641        assert!(result.is_ok());
642        assert!(result.unwrap().visual_layout.is_none());
643    }
644
645    #[test]
646    fn test_state_machine_request_clone() {
647        let request = StateMachineRequest {
648            state_machine: create_test_state_machine(),
649            visual_layout: None,
650        };
651        let cloned = request.clone();
652        assert!(cloned.visual_layout.is_none());
653    }
654
655    #[test]
656    fn test_state_machine_request_debug() {
657        let request = StateMachineRequest {
658            state_machine: create_test_state_machine(),
659            visual_layout: None,
660        };
661        let debug = format!("{:?}", request);
662        assert!(debug.contains("StateMachineRequest"));
663    }
664
665    // TransitionRequest tests
666    #[test]
667    fn test_transition_request_new() {
668        let request = TransitionRequest {
669            resource_id: "order-123".to_string(),
670            to_state: "shipped".to_string(),
671            context: None,
672        };
673        assert_eq!(request.resource_id, "order-123");
674        assert_eq!(request.to_state, "shipped");
675        assert!(request.context.is_none());
676    }
677
678    #[test]
679    fn test_transition_request_with_context() {
680        let mut context = HashMap::new();
681        context.insert("priority".to_string(), serde_json::json!("high"));
682
683        let request = TransitionRequest {
684            resource_id: "order-123".to_string(),
685            to_state: "shipped".to_string(),
686            context: Some(context),
687        };
688        assert!(request.context.is_some());
689        assert_eq!(request.context.unwrap().get("priority"), Some(&serde_json::json!("high")));
690    }
691
692    #[test]
693    fn test_transition_request_serialize() {
694        let request = TransitionRequest {
695            resource_id: "test".to_string(),
696            to_state: "active".to_string(),
697            context: None,
698        };
699        let json = serde_json::to_string(&request).unwrap();
700        assert!(json.contains("resource_id"));
701        assert!(json.contains("to_state"));
702        assert!(!json.contains("context")); // skip_serializing_if removes None
703    }
704
705    #[test]
706    fn test_transition_request_deserialize() {
707        let json = r#"{"resource_id":"test","to_state":"active"}"#;
708        let request: TransitionRequest = serde_json::from_str(json).unwrap();
709        assert_eq!(request.resource_id, "test");
710        assert_eq!(request.to_state, "active");
711    }
712
713    // CreateInstanceRequest tests
714    #[test]
715    fn test_create_instance_request_new() {
716        let request = CreateInstanceRequest {
717            resource_id: "order-456".to_string(),
718            resource_type: "order".to_string(),
719        };
720        assert_eq!(request.resource_id, "order-456");
721        assert_eq!(request.resource_type, "order");
722    }
723
724    #[test]
725    fn test_create_instance_request_serialize() {
726        let request = CreateInstanceRequest {
727            resource_id: "test-id".to_string(),
728            resource_type: "test-type".to_string(),
729        };
730        let json = serde_json::to_string(&request).unwrap();
731        assert!(json.contains("test-id"));
732        assert!(json.contains("test-type"));
733    }
734
735    #[test]
736    fn test_create_instance_request_deserialize() {
737        let json = r#"{"resource_id":"id-1","resource_type":"type-1"}"#;
738        let request: CreateInstanceRequest = serde_json::from_str(json).unwrap();
739        assert_eq!(request.resource_id, "id-1");
740        assert_eq!(request.resource_type, "type-1");
741    }
742
743    // StateMachineResponse tests
744    #[test]
745    fn test_state_machine_response_without_layout() {
746        let response = StateMachineResponse {
747            state_machine: create_test_state_machine(),
748            visual_layout: None,
749        };
750        let json = serde_json::to_string(&response).unwrap();
751        assert!(!json.contains("visual_layout")); // skip_serializing_if
752    }
753
754    #[test]
755    fn test_state_machine_response_clone() {
756        let response = StateMachineResponse {
757            state_machine: create_test_state_machine(),
758            visual_layout: None,
759        };
760        let cloned = response.clone();
761        assert!(cloned.visual_layout.is_none());
762    }
763
764    // StateMachineListResponse tests
765    #[test]
766    fn test_state_machine_list_response_empty() {
767        let response = StateMachineListResponse {
768            state_machines: vec![],
769            total: 0,
770        };
771        assert_eq!(response.total, 0);
772        assert!(response.state_machines.is_empty());
773    }
774
775    #[test]
776    fn test_state_machine_list_response_with_items() {
777        let info = StateMachineInfo {
778            resource_type: "order".to_string(),
779            state_count: 5,
780            transition_count: 10,
781            sub_scenario_count: 2,
782            has_visual_layout: true,
783        };
784        let response = StateMachineListResponse {
785            state_machines: vec![info],
786            total: 1,
787        };
788        assert_eq!(response.total, 1);
789        assert_eq!(response.state_machines[0].resource_type, "order");
790    }
791
792    #[test]
793    fn test_state_machine_list_response_serialize() {
794        let response = StateMachineListResponse {
795            state_machines: vec![],
796            total: 0,
797        };
798        let json = serde_json::to_string(&response).unwrap();
799        assert!(json.contains("state_machines"));
800        assert!(json.contains("total"));
801    }
802
803    // StateMachineInfo tests
804    #[test]
805    fn test_state_machine_info_new() {
806        let info = StateMachineInfo {
807            resource_type: "user".to_string(),
808            state_count: 3,
809            transition_count: 5,
810            sub_scenario_count: 1,
811            has_visual_layout: false,
812        };
813        assert_eq!(info.resource_type, "user");
814        assert_eq!(info.state_count, 3);
815        assert_eq!(info.transition_count, 5);
816        assert_eq!(info.sub_scenario_count, 1);
817        assert!(!info.has_visual_layout);
818    }
819
820    #[test]
821    fn test_state_machine_info_clone() {
822        let info = StateMachineInfo {
823            resource_type: "product".to_string(),
824            state_count: 4,
825            transition_count: 8,
826            sub_scenario_count: 0,
827            has_visual_layout: true,
828        };
829        let cloned = info.clone();
830        assert_eq!(info.resource_type, cloned.resource_type);
831        assert_eq!(info.state_count, cloned.state_count);
832    }
833
834    #[test]
835    fn test_state_machine_info_serialize() {
836        let info = StateMachineInfo {
837            resource_type: "item".to_string(),
838            state_count: 2,
839            transition_count: 3,
840            sub_scenario_count: 0,
841            has_visual_layout: false,
842        };
843        let json = serde_json::to_string(&info).unwrap();
844        assert!(json.contains("\"resource_type\":\"item\""));
845        assert!(json.contains("\"state_count\":2"));
846    }
847
848    // StateInstanceResponse tests
849    #[test]
850    fn test_state_instance_response_new() {
851        let response = StateInstanceResponse {
852            resource_id: "order-1".to_string(),
853            current_state: "pending".to_string(),
854            resource_type: "order".to_string(),
855            history_count: 0,
856            state_data: HashMap::new(),
857        };
858        assert_eq!(response.resource_id, "order-1");
859        assert_eq!(response.current_state, "pending");
860        assert_eq!(response.history_count, 0);
861    }
862
863    #[test]
864    fn test_state_instance_response_with_data() {
865        let mut state_data = HashMap::new();
866        state_data.insert("total".to_string(), serde_json::json!(100.50));
867
868        let response = StateInstanceResponse {
869            resource_id: "order-2".to_string(),
870            current_state: "confirmed".to_string(),
871            resource_type: "order".to_string(),
872            history_count: 3,
873            state_data,
874        };
875        assert_eq!(response.history_count, 3);
876        assert!(response.state_data.contains_key("total"));
877    }
878
879    #[test]
880    fn test_state_instance_response_serialize() {
881        let response = StateInstanceResponse {
882            resource_id: "test".to_string(),
883            current_state: "active".to_string(),
884            resource_type: "resource".to_string(),
885            history_count: 5,
886            state_data: HashMap::new(),
887        };
888        let json = serde_json::to_string(&response).unwrap();
889        assert!(json.contains("resource_id"));
890        assert!(json.contains("current_state"));
891        assert!(json.contains("history_count"));
892    }
893
894    // StateInstanceListResponse tests
895    #[test]
896    fn test_state_instance_list_response_empty() {
897        let response = StateInstanceListResponse {
898            instances: vec![],
899            total: 0,
900        };
901        assert_eq!(response.total, 0);
902        assert!(response.instances.is_empty());
903    }
904
905    #[test]
906    fn test_state_instance_list_response_with_instances() {
907        let instance = StateInstanceResponse {
908            resource_id: "inst-1".to_string(),
909            current_state: "ready".to_string(),
910            resource_type: "service".to_string(),
911            history_count: 2,
912            state_data: HashMap::new(),
913        };
914        let response = StateInstanceListResponse {
915            instances: vec![instance],
916            total: 1,
917        };
918        assert_eq!(response.total, 1);
919    }
920
921    // NextStatesResponse tests
922    #[test]
923    fn test_next_states_response_empty() {
924        let response = NextStatesResponse {
925            next_states: vec![],
926        };
927        assert!(response.next_states.is_empty());
928    }
929
930    #[test]
931    fn test_next_states_response_with_states() {
932        let response = NextStatesResponse {
933            next_states: vec!["shipped".to_string(), "cancelled".to_string()],
934        };
935        assert_eq!(response.next_states.len(), 2);
936        assert!(response.next_states.contains(&"shipped".to_string()));
937    }
938
939    #[test]
940    fn test_next_states_response_serialize() {
941        let response = NextStatesResponse {
942            next_states: vec!["state1".to_string(), "state2".to_string()],
943        };
944        let json = serde_json::to_string(&response).unwrap();
945        assert!(json.contains("state1"));
946        assert!(json.contains("state2"));
947    }
948
949    // ImportExportResponse tests
950    #[test]
951    fn test_import_export_response_empty() {
952        let response = ImportExportResponse {
953            state_machines: vec![],
954            visual_layouts: HashMap::new(),
955        };
956        assert!(response.state_machines.is_empty());
957        assert!(response.visual_layouts.is_empty());
958    }
959
960    #[test]
961    fn test_import_export_response_serialize() {
962        let response = ImportExportResponse {
963            state_machines: vec![],
964            visual_layouts: HashMap::new(),
965        };
966        let json = serde_json::to_string(&response).unwrap();
967        assert!(json.contains("state_machines"));
968        assert!(json.contains("visual_layouts"));
969    }
970
971    #[test]
972    fn test_import_export_response_deserialize() {
973        let json = r#"{"state_machines":[],"visual_layouts":{}}"#;
974        let response: ImportExportResponse = serde_json::from_str(json).unwrap();
975        assert!(response.state_machines.is_empty());
976    }
977
978    // Router tests
979    #[test]
980    fn test_create_state_machine_routes() {
981        let router = create_state_machine_routes();
982        // Just verify it creates without panicking
983        let _ = format!("{:?}", router);
984    }
985}