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 transfers: Option<BTreeMap<String, TransferNode>>,
373
374 #[serde(default)]
375 pub description: Option<String>,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct TopEvent {
380 pub id: String,
381 pub description: String,
382 #[serde(default)]
383 pub message: Option<ExternalRef>,
384 #[serde(rename = "rootCause")]
385 pub root_cause: FaultTreeNodeRef,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct Gate {
390 #[serde(rename = "type")]
391 pub gate_type: GateType,
392 pub inputs: Vec<FaultTreeNodeRef>,
393 #[serde(default)]
394 pub k: Option<u32>,
395 #[serde(default)]
396 pub description: Option<String>,
397 #[serde(default, alias = "inhibitCondition")]
398 pub inhibit_condition: Option<String>,
399}
400
401#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
402pub enum GateType {
403 #[serde(rename = "AND")]
404 And,
405 #[serde(rename = "OR")]
406 Or,
407 #[serde(rename = "NOT")]
408 Not,
409 #[serde(rename = "XOR")]
410 Xor,
411 #[serde(rename = "VOTING")]
412 Voting,
413 #[serde(rename = "INHIBIT")]
414 Inhibit,
415 #[serde(rename = "PRIORITY_AND")]
416 PriorityAnd,
417}
418
419#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
420pub enum BasicEventType {
421 #[serde(rename = "basic")]
422 Basic,
423 #[serde(rename = "house")]
424 House,
425 #[serde(rename = "undeveloped")]
426 Undeveloped,
427 #[serde(rename = "conditional")]
428 Conditional,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct TransferNode {
433 pub target: String,
434 #[serde(default)]
435 pub label: Option<String>,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct BasicEvent {
440 pub description: String,
441 #[serde(default)]
442 pub probability: Option<f64>,
443 #[serde(default, alias = "failureRate")]
444 pub failure_rate: Option<f64>,
445 #[serde(default, alias = "missionTime")]
446 pub mission_time: Option<f64>,
447 #[serde(default)]
448 pub undeveloped: Option<bool>,
449 #[serde(default, alias = "eventType")]
450 pub event_type: Option<BasicEventType>,
451 #[serde(default)]
452 pub message: Option<ExternalRef>,
453}
454
455#[derive(Debug, Clone)]
456pub struct ExternalRef {
457 pub alias: String,
458 pub pointer: String,
459}
460
461#[derive(Debug, Clone)]
462pub struct InternalRef {
463 pub pointer: String,
464}
465
466pub type FaultTreeNodeRef = String;
467
468impl Serialize for ExternalRef {
469 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
470 where
471 S: Serializer,
472 {
473 serializer.serialize_str(&format!("{}#{}", self.alias, self.pointer))
474 }
475}
476
477impl<'de> Deserialize<'de> for ExternalRef {
478 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
479 where
480 D: Deserializer<'de>,
481 {
482 let s = String::deserialize(deserializer)?;
483 parse_external_ref(&s).map_err(serde::de::Error::custom)
484 }
485}
486
487fn parse_external_ref(s: &str) -> Result<ExternalRef, String> {
488 if let Some(hash_pos) = s.find('#') {
489 let alias = &s[..hash_pos];
490 let pointer = &s[hash_pos..];
491 if alias.is_empty() {
492 if pointer.starts_with("#/") {
493 return Err(format!(
494 "bare JSON Pointer '{}' without import alias; use InternalRef for same-document references",
495 pointer
496 ));
497 }
498 return Err("empty alias in external reference".to_string());
499 }
500 if alias
501 .chars()
502 .any(|c| !c.is_ascii_alphanumeric() && c != '_')
503 {
504 return Err(format!("invalid import alias '{}'", alias));
505 }
506 Ok(ExternalRef {
507 alias: alias.to_string(),
508 pointer: pointer.to_string(),
509 })
510 } else {
511 Err(format!("no '#' found in external reference '{}'", s))
512 }
513}
514
515impl Serialize for InternalRef {
516 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
517 where
518 S: Serializer,
519 {
520 serializer.serialize_str(&self.pointer)
521 }
522}
523
524impl<'de> Deserialize<'de> for InternalRef {
525 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
526 where
527 D: Deserializer<'de>,
528 {
529 let s = String::deserialize(deserializer)?;
530 if !s.starts_with("#/") {
531 return Err(serde::de::Error::custom(format!(
532 "invalid internal reference '{}'; must start with '#/'",
533 s
534 )));
535 }
536 Ok(InternalRef { pointer: s })
537 }
538}
539
540impl ExternalRef {
541 pub fn as_string(&self) -> String {
542 format!("{}#{}", self.alias, self.pointer)
543 }
544}
545
546impl InternalRef {
547 pub fn as_string(&self) -> String {
548 self.pointer.clone()
549 }
550}
551
552#[derive(Debug, Clone)]
553pub enum ParsedReference {
554 External(ExternalRef),
555 Internal(InternalRef),
556}
557
558pub fn parse_reference(s: &str) -> Result<ParsedReference, String> {
559 if s.starts_with("#/") {
560 Ok(ParsedReference::Internal(InternalRef {
561 pointer: s.to_string(),
562 }))
563 } else if let Some(hash_pos) = s.find('#') {
564 let alias = &s[..hash_pos];
565 let pointer = &s[hash_pos..];
566 if alias.is_empty() || pointer.is_empty() || !pointer.starts_with('#') {
567 return Err(format!("invalid reference syntax: '{}'", s));
568 }
569 parse_external_ref(s).map(ParsedReference::External)
570 } else {
571 Err(format!(
572 "reference '{}' matches neither external nor internal format",
573 s
574 ))
575 }
576}