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()
267            || self.probability.is_some()
268            || self.probability_of_success.is_some()
269            || self.probability_of_failure.is_some()
270    }
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct Operation {
275    #[serde(default = "default_action")]
276    pub action: ActionKind,
277    pub handler: String,
278
279    #[serde(default)]
280    pub emits: Option<ExternalRef>,
281
282    pub next: NodeId,
283
284    #[serde(default, alias = "onFailure")]
285    pub on_failure: Option<NodeId>,
286
287    #[serde(default, alias = "onFailureProbabilitySource")]
288    pub on_failure_probability_source: Option<InternalRef>,
289
290    #[serde(default, alias = "retryPolicy")]
291    pub retry_policy: Option<RetryPolicy>,
292
293    #[serde(default, alias = "timeoutMs")]
294    pub timeout_ms: Option<u64>,
295
296    #[serde(default)]
297    pub description: Option<String>,
298}
299
300fn default_action() -> ActionKind {
301    ActionKind::Execute
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub enum ActionKind {
306    #[serde(rename = "execute")]
307    Execute,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct RetryPolicy {
312    #[serde(default = "default_max_attempts", alias = "maxAttempts")]
313    pub max_attempts: u32,
314
315    #[serde(default = "default_backoff_ms", alias = "backoffMs")]
316    pub backoff_ms: u64,
317
318    #[serde(default, alias = "backoffStrategy")]
319    pub backoff_strategy: Option<BackoffStrategy>,
320}
321
322fn default_max_attempts() -> u32 {
323    1
324}
325fn default_backoff_ms() -> u64 {
326    0
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize, Default)]
330pub enum BackoffStrategy {
331    #[serde(rename = "fixed")]
332    #[default]
333    Fixed,
334    #[serde(rename = "exponential")]
335    Exponential,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct Consequence {
340    #[serde(rename = "operation")]
341    pub consequence_operation: ConsequenceOperation,
342    #[serde(default)]
343    pub channel: Option<ExternalRef>,
344    #[serde(default)]
345    pub message: Option<ExternalRef>,
346    #[serde(default)]
347    pub description: Option<String>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub enum ConsequenceOperation {
352    #[serde(rename = "send")]
353    Send,
354    #[serde(rename = "terminate")]
355    Terminate,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct FaultTree {
360    #[serde(rename = "topEvent")]
361    pub top_event: TopEvent,
362
363    #[serde(default)]
364    pub gates: Option<BTreeMap<GateId, Gate>>,
365
366    #[serde(rename = "basicEvents")]
367    pub basic_events: BTreeMap<String, BasicEvent>,
368
369    #[serde(default)]
370    pub transfers: Option<BTreeMap<String, TransferNode>>,
371
372    #[serde(default)]
373    pub description: Option<String>,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct TopEvent {
378    pub id: String,
379    pub description: String,
380    #[serde(default)]
381    pub message: Option<ExternalRef>,
382    #[serde(rename = "rootCause")]
383    pub root_cause: FaultTreeNodeRef,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct Gate {
388    #[serde(rename = "type")]
389    pub gate_type: GateType,
390    pub inputs: Vec<FaultTreeNodeRef>,
391    #[serde(default)]
392    pub k: Option<u32>,
393    #[serde(default)]
394    pub description: Option<String>,
395    #[serde(default, alias = "inhibitCondition")]
396    pub inhibit_condition: Option<String>,
397}
398
399#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
400pub enum GateType {
401    #[serde(rename = "AND")]
402    And,
403    #[serde(rename = "OR")]
404    Or,
405    #[serde(rename = "NOT")]
406    Not,
407    #[serde(rename = "XOR")]
408    Xor,
409    #[serde(rename = "VOTING")]
410    Voting,
411    #[serde(rename = "INHIBIT")]
412    Inhibit,
413    #[serde(rename = "PRIORITY_AND")]
414    PriorityAnd,
415}
416
417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
418pub enum BasicEventType {
419    #[serde(rename = "basic")]
420    Basic,
421    #[serde(rename = "house")]
422    House,
423    #[serde(rename = "undeveloped")]
424    Undeveloped,
425    #[serde(rename = "conditional")]
426    Conditional,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct TransferNode {
431    pub target: String,
432    #[serde(default)]
433    pub label: Option<String>,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct BasicEvent {
438    pub description: String,
439    #[serde(default)]
440    pub probability: Option<f64>,
441    #[serde(default, alias = "failureRate")]
442    pub failure_rate: Option<f64>,
443    #[serde(default, alias = "missionTime")]
444    pub mission_time: Option<f64>,
445    #[serde(default)]
446    pub undeveloped: Option<bool>,
447    #[serde(default, alias = "eventType")]
448    pub event_type: Option<BasicEventType>,
449    #[serde(default)]
450    pub message: Option<ExternalRef>,
451}
452
453#[derive(Debug, Clone)]
454pub struct ExternalRef {
455    pub alias: String,
456    pub pointer: String,
457}
458
459#[derive(Debug, Clone)]
460pub struct InternalRef {
461    pub pointer: String,
462}
463
464pub type FaultTreeNodeRef = String;
465
466impl Serialize for ExternalRef {
467    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
468    where
469        S: Serializer,
470    {
471        serializer.serialize_str(&format!("{}#{}", self.alias, self.pointer))
472    }
473}
474
475impl<'de> Deserialize<'de> for ExternalRef {
476    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
477    where
478        D: Deserializer<'de>,
479    {
480        let s = String::deserialize(deserializer)?;
481        parse_external_ref(&s).map_err(serde::de::Error::custom)
482    }
483}
484
485fn parse_external_ref(s: &str) -> Result<ExternalRef, String> {
486    if let Some(hash_pos) = s.find('#') {
487        let alias = &s[..hash_pos];
488        let pointer = &s[hash_pos..];
489        if alias.is_empty() {
490            if pointer.starts_with("#/") {
491                return Err(format!(
492                    "bare JSON Pointer '{}' without import alias; use InternalRef for same-document references",
493                    pointer
494                ));
495            }
496            return Err("empty alias in external reference".to_string());
497        }
498        if alias
499            .chars()
500            .any(|c| !c.is_ascii_alphanumeric() && c != '_')
501        {
502            return Err(format!("invalid import alias '{}'", alias));
503        }
504        Ok(ExternalRef {
505            alias: alias.to_string(),
506            pointer: pointer.to_string(),
507        })
508    } else {
509        Err(format!("no '#' found in external reference '{}'", s))
510    }
511}
512
513impl Serialize for InternalRef {
514    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
515    where
516        S: Serializer,
517    {
518        serializer.serialize_str(&self.pointer)
519    }
520}
521
522impl<'de> Deserialize<'de> for InternalRef {
523    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
524    where
525        D: Deserializer<'de>,
526    {
527        let s = String::deserialize(deserializer)?;
528        if !s.starts_with("#/") {
529            return Err(serde::de::Error::custom(format!(
530                "invalid internal reference '{}'; must start with '#/'",
531                s
532            )));
533        }
534        Ok(InternalRef { pointer: s })
535    }
536}
537
538impl ExternalRef {
539    pub fn as_string(&self) -> String {
540        format!("{}#{}", self.alias, self.pointer)
541    }
542}
543
544impl InternalRef {
545    pub fn as_string(&self) -> String {
546        self.pointer.clone()
547    }
548}
549
550#[derive(Debug, Clone)]
551pub enum ParsedReference {
552    External(ExternalRef),
553    Internal(InternalRef),
554}
555
556pub fn parse_reference(s: &str) -> Result<ParsedReference, String> {
557    if s.starts_with("#/") {
558        Ok(ParsedReference::Internal(InternalRef {
559            pointer: s.to_string(),
560        }))
561    } else if let Some(hash_pos) = s.find('#') {
562        let alias = &s[..hash_pos];
563        let pointer = &s[hash_pos..];
564        if alias.is_empty() || pointer.is_empty() || !pointer.starts_with('#') {
565            return Err(format!("invalid reference syntax: '{}'", s));
566        }
567        parse_external_ref(s).map(ParsedReference::External)
568    } else {
569        Err(format!(
570            "reference '{}' matches neither external nor internal format",
571            s
572        ))
573    }
574}