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