Skip to main content

scxml/
flatten.rs

1use compact_str::CompactString;
2use serde::{Deserialize, Serialize};
3
4use crate::model::state::StateKind;
5use crate::model::{State, Statechart};
6
7/// A flat state representation for frontend rendering.
8///
9/// Matches the shape expected by statechart visualization components
10/// (e.g. `scxmlTypes.ts::flattenMachine()`).
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[cfg_attr(
13    feature = "rkyv",
14    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
15)]
16pub struct FlatState {
17    /// State identifier.
18    pub id: CompactString,
19    /// What kind of state this is.
20    pub kind: StateKind,
21    /// Parent state id, if nested.
22    pub parent: Option<CompactString>,
23    /// Whether this is the chart's initial state.
24    pub initial: bool,
25    /// Nesting depth (0 = top level).
26    pub depth: u32,
27}
28
29/// A flat transition representation for frontend rendering.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[cfg_attr(
32    feature = "rkyv",
33    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
34)]
35pub struct FlatTransition {
36    /// Source state id.
37    pub source: CompactString,
38    /// Target state id.
39    pub target: CompactString,
40    /// Triggering event, if any.
41    pub event: Option<CompactString>,
42    /// Guard condition name, if any.
43    pub guard: Option<CompactString>,
44}
45
46/// Flatten a statechart into lists of states and transitions suitable for
47/// frontend rendering.
48///
49/// ```rust
50/// use scxml::{parse_xml, flatten};
51///
52/// let xml = r#"
53///     <scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="a">
54///         <state id="a"><transition event="go" target="b"/></state>
55///         <final id="b"/>
56///     </scxml>
57/// "#;
58/// let chart = parse_xml(xml).unwrap();
59/// let (states, transitions) = flatten(&chart);
60/// assert_eq!(states.len(), 2);
61/// assert_eq!(transitions.len(), 1);
62/// ```
63///
64/// Walks the state tree depth-first, producing flat lists with parent
65/// references and depth indicators. Transitions are denormalized to
66/// source→target pairs (one per target for multi-target transitions).
67pub fn flatten(chart: &Statechart) -> (Vec<FlatState>, Vec<FlatTransition>) {
68    let (state_count, trans_count) = {
69        let mut sc = 0;
70        let mut tc = 0;
71        for s in chart.iter_all_states() {
72            sc += 1;
73            tc += s.transitions.len();
74        }
75        (sc, tc)
76    };
77    let mut states = Vec::with_capacity(state_count);
78    let mut transitions = Vec::with_capacity(trans_count);
79
80    let limit = crate::max_depth();
81    for state in &chart.states {
82        flatten_state(
83            state,
84            None,
85            0,
86            &chart.initial,
87            &mut states,
88            &mut transitions,
89            limit,
90        );
91    }
92
93    (states, transitions)
94}
95
96fn flatten_state(
97    state: &State,
98    parent: Option<&CompactString>,
99    depth: u32,
100    chart_initial: &CompactString,
101    states: &mut Vec<FlatState>,
102    transitions: &mut Vec<FlatTransition>,
103    limit: usize,
104) {
105    if depth as usize > limit {
106        return;
107    }
108    let is_initial = state.id == *chart_initial;
109
110    states.push(FlatState {
111        id: state.id.clone(),
112        kind: state.kind,
113        parent: parent.cloned(),
114        initial: is_initial,
115        depth,
116    });
117
118    // Flatten transitions.
119    for t in &state.transitions {
120        for target in &t.targets {
121            transitions.push(FlatTransition {
122                source: state.id.clone(),
123                target: target.clone(),
124                event: t.event.clone(),
125                guard: t.guard.clone(),
126            });
127        }
128    }
129
130    // Recurse into children.
131    for child in &state.children {
132        flatten_state(
133            child,
134            Some(&state.id),
135            depth + 1,
136            chart_initial,
137            states,
138            transitions,
139            limit,
140        );
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::model::{State, Transition};
148
149    #[test]
150    fn flatten_simple_chart() {
151        let chart = Statechart::new(
152            "a",
153            vec![
154                {
155                    let mut s = State::atomic("a");
156                    s.transitions.push(Transition::new("go", "b"));
157                    s
158                },
159                {
160                    let mut s = State::atomic("b");
161                    s.transitions.push(Transition::new("done", "end"));
162                    s
163                },
164                State::final_state("end"),
165            ],
166        );
167
168        let (states, transitions) = flatten(&chart);
169        assert_eq!(states.len(), 3);
170        assert_eq!(transitions.len(), 2);
171        assert!(states[0].initial);
172        assert_eq!(states[0].depth, 0);
173    }
174
175    #[test]
176    fn flatten_nested_chart() {
177        let chart = Statechart::new(
178            "main",
179            vec![State::compound(
180                "main",
181                "child_a",
182                vec![
183                    {
184                        let mut s = State::atomic("child_a");
185                        s.transitions.push(Transition::new("next", "child_b"));
186                        s
187                    },
188                    State::atomic("child_b"),
189                ],
190            )],
191        );
192
193        let (states, _) = flatten(&chart);
194        assert_eq!(states.len(), 3); // main + child_a + child_b
195        assert_eq!(states[0].depth, 0);
196        assert_eq!(states[1].depth, 1);
197        assert_eq!(states[1].parent.as_deref(), Some("main"));
198    }
199}