Skip to main content

determa_state/
model.rs

1//! YAML machine-definition model (SPEC §4) and raw deserialization.
2//!
3//! These structs mirror `schema/machine.schema.json` closely. Structural validation
4//! (unknown fields, required fields, basic shapes) is enforced by serde's
5//! `deny_unknown_fields` plus [`crate::validate`]; richer semantic checks (reference
6//! resolution, choice defaults/acyclicity, esv scope, contracts) live in the validator.
7
8use crate::value::Value;
9use serde::Deserialize;
10use std::collections::BTreeMap;
11
12// --- action (parsed from a single-key mapping) -----------------------------
13
14#[derive(Debug, Clone, PartialEq)]
15pub enum Action {
16    /// `{ assign: { var: "<cel>", ... } }`
17    Assign(Vec<(String, String)>),
18    /// `{ publish: { event, to?, payload? } }`
19    Publish {
20        event: String,
21        to: Option<String>,
22        payload: Vec<(String, String)>,
23    },
24    /// `{ refresh: {} }` or `{ refresh: { only: [...] } }`
25    Refresh { only: Option<Vec<String>> },
26    /// `{ spawn: { def, payload?, result? } }`
27    Spawn {
28        def: String,
29        payload: Vec<(String, String)>,
30        result: Option<String>,
31    },
32    /// `{ stop: {} }`
33    Stop,
34}
35
36/// Parse one action from its raw YAML mapping.
37pub fn parse_action(raw: &serde_yaml::Value) -> Result<Action, String> {
38    let map = raw
39        .as_mapping()
40        .ok_or_else(|| "action must be a mapping".to_string())?;
41    if map.len() != 1 {
42        return Err(format!(
43            "action must have exactly one key, found {}",
44            map.len()
45        ));
46    }
47    let (k, v) = map.iter().next().unwrap();
48    let key = k.as_str().ok_or_else(|| "action key must be a string".to_string())?;
49    match key {
50        "assign" => {
51            let m = v.as_mapping().ok_or("assign value must be a mapping")?;
52            let mut pairs = Vec::new();
53            for (kk, vv) in m {
54                let name = kk.as_str().ok_or("assign key must be a string")?.to_string();
55                let expr = vv.as_str().ok_or("assign value must be a CEL string")?.to_string();
56                pairs.push((name, expr));
57            }
58            if pairs.is_empty() {
59                return Err("assign must have at least one variable".to_string());
60            }
61            Ok(Action::Assign(pairs))
62        }
63        "publish" => {
64            let m = v.as_mapping().ok_or("publish value must be a mapping")?;
65            let event = m
66                .get("event")
67                .and_then(|x| x.as_str())
68                .ok_or("publish.event is required")?
69                .to_string();
70            let to = m.get("to").and_then(|x| x.as_str()).map(|s| s.to_string());
71            let payload = parse_payload_cel(m.get("payload"))?;
72            Ok(Action::Publish { event, to, payload })
73        }
74        "refresh" => {
75            let only = if let Some(m) = v.as_mapping() {
76                m.get("only")
77                    .and_then(|x| x.as_sequence())
78                    .map(|seq| {
79                        seq.iter()
80                            .filter_map(|x| x.as_str().map(|s| s.to_string()))
81                            .collect()
82                    })
83            } else {
84                None
85            };
86            Ok(Action::Refresh { only })
87        }
88        "spawn" => {
89            let m = v.as_mapping().ok_or("spawn value must be a mapping")?;
90            let def = m
91                .get("def")
92                .and_then(|x| x.as_str())
93                .ok_or("spawn.def is required")?
94                .to_string();
95            let payload = parse_payload_cel(m.get("payload"))?;
96            let result = m.get("result").and_then(|x| x.as_str()).map(|s| s.to_string());
97            Ok(Action::Spawn {
98                def,
99                payload,
100                result,
101            })
102        }
103        "stop" => Ok(Action::Stop),
104        other => Err(format!("unknown action '{other}'")),
105    }
106}
107
108fn parse_payload_cel(v: Option<&serde_yaml::Value>) -> Result<Vec<(String, String)>, String> {
109    match v {
110        None => Ok(Vec::new()),
111        Some(serde_yaml::Value::Null) => Ok(Vec::new()),
112        Some(m) => {
113            let m = m.as_mapping().ok_or("payload must be a mapping")?;
114            let mut pairs = Vec::new();
115            for (kk, vv) in m {
116                let name = kk.as_str().ok_or("payload key must be a string")?.to_string();
117                let expr = vv.as_str().ok_or("payload value must be a CEL string")?.to_string();
118                pairs.push((name, expr));
119            }
120            Ok(pairs)
121        }
122    }
123}
124
125pub fn parse_actions(raw: Option<&serde_yaml::Value>) -> Result<Vec<Action>, String> {
126    match raw {
127        None => Ok(Vec::new()),
128        Some(serde_yaml::Value::Null) => Ok(Vec::new()),
129        Some(seq) => {
130            let seq = seq.as_sequence().ok_or("action list must be a sequence")?;
131            seq.iter().map(parse_action).collect()
132        }
133    }
134}
135
136// --- raw serde structs -----------------------------------------------------
137
138#[derive(Debug, Clone, Deserialize)]
139pub struct Languages {
140    #[serde(default)]
141    pub guard: Option<String>,
142    #[serde(default)]
143    pub action: Option<String>,
144}
145
146#[derive(Debug, Clone, Deserialize)]
147#[serde(deny_unknown_fields)]
148pub struct PayloadField {
149    #[serde(rename = "type")]
150    pub ty: String,
151    #[serde(default)]
152    pub required: Option<bool>,
153    #[serde(default)]
154    pub default: Option<serde_yaml::Value>,
155}
156
157#[derive(Debug, Clone, Deserialize)]
158#[serde(deny_unknown_fields)]
159pub struct EventDecl {
160    #[serde(default)]
161    pub scope: Option<String>,
162    #[serde(default)]
163    pub payload: Option<BTreeMap<String, PayloadField>>,
164}
165
166#[derive(Debug, Clone, Deserialize)]
167#[serde(deny_unknown_fields)]
168pub struct EsvDecl {
169    #[serde(rename = "type")]
170    pub ty: String,
171    #[serde(default)]
172    pub init: Option<serde_yaml::Value>,
173    #[serde(default)]
174    pub external: Option<bool>,
175}
176
177#[derive(Debug, Clone, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct Transition {
180    #[serde(default)]
181    pub transition_to: Option<String>,
182    #[serde(default)]
183    pub guard: Option<String>,
184    #[serde(default)]
185    pub lang: Option<String>,
186    #[serde(default)]
187    pub action: Option<serde_yaml::Value>,
188    #[serde(default)]
189    pub internal: Option<bool>,
190    #[serde(default)]
191    pub local: Option<bool>,
192}
193
194#[derive(Debug, Clone, Deserialize)]
195#[serde(untagged)]
196pub enum TransitionOrList {
197    One(Transition),
198    Many(Vec<Transition>),
199}
200
201#[derive(Debug, Clone, Deserialize)]
202#[serde(deny_unknown_fields)]
203pub struct InitialTransition {
204    pub transition_to: String,
205    #[serde(default)]
206    pub guard: Option<String>,
207    #[serde(default)]
208    pub action: Option<serde_yaml::Value>,
209}
210
211#[derive(Debug, Clone, Deserialize)]
212#[serde(deny_unknown_fields)]
213pub struct ChoiceBranch {
214    pub transition_to: String,
215    #[serde(default)]
216    pub guard: Option<String>,
217    #[serde(default)]
218    pub action: Option<serde_yaml::Value>,
219}
220
221#[derive(Debug, Clone, Deserialize)]
222#[serde(deny_unknown_fields)]
223pub struct AfterTimer {
224    pub duration: String,
225    #[serde(default)]
226    pub transition_to: Option<String>,
227    #[serde(default)]
228    pub guard: Option<String>,
229    #[serde(default)]
230    pub action: Option<serde_yaml::Value>,
231}
232
233#[derive(Debug, Clone, Deserialize)]
234#[serde(deny_unknown_fields)]
235pub struct Region {
236    pub initial: InitialTransition,
237    pub states: BTreeMap<String, StateNode>,
238}
239
240#[derive(Debug, Clone, Deserialize)]
241#[serde(deny_unknown_fields)]
242pub struct StateNode {
243    #[serde(rename = "type", default)]
244    pub ty: Option<String>,
245    #[serde(default)]
246    pub esvs: Option<BTreeMap<String, EsvDecl>>,
247    #[serde(default)]
248    pub entry: Option<serde_yaml::Value>,
249    #[serde(default)]
250    pub exit: Option<serde_yaml::Value>,
251    #[serde(default)]
252    pub initial: Option<InitialTransition>,
253    #[serde(default)]
254    pub states: Option<BTreeMap<String, StateNode>>,
255    #[serde(default)]
256    pub regions: Option<Vec<Region>>,
257    #[serde(default)]
258    pub on_events: Option<BTreeMap<String, TransitionOrList>>,
259    #[serde(default)]
260    pub after: Option<Vec<AfterTimer>>,
261    #[serde(default)]
262    pub defer: Option<Vec<String>>,
263    #[serde(default)]
264    pub history: Option<String>,
265    #[serde(default)]
266    pub choice: Option<Vec<ChoiceBranch>>,
267    /// `meta`: opaque state annotations for host layers (SPEC §4.5). Purely
268    /// informative — no effect on dispatch/validation/snapshots.
269    #[serde(default)]
270    pub meta: Option<serde_yaml::Value>,
271    /// `submachine: <id>` inlines another definition synchronously (SPEC §5.6.1).
272    #[serde(default)]
273    pub submachine: Option<String>,
274    /// `with: { <externalEsv>: <CEL> }` seeds the submachine's external esvs.
275    #[serde(default)]
276    pub with: Option<BTreeMap<String, String>>,
277    /// Engine-set: this node is the inlined `top` of a submachine (esv-scope
278    /// boundary). Never appears in YAML.
279    #[serde(skip)]
280    pub is_sm_boundary: bool,
281    #[serde(skip)]
282    pub sm_with: BTreeMap<String, String>,
283}
284
285/// Inline `submachine:` references into a nested state tree (SPEC §5.6.1).
286///
287/// A state with `submachine: <id>` becomes a composite whose single child (named
288/// `<id>`) is the referenced definition's `top` (recursively inlined), marked as an
289/// esv-scope boundary and carrying the `with:` seeding. `registry` maps definition
290/// id -> its raw `top`. Returns errors for unknown or cyclic references.
291pub fn inline_submachines(
292    node: &StateNode,
293    registry: &std::collections::HashMap<String, StateNode>,
294    stack: &std::collections::HashSet<String>,
295) -> Result<StateNode, Vec<crate::loader::LoadError>> {
296    use crate::loader::LoadError;
297    if let Some(sub_id) = &node.submachine {
298        let reg_top = match registry.get(sub_id) {
299            Some(t) => t,
300            None => {
301                return Err(vec![LoadError {
302                    path: "/submachine".into(),
303                    message: format!("unknown submachine '{sub_id}'"),
304                }]);
305            }
306        };
307        if stack.contains(sub_id) {
308            return Err(vec![LoadError {
309                path: "/submachine".into(),
310                message: format!("cyclic submachine '{sub_id}'"),
311            }]);
312        }
313        let mut new_stack = stack.clone();
314        new_stack.insert(sub_id.clone());
315        let mut child = inline_submachines(reg_top, registry, &new_stack)?;
316        child.is_sm_boundary = true;
317        child.sm_with = node.with.clone().unwrap_or_default();
318        let mut out = node.clone();
319        out.submachine = None;
320        out.with = None;
321        out.is_sm_boundary = false;
322        out.sm_with = BTreeMap::new();
323        let mut states = BTreeMap::new();
324        states.insert(sub_id.clone(), child);
325        out.states = Some(states);
326        out.initial = Some(InitialTransition {
327            transition_to: sub_id.clone(),
328            guard: None,
329            action: None,
330        });
331        return Ok(out);
332    }
333    let mut out = node.clone();
334    if let Some(states) = &node.states {
335        let mut new_states = BTreeMap::new();
336        for (k, v) in states {
337            new_states.insert(k.clone(), inline_submachines(v, registry, stack)?);
338        }
339        out.states = Some(new_states);
340    }
341    if let Some(regions) = &node.regions {
342        let mut new_regions = Vec::new();
343        for r in regions {
344            let mut nr = r.clone();
345            let mut ns = BTreeMap::new();
346            for (k, v) in &r.states {
347                ns.insert(k.clone(), inline_submachines(v, registry, stack)?);
348            }
349            nr.states = ns;
350            new_regions.push(nr);
351        }
352        out.regions = Some(new_regions);
353    }
354    Ok(out)
355}
356
357#[derive(Debug, Clone, Deserialize)]
358#[serde(deny_unknown_fields)]
359pub struct Migration {
360    pub from: i64,
361    pub to: i64,
362    #[serde(default)]
363    pub when: Option<String>,
364    #[serde(default)]
365    pub state_map: Option<BTreeMap<String, String>>,
366    #[serde(default)]
367    pub esvs: Option<serde_yaml::Value>,
368}
369
370#[derive(Debug, Clone, Deserialize)]
371#[serde(deny_unknown_fields)]
372pub struct RawMachine {
373    #[serde(default)]
374    pub format: Option<i64>,
375    pub id: String,
376    #[serde(default)]
377    pub version: Option<i64>,
378    #[serde(default)]
379    pub contracts: Vec<String>,
380    #[serde(default)]
381    pub subscribe: Vec<String>,
382    #[serde(default)]
383    pub languages: Option<Languages>,
384    #[serde(default)]
385    pub events: Option<BTreeMap<String, EventDecl>>,
386    #[serde(default)]
387    pub migrations: Option<Vec<Migration>>,
388    /// `meta`: opaque machine-level annotations for host layers (SPEC §4.1).
389    /// Purely informative — no effect on dispatch/validation/snapshots.
390    #[serde(default)]
391    pub meta: Option<serde_yaml::Value>,
392    pub top: StateNode,
393}
394
395/// Convert a raw esv init literal (YAML) into a Value, checking it matches the type.
396pub fn esv_init_value(ty: &str, raw: &serde_yaml::Value) -> Result<Value, String> {
397    let v = Value::from_yaml(raw);
398    if v.matches_type(ty) {
399        Ok(v)
400    } else {
401        Err(format!(
402            "esv init {:?} does not match declared type '{}'",
403            v, ty
404        ))
405    }
406}