Skip to main content

mockforge_foundation/state_machine/
sub_scenario.rs

1//! Sub-scenario support for nested state machines
2//!
3//! Sub-scenarios allow state machines to reference and execute nested state machines,
4//! enabling composition of complex workflows from simpler building blocks.
5
6use super::rules::StateMachine;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Sub-scenario definition for nested state machine execution
11///
12/// A sub-scenario is a nested state machine that can be referenced from a parent
13/// state machine. It supports input/output mapping to pass data between parent
14/// and child state machines.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17pub struct SubScenario {
18    /// Unique identifier for this sub-scenario
19    pub id: String,
20
21    /// Human-readable name
22    pub name: String,
23
24    /// Nested state machine definition
25    pub state_machine: StateMachine,
26
27    /// Input mapping: maps parent state variables to sub-scenario input variables
28    ///
29    /// Example: `{"parent.status" => "sub.input.status"}`
30    #[serde(default)]
31    pub input_mapping: HashMap<String, String>,
32
33    /// Output mapping: maps sub-scenario output variables to parent state variables
34    ///
35    /// Example: `{"sub.output.result" => "parent.result"}`
36    #[serde(default)]
37    pub output_mapping: HashMap<String, String>,
38
39    /// Optional description
40    pub description: Option<String>,
41}
42
43impl SubScenario {
44    /// Create a new sub-scenario
45    pub fn new(
46        id: impl Into<String>,
47        name: impl Into<String>,
48        state_machine: StateMachine,
49    ) -> Self {
50        Self {
51            id: id.into(),
52            name: name.into(),
53            state_machine,
54            input_mapping: HashMap::new(),
55            output_mapping: HashMap::new(),
56            description: None,
57        }
58    }
59
60    /// Set description
61    pub fn with_description(mut self, description: impl Into<String>) -> Self {
62        self.description = Some(description.into());
63        self
64    }
65
66    /// Add an input mapping
67    ///
68    /// Maps a parent state variable to a sub-scenario input variable.
69    pub fn with_input_mapping(
70        mut self,
71        parent_var: impl Into<String>,
72        sub_var: impl Into<String>,
73    ) -> Self {
74        self.input_mapping.insert(parent_var.into(), sub_var.into());
75        self
76    }
77
78    /// Add an output mapping
79    ///
80    /// Maps a sub-scenario output variable to a parent state variable.
81    pub fn with_output_mapping(
82        mut self,
83        sub_var: impl Into<String>,
84        parent_var: impl Into<String>,
85    ) -> Self {
86        self.output_mapping.insert(sub_var.into(), parent_var.into());
87        self
88    }
89
90    /// Get the nested state machine
91    pub fn state_machine(&self) -> &StateMachine {
92        &self.state_machine
93    }
94
95    /// Get the nested state machine mutably
96    pub fn state_machine_mut(&mut self) -> &mut StateMachine {
97        &mut self.state_machine
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::state_machine::rules::{StateMachine, StateTransition};
105
106    #[test]
107    fn test_sub_scenario_creation() {
108        let nested_machine = StateMachine::new(
109            "sub_resource",
110            vec!["start".to_string(), "end".to_string()],
111            "start",
112        );
113
114        let sub_scenario = SubScenario::new("sub1", "Test Sub-Scenario", nested_machine)
115            .with_description("A test sub-scenario")
116            .with_input_mapping("parent.status", "sub.input.status")
117            .with_output_mapping("sub.output.result", "parent.result");
118
119        assert_eq!(sub_scenario.id, "sub1");
120        assert_eq!(sub_scenario.name, "Test Sub-Scenario");
121        assert_eq!(sub_scenario.input_mapping.len(), 1);
122        assert_eq!(sub_scenario.output_mapping.len(), 1);
123        assert_eq!(
124            sub_scenario.input_mapping.get("parent.status"),
125            Some(&"sub.input.status".to_string())
126        );
127    }
128
129    #[test]
130    fn test_sub_scenario_serialization() {
131        let nested_machine = StateMachine::new(
132            "sub_resource",
133            vec!["start".to_string(), "end".to_string()],
134            "start",
135        )
136        .add_transition(StateTransition::new("start", "end"));
137
138        let sub_scenario = SubScenario::new("sub1", "Test", nested_machine)
139            .with_input_mapping("parent.x", "sub.x");
140
141        let json = serde_json::to_string(&sub_scenario).unwrap();
142        let deserialized: SubScenario = serde_json::from_str(&json).unwrap();
143
144        assert_eq!(deserialized.id, "sub1");
145        assert_eq!(deserialized.input_mapping.len(), 1);
146    }
147}