1use crate::value::Value;
9use serde::Deserialize;
10use std::collections::BTreeMap;
11
12#[derive(Debug, Clone, PartialEq)]
15pub enum Action {
16 Assign(Vec<(String, String)>),
18 Publish {
20 event: String,
21 to: Option<String>,
22 payload: Vec<(String, String)>,
23 },
24 Refresh { only: Option<Vec<String>> },
26 Spawn {
28 def: String,
29 payload: Vec<(String, String)>,
30 result: Option<String>,
31 },
32 Stop,
34}
35
36pub 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#[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 #[serde(default)]
270 pub meta: Option<serde_yaml::Value>,
271 #[serde(default)]
273 pub submachine: Option<String>,
274 #[serde(default)]
276 pub with: Option<BTreeMap<String, String>>,
277 #[serde(skip)]
280 pub is_sm_boundary: bool,
281 #[serde(skip)]
282 pub sm_with: BTreeMap<String, String>,
283}
284
285pub 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 #[serde(default)]
391 pub meta: Option<serde_yaml::Value>,
392 pub top: StateNode,
393}
394
395pub 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}