1use etdl_parser::ast::{BackoffStrategy, Condition, EtlDocument, EventTree, Node};
2use etdl_parser::asyncapi::AsyncApiRegistry;
3use etdl_parser::ecel;
4use std::collections::BTreeMap;
5
6use crate::validate::Diagnostic;
7
8use super::CodeGenerator;
9
10pub struct RustCodeGenerator {
11 pub version: String,
12}
13
14impl Default for RustCodeGenerator {
15 fn default() -> Self {
16 Self::new()
17 }
18}
19
20impl RustCodeGenerator {
21 pub fn new() -> Self {
22 RustCodeGenerator {
23 version: env!("CARGO_PKG_VERSION").to_string(),
24 }
25 }
26}
27
28impl CodeGenerator for RustCodeGenerator {
29 fn generate_all(
30 &self,
31 doc: &EtlDocument,
32 fault_tree_probs: &BTreeMap<String, f64>,
33 _registry: &AsyncApiRegistry,
34 _diagnostics: &mut Vec<Diagnostic>,
35 ) -> Result<String, String> {
36 let mut output = String::new();
37
38 output.push_str(&format!(
39 "// AUTOGENERATED BY ETDL COMPILER v{} - DO NOT EDIT DIRECTLY\n\n",
40 self.version
41 ));
42
43 let imports = collect_imports(doc);
44 output.push_str(&imports);
45 output.push('\n');
46
47 let constants = generate_fault_tree_constants(doc, fault_tree_probs);
48 output.push_str(&constants);
49
50 for tree in doc.event_trees.values() {
51 let handler_code = generate_event_tree_handler(doc, tree, fault_tree_probs)?;
52 output.push_str(&handler_code);
53 output.push('\n');
54 }
55
56 Ok(output)
57 }
58}
59
60fn collect_imports(doc: &EtlDocument) -> String {
61 let mut imports = String::from(
62 "use std::time::Duration;\n\
63 use etdl_core::BranchMonitor;\n\
64 use etdl_core::condition::{contains, matches};\n\
65 use etdl_core::publisher::Publisher;\n\
66 use etdl_core::retry::{RetryPolicy, BackoffStrategy};\n\
67 use etdl_core::WorkflowError;\n\
68 use serde::Serialize;\n",
69 );
70
71 let mut alias_set: BTreeMap<String, bool> = BTreeMap::new();
72
73 for tree in doc.event_trees.values() {
74 alias_set.insert(tree.initiating_event.message.alias.clone(), true);
75
76 for node in tree.nodes.values() {
77 match node {
78 Node::Operation(op) => {
79 if let Some(ref ext_ref) = op.emits {
80 alias_set.insert(ext_ref.alias.clone(), true);
81 }
82 }
83 Node::Consequence(cons) => {
84 if let Some(ref ext_ref) = cons.channel {
85 alias_set.insert(ext_ref.alias.clone(), true);
86 }
87 if let Some(ref ext_ref) = cons.message {
88 alias_set.insert(ext_ref.alias.clone(), true);
89 }
90 }
91 _ => {}
92 }
93 }
94 }
95
96 for alias in alias_set.keys() {
97 let mod_name = alias.replace('-', "_");
98 imports.push_str(&format!("use {}::messages::*;\n", mod_name));
99 }
100
101 imports
102}
103
104fn generate_fault_tree_constants(
105 doc: &EtlDocument,
106 fault_tree_probs: &BTreeMap<String, f64>,
107) -> String {
108 let mut output = String::new();
109
110 for tree in doc.event_trees.values() {
111 for (node_id, node) in &tree.nodes {
112 if let Node::Operation(op) = node {
113 if let Some(ref ps) = op.on_failure_probability_source {
114 if let Some((ft_id, prob)) = find_fault_tree_prob(ps, fault_tree_probs) {
115 output.push_str(&format!(
116 "// Computed from faultTrees.{}.topEvent at build time (Section 5.16)\n",
117 ft_id
118 ));
119 let const_name =
120 to_upper_snake(&format!("{}_failure_probability", node_id));
121 output.push_str(&format!("const {}: f64 = {:.6};\n\n", const_name, prob));
122 }
123 }
124 }
125 }
126 }
127
128 output
129}
130
131fn find_fault_tree_prob(
135 ps: &etdl_parser::ast::InternalRef,
136 fault_tree_probs: &BTreeMap<String, f64>,
137) -> Option<(String, f64)> {
138 let ft_id = extract_ft_id(&ps.pointer);
139 fault_tree_probs.get(&ft_id).map(|&v| (ft_id.clone(), v))
140}
141
142fn generate_event_tree_handler(
143 doc: &EtlDocument,
144 tree: &EventTree,
145 fault_tree_probs: &BTreeMap<String, f64>,
146) -> Result<String, String> {
147 let mut output = String::new();
148
149 let fn_name = format!("handle_{}", to_snake_case(&tree.initiating_event.id));
150 let message_type = ref_to_rust_type(&tree.initiating_event.message);
151
152 output.push_str(&format!(
153 "pub async fn {}(message: {}, publisher: &dyn Publisher) -> Result<(), WorkflowError> {{\n",
154 fn_name, message_type
155 ));
156
157 let first_barrier = find_first_barrier(tree);
158 let monitor_name = first_barrier
159 .map(to_snake_case)
160 .unwrap_or_else(|| "monitor".to_string());
161
162 output.push_str(&format!(
163 " let mut {} = BranchMonitor::new(\"{}\");\n\n",
164 monitor_name,
165 first_barrier.unwrap_or("root")
166 ));
167
168 let start_node_id = &tree.initiating_event.next;
169 let body = generate_node_code(doc, tree, start_node_id, 1, fault_tree_probs, &monitor_name)?;
170 output.push_str(&body);
171
172 output.push_str(" Ok(())\n");
173 output.push_str("}\n");
174
175 Ok(output)
176}
177
178fn find_first_barrier(tree: &EventTree) -> Option<&str> {
179 let mut current = &tree.initiating_event.next;
180 loop {
181 match tree.nodes.get(current.as_str()) {
182 Some(Node::Barrier(_)) => return Some(current.as_str()),
183 Some(Node::Operation(op)) => {
184 current = &op.next;
185 }
186 Some(Node::Consequence(_)) => return None,
187 None => return None,
188 }
189 }
190}
191
192fn generate_node_code(
193 doc: &EtlDocument,
194 tree: &EventTree,
195 node_id: &str,
196 depth: usize,
197 fault_tree_probs: &BTreeMap<String, f64>,
198 monitor_name: &str,
199) -> Result<String, String> {
200 let node = tree
201 .nodes
202 .get(node_id)
203 .ok_or_else(|| format!("node '{}' not found", node_id))?;
204
205 match node {
206 Node::Barrier(barrier) => generate_barrier_code(
207 tree,
208 node_id,
209 barrier,
210 depth,
211 doc,
212 fault_tree_probs,
213 monitor_name,
214 ),
215 Node::Operation(op) => generate_operation_code(
216 tree,
217 node_id,
218 op,
219 depth,
220 doc,
221 fault_tree_probs,
222 monitor_name,
223 ),
224 Node::Consequence(cons) => generate_consequence_code(cons, depth),
225 }
226}
227
228fn generate_barrier_code(
229 tree: &EventTree,
230 node_id: &str,
231 barrier: &etdl_parser::ast::Barrier,
232 depth: usize,
233 doc: &EtlDocument,
234 fault_tree_probs: &BTreeMap<String, f64>,
235 monitor_name: &str,
236) -> Result<String, String> {
237 let indent = " ".repeat(depth);
238 let mut output = String::new();
239
240 for (i, branch) in barrier.branches.iter().enumerate() {
241 if i == 0 {
242 if branch.condition == Condition::Default {
243 let prob = get_branch_prob(branch, node_id, fault_tree_probs);
244 if let Some(p) = prob {
245 output.push_str(&format!(
246 "{} {}.record_branch(\"{}\", {:.6});\n",
247 indent, monitor_name, branch.outcome, p
248 ));
249 }
250 let body = generate_node_code(
251 doc,
252 tree,
253 &branch.next,
254 depth + 1,
255 fault_tree_probs,
256 monitor_name,
257 )?;
258 output.push_str(&body);
259 } else {
260 let cond = condition_to_rust_code(&branch.condition);
261 output.push_str(&format!("{}if {} {{\n", indent, cond));
262
263 let prob = get_branch_prob(branch, node_id, fault_tree_probs);
264 if let Some(p) = prob {
265 output.push_str(&format!(
266 "{} {}.record_branch(\"{}\", {:.6});\n",
267 indent, monitor_name, branch.outcome, p
268 ));
269 }
270
271 let body = generate_node_code(
272 doc,
273 tree,
274 &branch.next,
275 depth + 1,
276 fault_tree_probs,
277 monitor_name,
278 )?;
279 output.push_str(&body);
280 output.push_str(&format!("{}}}", indent));
281 }
282 } else if branch.condition == Condition::Default {
283 output.push_str(" else {\n");
284
285 let prob = get_branch_prob(branch, node_id, fault_tree_probs);
286 if let Some(p) = prob {
287 output.push_str(&format!(
288 "{} {}.record_branch(\"{}\", {:.6});\n",
289 indent, monitor_name, branch.outcome, p
290 ));
291 }
292
293 let body = generate_node_code(
294 doc,
295 tree,
296 &branch.next,
297 depth + 1,
298 fault_tree_probs,
299 monitor_name,
300 )?;
301 output.push_str(&body);
302 output.push_str(&format!("{}}}\n", indent));
303 } else {
304 let cond = condition_to_rust_code(&branch.condition);
305 output.push_str(&format!(" else if {} {{\n", cond));
306
307 let prob = get_branch_prob(branch, node_id, fault_tree_probs);
308 if let Some(p) = prob {
309 output.push_str(&format!(
310 "{} {}.record_branch(\"{}\", {:.6});\n",
311 indent, monitor_name, branch.outcome, p
312 ));
313 }
314
315 let body = generate_node_code(
316 doc,
317 tree,
318 &branch.next,
319 depth + 1,
320 fault_tree_probs,
321 monitor_name,
322 )?;
323 output.push_str(&body);
324 output.push_str(&format!("{}}}", indent));
325 }
326 }
327
328 Ok(output)
329}
330
331fn generate_operation_code(
332 tree: &EventTree,
333 node_id: &str,
334 op: &etdl_parser::ast::Operation,
335 depth: usize,
336 doc: &EtlDocument,
337 fault_tree_probs: &BTreeMap<String, f64>,
338 monitor_name: &str,
339) -> Result<String, String> {
340 let indent = " ".repeat(depth);
341 let mut output = String::new();
342
343 let handler_name = to_snake_case(&op.handler);
344 let timeout = op.timeout_ms.unwrap_or(5000);
345
346 if let Some(ref retry) = op.retry_policy {
347 let strategy = match retry
348 .backoff_strategy
349 .as_ref()
350 .unwrap_or(&BackoffStrategy::Fixed)
351 {
352 BackoffStrategy::Exponential => "BackoffStrategy::Exponential",
353 BackoffStrategy::Fixed => "BackoffStrategy::Fixed",
354 };
355 output.push_str(&format!(
356 "{}let retry = RetryPolicy {{\n\
357 {} max_attempts: {},\n\
358 {} backoff_ms: {},\n\
359 {} strategy: {},\n\
360 {}}};\n",
361 indent, indent, retry.max_attempts, indent, retry.backoff_ms, indent, strategy, indent
362 ));
363 output.push_str(&format!(
364 "{}match retry.execute(|| {}(&message), Duration::from_millis({})).await {{\n",
365 indent, handler_name, timeout
366 ));
367 } else {
368 output.push_str(&format!(
369 "{}match {}(&message).await {{\n",
370 indent, handler_name
371 ));
372 }
373
374 output.push_str(&format!("{} Ok(_result) => {{\n", indent));
375
376 let next_node = tree.nodes.get(&op.next);
377 match next_node {
378 Some(Node::Consequence(cons)) => {
379 emit_send(cons, "_result", &indent, &mut output);
380 }
381 Some(_) => {
382 generate_node_code(
383 doc,
384 tree,
385 &op.next,
386 depth + 2,
387 fault_tree_probs,
388 monitor_name,
389 )
390 .map(|body| output.push_str(&body))?;
391 }
392 None => {}
393 }
394
395 output.push_str(&format!("{} }}\n", indent));
396
397 if let Some(ref _on_failure_id) = op.on_failure {
398 output.push_str(&format!("{} Err(err) => {{\n", indent));
399
400 if let Some(ref ps) = op.on_failure_probability_source {
401 let const_name = to_upper_snake(&format!("{}_failure_probability", node_id));
402 let prob_exists = find_fault_tree_prob(ps, fault_tree_probs).is_some();
403 if prob_exists {
404 output.push_str(&format!(
405 "{} {}.record_failure(\"{}\", &err, Some({}));\n",
406 indent, monitor_name, node_id, const_name
407 ));
408 } else {
409 output.push_str(&format!(
410 "{} {}.record_failure(\"{}\", &err, None);\n",
411 indent, monitor_name, node_id
412 ));
413 }
414 } else {
415 output.push_str(&format!(
416 "{} {}.record_failure(\"{}\", &err, None);\n",
417 indent, monitor_name, node_id
418 ));
419 }
420
421 let on_failure_id = op.on_failure.as_ref().unwrap();
422 match tree.nodes.get(on_failure_id) {
423 Some(Node::Consequence(cons)) => {
424 emit_send(cons, "message", &indent, &mut output);
425 }
426 _ => {
427 generate_node_code(
428 doc,
429 tree,
430 on_failure_id,
431 depth + 2,
432 fault_tree_probs,
433 monitor_name,
434 )
435 .map(|body| output.push_str(&body))?;
436 }
437 }
438
439 output.push_str(&format!("{} }}\n", indent));
440 } else {
441 output.push_str(&format!(
442 "{} Err(err) => return Err(WorkflowError::new(format!(\"{{}}\", err))),\n",
443 indent
444 ));
445 }
446
447 output.push_str(&format!("{}}}\n", indent));
448
449 Ok(output)
450}
451
452fn emit_send(
454 cons: &etdl_parser::ast::Consequence,
455 payload_expr: &str,
456 indent: &str,
457 output: &mut String,
458) {
459 match cons.consequence_operation {
460 etdl_parser::ast::ConsequenceOperation::Send => {
461 if let Some(ref channel_ref) = cons.channel {
462 let channel_name = extract_last_segment(channel_ref);
463 output.push_str(&format!(
464 "{} publisher.publish(\"{}\", &etdl_core::serde_json::to_value({}).map_err(|e| WorkflowError::new(format!(\"{{}}\", e)))?)?;\n",
465 indent, channel_name, payload_expr
466 ));
467 }
468 }
469 etdl_parser::ast::ConsequenceOperation::Terminate => {}
470 }
471}
472
473fn generate_consequence_code(
474 cons: &etdl_parser::ast::Consequence,
475 depth: usize,
476) -> Result<String, String> {
477 let indent = " ".repeat(depth);
478 let mut output = String::new();
479
480 match cons.consequence_operation {
481 etdl_parser::ast::ConsequenceOperation::Send => {
482 if let Some(ref channel_ref) = cons.channel {
483 let channel_name = extract_last_segment(channel_ref);
484 output.push_str(&format!(
485 "{}publisher.publish(\"{}\", &etdl_core::serde_json::to_value(message).map_err(|e| WorkflowError::new(format!(\"{{}}\", e)))?)?;\n",
486 indent, channel_name
487 ));
488 }
489 }
490 etdl_parser::ast::ConsequenceOperation::Terminate => {}
491 }
492
493 if depth == 1 {
494 output.push_str(&format!("{}Ok(())\n", indent));
495 }
496
497 Ok(output)
498}
499
500fn condition_to_rust_code(condition: &Condition) -> String {
501 match condition {
502 Condition::Default => "true".to_string(),
503 Condition::Comparison(cmp) => {
504 use ecel::Comparator as C;
505 match cmp.op {
506 C::In => {
508 let left = path_or_literal(&cmp.left);
509 let right = array_or_literal(&cmp.right);
510 format!("etdl_core::condition::contains(&{}, &{})", right, left)
511 }
512 C::Matches => {
513 let left = path_or_literal(&cmp.left);
514 let right = literal_to_val_str(&literal_of(&cmp.right));
515 format!("etdl_core::condition::matches({}, {})", left, right)
516 }
517 _ => {
518 let (left_path, has_wildcard) = build_path_expression(&cmp.left);
519 let right = operand_to_val_str(&cmp.right);
520 let op = comparator_str(&cmp.op);
521
522 if has_wildcard {
523 format!(
525 "{}.iter().all(|item| item{} {} {})",
526 left_path.path_prefix, left_path.remaining_path, op, right
527 )
528 } else {
529 let l = if left_path.path_prefix.is_empty()
530 && left_path.remaining_path.is_empty()
531 {
532 operand_to_val_str(&cmp.left)
533 } else {
534 left_path.path_prefix + &left_path.remaining_path
535 };
536 let r = if right.is_empty() {
537 operand_to_path_expr(&cmp.right)
538 } else {
539 right
540 };
541 format!("{} {} {}", l, op, r)
542 }
543 }
544 }
545 }
546 }
547}
548
549fn path_or_literal(operand: &ecel::Operand) -> String {
552 match operand {
553 ecel::Operand::Path(_) => operand_to_path_expr(operand),
554 ecel::Operand::Literal(lit) => literal_to_val_str(lit),
555 }
556}
557
558fn array_or_literal(operand: &ecel::Operand) -> String {
560 match operand {
561 ecel::Operand::Path(_) => operand_to_path_expr(operand),
562 ecel::Operand::Literal(lit) => literal_to_val_str(lit),
563 }
564}
565
566fn literal_of(operand: &ecel::Operand) -> ecel::Literal {
567 match operand {
568 ecel::Operand::Literal(lit) => lit.clone(),
569 ecel::Operand::Path(_) => ecel::Literal::String(String::new()),
570 }
571}
572
573struct PathParts {
574 path_prefix: String,
575 remaining_path: String,
576}
577
578fn build_path_expression(operand: &ecel::Operand) -> (PathParts, bool) {
579 match operand {
580 ecel::Operand::Path(path_expr) => {
581 let segments = &path_expr.segments;
582 let mut pre_wildcard = Vec::new();
583 let mut post_wildcard = Vec::new();
584 let mut has_wildcard = false;
585
586 for (i, seg) in segments.iter().enumerate() {
587 if i == 0 {
588 continue;
589 }
590 if has_wildcard {
591 post_wildcard.push(seg.clone());
592 } else if matches!(seg, ecel::PathSegment::Wildcard) {
593 has_wildcard = true;
594 } else {
595 pre_wildcard.push(seg.clone());
596 }
597 }
598
599 let mut prefix = String::from("message");
600 for seg in &pre_wildcard {
601 match seg {
602 ecel::PathSegment::Field(name) => {
603 prefix.push('.');
604 prefix.push_str(&to_snake_case(name));
605 }
606 ecel::PathSegment::Index(idx) => {
607 prefix.push_str(&format!("[{}]", idx));
608 }
609 ecel::PathSegment::QuotedKey(name) => {
610 prefix.push_str(&format!("[\"{}\"]", name));
611 }
612 _ => {}
613 }
614 }
615
616 let mut suffix = String::new();
617 for seg in &post_wildcard {
618 match seg {
619 ecel::PathSegment::Field(name) => {
620 suffix.push('.');
621 suffix.push_str(&to_snake_case(name));
622 }
623 ecel::PathSegment::Index(idx) => {
624 suffix.push_str(&format!("[{}]", idx));
625 }
626 ecel::PathSegment::QuotedKey(name) => {
627 suffix.push_str(&format!("[\"{}\"]", name));
628 }
629 _ => {}
630 }
631 }
632
633 (
634 PathParts {
635 path_prefix: prefix,
636 remaining_path: suffix,
637 },
638 has_wildcard,
639 )
640 }
641 ecel::Operand::Literal(_) => (
642 PathParts {
643 path_prefix: String::new(),
644 remaining_path: String::new(),
645 },
646 false,
647 ),
648 }
649}
650
651fn operand_to_path_expr(operand: &ecel::Operand) -> String {
652 match operand {
653 ecel::Operand::Path(path) => {
654 let segments = &path.segments;
655 let mut out = String::from("message");
656 for seg in segments.iter().skip(1) {
657 match seg {
658 ecel::PathSegment::Field(name) => {
659 out.push('.');
660 out.push_str(&to_snake_case(name));
661 }
662 ecel::PathSegment::Wildcard => {}
663 ecel::PathSegment::Index(idx) => {
664 out.push_str(&format!("[{}]", idx));
665 }
666 ecel::PathSegment::QuotedKey(name) => {
667 out.push_str(&format!("[\"{}\"]", name));
668 }
669 }
670 }
671 out
672 }
673 ecel::Operand::Literal(_) => String::new(),
674 }
675}
676
677fn operand_to_val_str(operand: &ecel::Operand) -> String {
678 match operand {
679 ecel::Operand::Path(_) => "".to_string(),
680 ecel::Operand::Literal(lit) => literal_to_val_str(lit),
681 }
682}
683
684fn literal_to_val_str(lit: &ecel::Literal) -> String {
685 match lit {
686 ecel::Literal::Number(n) => n.to_string(),
687 ecel::Literal::String(s) => format!("\"{}\"", s),
688 ecel::Literal::Bool(b) => b.to_string(),
689 ecel::Literal::Null => "None".to_string(),
690 ecel::Literal::Array(items) => {
691 let inner: Vec<String> = items.iter().map(literal_to_val_str).collect();
692 format!("vec![{}]", inner.join(", "))
693 }
694 }
695}
696
697fn comparator_str(op: &ecel::Comparator) -> &str {
703 match op {
704 ecel::Comparator::Eq => "==",
705 ecel::Comparator::Neq => "!=",
706 ecel::Comparator::Gte => ">=",
707 ecel::Comparator::Lte => "<=",
708 ecel::Comparator::Gt => ">",
709 ecel::Comparator::Lt => "<",
710 ecel::Comparator::In => "in",
711 ecel::Comparator::Matches => "matches",
712 }
713}
714
715fn ref_to_rust_type(ext_ref: &etdl_parser::ast::ExternalRef) -> String {
716 extract_last_segment(ext_ref)
717}
718
719fn extract_last_segment(ext_ref: &etdl_parser::ast::ExternalRef) -> String {
720 let pointer = &ext_ref.pointer;
721 let parts: Vec<&str> = pointer.split('/').collect();
722 let last = parts.last().unwrap_or(&"Unknown");
723 to_pascal_case(last)
724}
725
726fn get_branch_prob(
727 branch: &etdl_parser::ast::Branch,
728 _node_id: &str,
729 fault_tree_probs: &BTreeMap<String, f64>,
730) -> Option<f64> {
731 if let Some(ref ps) = branch.probability_source {
732 let ft_id = extract_ft_id(&ps.pointer);
733 return fault_tree_probs.get(&ft_id).copied();
734 }
735 branch.effective_probability()
736}
737
738fn extract_ft_id(pointer: &str) -> String {
739 pointer
740 .trim_start_matches("#/faultTrees/")
741 .trim_end_matches("/topEvent")
742 .to_string()
743}
744
745fn to_snake_case(s: &str) -> String {
746 let mut result = String::new();
747 for (i, c) in s.chars().enumerate() {
748 if c.is_uppercase() {
749 if i > 0 {
750 result.push('_');
751 }
752 result.push(c.to_lowercase().next().unwrap());
753 } else {
754 result.push(c);
755 }
756 }
757 result
758}
759
760fn to_upper_snake(s: &str) -> String {
761 let snake = to_snake_case(s);
762 snake.to_uppercase()
763}
764
765fn to_pascal_case(s: &str) -> String {
766 let mut result = String::new();
767 let mut capitalize = true;
768 for c in s.chars() {
769 if c == '_' || c == '-' || c == ' ' {
770 capitalize = true;
771 } else if capitalize {
772 result.extend(c.to_uppercase());
773 capitalize = false;
774 } else {
775 result.push(c);
776 }
777 }
778 result
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784 use etdl_parser::ast::{EtlDocument, InternalRef};
785
786 fn parse(yaml: &str) -> EtlDocument {
787 serde_yaml::from_str(yaml).expect("valid yaml")
788 }
789
790 fn multi_ft_doc() -> EtlDocument {
791 parse(
792 r##"
793etdl: "1.0.0"
794info:
795 title: "MultiFT"
796 version: "1.0.0"
797 domain: "D"
798asyncapi_imports: {}
799eventTrees:
800 T:
801 initiatingEvent:
802 id: I
803 message: "a#/m"
804 next: O
805 nodes:
806 O:
807 type: operation
808 action: execute
809 handler: "h"
810 next: C
811 onFailure: FC
812 onFailureProbabilitySource: "#/faultTrees/B/topEvent"
813 C:
814 type: consequence
815 operation: terminate
816 FC:
817 type: consequence
818 operation: terminate
819faultTrees:
820 A:
821 topEvent:
822 id: A1
823 description: "a"
824 rootCause: AE
825 basicEvents:
826 AE:
827 description: "ae"
828 probability: 0.9
829 B:
830 topEvent:
831 id: B1
832 description: "b"
833 rootCause: BE
834 basicEvents:
835 BE:
836 description: "be"
837 probability: 0.01
838"##,
839 )
840 }
841
842 #[test]
843 fn find_fault_tree_prob_selects_by_pointer() {
844 let doc = multi_ft_doc();
845 let probs = crate::fault_tree::resolve_fault_trees(&doc, &mut Vec::new());
846 assert_eq!(probs["A"], 0.9);
847 assert_eq!(probs["B"], 0.01);
848
849 let ps = InternalRef {
851 pointer: "#/faultTrees/B/topEvent".to_string(),
852 };
853 let (id, prob) = find_fault_tree_prob(&ps, &probs).expect("resolves");
854 assert_eq!(id, "B");
855 assert!((prob - 0.01).abs() < 1e-9);
856 }
857
858 #[test]
859 fn generated_constants_use_correct_tree() {
860 let doc = multi_ft_doc();
861 let probs = crate::fault_tree::resolve_fault_trees(&doc, &mut Vec::new());
862 let constants = generate_fault_tree_constants(&doc, &probs);
863 assert!(constants.contains("faultTrees.B.topEvent"));
864 assert!(constants.contains("= 0.010000"));
865 assert!(!constants.contains("faultTrees.A.topEvent"));
866 }
867
868 #[test]
869 fn in_operator_lowers_to_contains() {
870 let cond = etdl_parser::ecel::parse_condition(
871 "message.payload.status in [\"PAID\", \"AUTHORIZED\"]",
872 )
873 .unwrap();
874 let code = condition_to_rust_code(&cond);
875 assert!(
876 code.contains("etdl_core::condition::contains"),
877 "got: {}",
878 code
879 );
880 assert!(code.contains("\"PAID\""));
881 }
882
883 #[test]
884 fn matches_operator_lowers_to_regex() {
885 let cond = etdl_parser::ecel::parse_condition(
886 "message.payload.reference matches \"^ORD-[0-9]{8}$\"",
887 )
888 .unwrap();
889 let code = condition_to_rust_code(&cond);
890 assert!(
891 code.contains("etdl_core::condition::matches"),
892 "got: {}",
893 code
894 );
895 }
896
897 #[test]
898 fn comparison_emits_valid_rust() {
899 let cond = etdl_parser::ecel::parse_condition("message.payload.amount >= 10000").unwrap();
900 let code = condition_to_rust_code(&cond);
901 assert_eq!(code, "message.payload.amount >= 10000");
902 }
903}