Skip to main content

etdl_parser/
ast.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::collections::BTreeMap;
3
4pub use crate::ecel::{parse_condition, Condition};
5
6pub type NodeId = String;
7pub type GateId = String;
8
9#[derive(Debug, Clone, Serialize)]
10pub struct EtlDocument {
11    pub etdl: String,
12    pub info: Info,
13    #[serde(default)]
14    pub asyncapi_imports: BTreeMap<String, String>,
15
16    #[serde(default)]
17    pub components: Option<Components>,
18
19    pub event_trees: BTreeMap<String, EventTree>,
20
21    #[serde(default)]
22    pub fault_trees: Option<BTreeMap<String, FaultTree>>,
23
24    pub extensions: BTreeMap<String, serde_yaml::Value>,
25}
26
27impl<'de> Deserialize<'de> for EtlDocument {
28    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
29    where
30        D: Deserializer<'de>,
31    {
32        use serde::de::{Error, MapAccess, Visitor};
33        use std::fmt;
34
35        struct DocVisitor;
36
37        impl<'de> Visitor<'de> for DocVisitor {
38            type Value = EtlDocument;
39
40            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
41                f.write_str("an ETDL document")
42            }
43
44            fn visit_map<A>(self, mut map: A) -> Result<EtlDocument, A::Error>
45            where
46                A: MapAccess<'de>,
47            {
48                let mut etdl: Option<String> = None;
49                let mut info: Option<Info> = None;
50                let mut asyncapi_imports: BTreeMap<String, String> = BTreeMap::new();
51                let mut components: Option<Components> = None;
52                let mut event_trees_map: BTreeMap<String, EventTree> = BTreeMap::new();
53                let mut event_tree_legacy: Option<EventTree> = None;
54                let mut fault_trees: Option<BTreeMap<String, FaultTree>> = None;
55                let mut extensions: BTreeMap<String, serde_yaml::Value> = BTreeMap::new();
56
57                while let Some(key) = map.next_key::<String>()? {
58                    match key.as_str() {
59                        "etdl" => {
60                            if etdl.is_some() {
61                                return Err(Error::duplicate_field("etdl"));
62                            }
63                            etdl = Some(map.next_value()?);
64                        }
65                        "info" => {
66                            if info.is_some() {
67                                return Err(Error::duplicate_field("info"));
68                            }
69                            info = Some(map.next_value()?);
70                        }
71                        "asyncapi_imports" => {
72                            asyncapi_imports = map.next_value()?;
73                        }
74                        "components" => {
75                            components = map.next_value()?;
76                        }
77                        "eventTrees" => {
78                            event_trees_map = map.next_value()?;
79                        }
80                        "eventTree" => {
81                            event_tree_legacy = Some(map.next_value()?);
82                        }
83                        "faultTrees" => {
84                            fault_trees = map.next_value()?;
85                        }
86                        k if k.starts_with("x-") => {
87                            let val: serde_yaml::Value = map.next_value()?;
88                            extensions.insert(k.to_string(), val);
89                        }
90                        unknown => {
91                            return Err(Error::custom(format!(
92                                "unrecognized field '{}' in ETDL document; extension fields must start with 'x-'",
93                                unknown
94                            )));
95                        }
96                    }
97                }
98
99                let etdl = etdl.ok_or_else(|| Error::missing_field("etdl"))?;
100                let info = info.ok_or_else(|| Error::missing_field("info"))?;
101
102                let event_trees = match (event_trees_map.is_empty(), event_tree_legacy) {
103                    (true, Some(tree)) => {
104                        let mut map = BTreeMap::new();
105                        map.insert("default".to_string(), tree);
106                        map
107                    }
108                    (true, None) => {
109                        return Err(Error::custom(
110                            "at least one of 'eventTrees' or 'eventTree' (deprecated) must be present",
111                        ));
112                    }
113                    (false, None) => event_trees_map,
114                    (false, Some(_)) => {
115                        return Err(Error::custom(
116                            "both 'eventTrees' and 'eventTree' (deprecated) provided; use only 'eventTrees'",
117                        ));
118                    }
119                };
120
121                Ok(EtlDocument {
122                    etdl,
123                    info,
124                    asyncapi_imports,
125                    components,
126                    event_trees,
127                    fault_trees,
128                    extensions,
129                })
130            }
131        }
132
133        deserializer.deserialize_map(DocVisitor)
134    }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct Info {
139    pub title: String,
140    pub version: String,
141    #[serde(deserialize_with = "deserialize_domain")]
142    pub domain: String,
143    #[serde(default)]
144    pub description: Option<String>,
145}
146
147fn deserialize_domain<'de, D>(deserializer: D) -> Result<String, D::Error>
148where
149    D: Deserializer<'de>,
150{
151    let s = String::deserialize(deserializer)?;
152    if s.is_empty() || !s.chars().next().unwrap().is_ascii_alphabetic() {
153        return Err(serde::de::Error::custom(
154            "domain must match ^[A-Za-z][A-Za-z0-9]*$",
155        ));
156    }
157    if s.chars().any(|c| !c.is_ascii_alphanumeric()) {
158        return Err(serde::de::Error::custom(
159            "domain must match ^[A-Za-z][A-Za-z0-9]*$",
160        ));
161    }
162    Ok(s)
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, Default)]
166pub struct Components {
167    #[serde(default)]
168    pub barriers: Option<BTreeMap<String, Barrier>>,
169    #[serde(default)]
170    pub operations: Option<BTreeMap<String, Operation>>,
171    #[serde(default)]
172    pub gates: Option<BTreeMap<String, Gate>>,
173    #[serde(default)]
174    pub basic_events: Option<BTreeMap<String, BasicEvent>>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct EventTree {
179    #[serde(rename = "initiatingEvent")]
180    pub initiating_event: InitiatingEvent,
181    pub nodes: BTreeMap<NodeId, Node>,
182    #[serde(default)]
183    pub description: Option<String>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct InitiatingEvent {
188    pub id: String,
189    pub message: ExternalRef,
190    pub next: NodeId,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(tag = "type")]
195pub enum Node {
196    #[serde(rename = "barrier")]
197    Barrier(Barrier),
198    #[serde(rename = "operation")]
199    Operation(Operation),
200    #[serde(rename = "consequence")]
201    Consequence(Consequence),
202}
203
204impl Node {
205    pub fn node_type(&self) -> &str {
206        match self {
207            Node::Barrier(_) => "barrier",
208            Node::Operation(_) => "operation",
209            Node::Consequence(_) => "consequence",
210        }
211    }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct Barrier {
216    pub branches: Vec<Branch>,
217    #[serde(default)]
218    pub description: Option<String>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct Branch {
223    pub outcome: String,
224
225    #[serde(deserialize_with = "deserialize_condition")]
226    pub condition: Condition,
227
228    #[serde(default, deserialize_with = "deserialize_optional_f64")]
229    pub probability: Option<f64>,
230
231    #[serde(default, alias = "probabilityOfSuccess")]
232    pub probability_of_success: Option<f64>,
233
234    #[serde(default, alias = "probabilityOfFailure")]
235    pub probability_of_failure: Option<f64>,
236
237    #[serde(default, alias = "probabilitySource")]
238    pub probability_source: Option<InternalRef>,
239
240    pub next: NodeId,
241}
242
243fn deserialize_condition<'de, D>(deserializer: D) -> Result<Condition, D::Error>
244where
245    D: Deserializer<'de>,
246{
247    let s = String::deserialize(deserializer)?;
248    parse_condition(&s).map_err(serde::de::Error::custom)
249}
250
251fn deserialize_optional_f64<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
252where
253    D: Deserializer<'de>,
254{
255    Option::<f64>::deserialize(deserializer)
256}
257
258impl Branch {
259    pub fn effective_probability(&self) -> Option<f64> {
260        self.probability
261            .or(self.probability_of_success)
262            .or(self.probability_of_failure)
263    }
264
265    pub fn has_probability_source(&self) -> bool {
266        self.probability_source.is_some() || self.probability.is_some() || self.probability_of_success.is_some() || self.probability_of_failure.is_some()
267    }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct Operation {
272    #[serde(default = "default_action")]
273    pub action: ActionKind,
274    pub handler: String,
275
276    #[serde(default)]
277    pub emits: Option<ExternalRef>,
278
279    pub next: NodeId,
280
281    #[serde(default, alias = "onFailure")]
282    pub on_failure: Option<NodeId>,
283
284    #[serde(default, alias = "onFailureProbabilitySource")]
285    pub on_failure_probability_source: Option<InternalRef>,
286
287    #[serde(default, alias = "retryPolicy")]
288    pub retry_policy: Option<RetryPolicy>,
289
290    #[serde(default, alias = "timeoutMs")]
291    pub timeout_ms: Option<u64>,
292
293    #[serde(default)]
294    pub description: Option<String>,
295}
296
297fn default_action() -> ActionKind {
298    ActionKind::Execute
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub enum ActionKind {
303    #[serde(rename = "execute")]
304    Execute,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct RetryPolicy {
309    #[serde(default = "default_max_attempts", alias = "maxAttempts")]
310    pub max_attempts: u32,
311
312    #[serde(default = "default_backoff_ms", alias = "backoffMs")]
313    pub backoff_ms: u64,
314
315    #[serde(default, alias = "backoffStrategy")]
316    pub backoff_strategy: Option<BackoffStrategy>,
317}
318
319fn default_max_attempts() -> u32 {
320    1
321}
322fn default_backoff_ms() -> u64 {
323    0
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub enum BackoffStrategy {
328    #[serde(rename = "fixed")]
329    Fixed,
330    #[serde(rename = "exponential")]
331    Exponential,
332}
333
334impl Default for BackoffStrategy {
335    fn default() -> Self {
336        BackoffStrategy::Fixed
337    }
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct Consequence {
342    #[serde(rename = "operation")]
343    pub consequence_operation: ConsequenceOperation,
344    #[serde(default)]
345    pub channel: Option<ExternalRef>,
346    #[serde(default)]
347    pub message: Option<ExternalRef>,
348    #[serde(default)]
349    pub description: Option<String>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub enum ConsequenceOperation {
354    #[serde(rename = "send")]
355    Send,
356    #[serde(rename = "terminate")]
357    Terminate,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct FaultTree {
362    #[serde(rename = "topEvent")]
363    pub top_event: TopEvent,
364
365    #[serde(default)]
366    pub gates: Option<BTreeMap<GateId, Gate>>,
367
368    #[serde(rename = "basicEvents")]
369    pub basic_events: BTreeMap<String, BasicEvent>,
370
371    #[serde(default)]
372    pub description: Option<String>,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct TopEvent {
377    pub id: String,
378    pub description: String,
379    #[serde(default)]
380    pub message: Option<ExternalRef>,
381    #[serde(rename = "rootCause")]
382    pub root_cause: FaultTreeNodeRef,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct Gate {
387    #[serde(rename = "type")]
388    pub gate_type: GateType,
389    pub inputs: Vec<FaultTreeNodeRef>,
390    #[serde(default)]
391    pub k: Option<u32>,
392    #[serde(default)]
393    pub description: Option<String>,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub enum GateType {
398    #[serde(rename = "AND")]
399    And,
400    #[serde(rename = "OR")]
401    Or,
402    #[serde(rename = "NOT")]
403    Not,
404    #[serde(rename = "XOR")]
405    Xor,
406    #[serde(rename = "VOTING")]
407    Voting,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct BasicEvent {
412    pub description: String,
413    #[serde(default)]
414    pub probability: Option<f64>,
415    #[serde(default, alias = "failureRate")]
416    pub failure_rate: Option<f64>,
417    #[serde(default, alias = "missionTime")]
418    pub mission_time: Option<f64>,
419    #[serde(default)]
420    pub undeveloped: Option<bool>,
421    #[serde(default)]
422    pub message: Option<ExternalRef>,
423}
424
425#[derive(Debug, Clone)]
426pub struct ExternalRef {
427    pub alias: String,
428    pub pointer: String,
429}
430
431#[derive(Debug, Clone)]
432pub struct InternalRef {
433    pub pointer: String,
434}
435
436pub type FaultTreeNodeRef = String;
437
438impl Serialize for ExternalRef {
439    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
440    where
441        S: Serializer,
442    {
443        serializer.serialize_str(&format!("{}#{}", self.alias, self.pointer))
444    }
445}
446
447impl<'de> Deserialize<'de> for ExternalRef {
448    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
449    where
450        D: Deserializer<'de>,
451    {
452        let s = String::deserialize(deserializer)?;
453        parse_external_ref(&s).map_err(serde::de::Error::custom)
454    }
455}
456
457fn parse_external_ref(s: &str) -> Result<ExternalRef, String> {
458    if let Some(hash_pos) = s.find('#') {
459        let alias = &s[..hash_pos];
460        let pointer = &s[hash_pos..];
461        if alias.is_empty() {
462            if pointer.starts_with("#/") {
463                return Err(format!(
464                    "bare JSON Pointer '{}' without import alias; use InternalRef for same-document references",
465                    pointer
466                ));
467            }
468            return Err("empty alias in external reference".to_string());
469        }
470        if alias
471            .chars()
472            .any(|c| !c.is_ascii_alphanumeric() && c != '_')
473        {
474            return Err(format!("invalid import alias '{}'", alias));
475        }
476        Ok(ExternalRef {
477            alias: alias.to_string(),
478            pointer: pointer.to_string(),
479        })
480    } else {
481        Err(format!("no '#' found in external reference '{}'", s))
482    }
483}
484
485impl Serialize for InternalRef {
486    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
487    where
488        S: Serializer,
489    {
490        serializer.serialize_str(&self.pointer)
491    }
492}
493
494impl<'de> Deserialize<'de> for InternalRef {
495    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
496    where
497        D: Deserializer<'de>,
498    {
499        let s = String::deserialize(deserializer)?;
500        if !s.starts_with("#/") {
501            return Err(serde::de::Error::custom(format!(
502                "invalid internal reference '{}'; must start with '#/'",
503                s
504            )));
505        }
506        Ok(InternalRef { pointer: s })
507    }
508}
509
510impl ExternalRef {
511    pub fn as_string(&self) -> String {
512        format!("{}#{}", self.alias, self.pointer)
513    }
514}
515
516impl InternalRef {
517    pub fn as_string(&self) -> String {
518        self.pointer.clone()
519    }
520}
521
522#[derive(Debug, Clone)]
523pub enum ParsedReference {
524    External(ExternalRef),
525    Internal(InternalRef),
526}
527
528pub fn parse_reference(s: &str) -> Result<ParsedReference, String> {
529    if s.starts_with("#/") {
530        Ok(ParsedReference::Internal(InternalRef {
531            pointer: s.to_string(),
532        }))
533    } else if let Some(hash_pos) = s.find('#') {
534        let alias = &s[..hash_pos];
535        let pointer = &s[hash_pos..];
536        if alias.is_empty() || pointer.is_empty() || !pointer.starts_with('#') {
537            return Err(format!("invalid reference syntax: '{}'", s));
538        }
539        parse_external_ref(s).map(ParsedReference::External)
540    } else {
541        Err(format!(
542            "reference '{}' matches neither external nor internal format",
543            s
544        ))
545    }
546}