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}