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#[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 #[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}