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, GeneratedFile};
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 target_name(&self) -> &'static str {
30 "rust"
31 }
32
33 fn generate_all(
34 &self,
35 doc: &EtlDocument,
36 fault_tree_probs: &BTreeMap<String, f64>,
37 _registry: &AsyncApiRegistry,
38 stem: &str,
39 _diagnostics: &mut Vec<Diagnostic>,
40 ) -> Result<Vec<GeneratedFile>, String> {
41 let mut output = String::new();
42
43 output.push_str(&format!(
44 "// AUTOGENERATED BY ETDL COMPILER v{} - DO NOT EDIT DIRECTLY\n\n",
45 self.version
46 ));
47
48 let imports = collect_imports(doc);
49 output.push_str(&imports);
50 output.push('\n');
51
52 let inline_types = generate_inline_message_types(doc);
53 if !inline_types.is_empty() {
54 output.push_str(&inline_types);
55 }
56
57 let constants = generate_fault_tree_constants(doc, fault_tree_probs);
58 output.push_str(&constants);
59
60 for tree in doc.event_trees.values() {
61 let handler_code = generate_event_tree_handler(doc, tree, fault_tree_probs)?;
62 output.push_str(&handler_code);
63 output.push('\n');
64 }
65
66 Ok(vec![GeneratedFile::new(format!("{stem}.rs"), output)])
67 }
68}
69
70fn collect_imports(doc: &EtlDocument) -> String {
71 let mut imports = String::from(
72 "use std::time::Duration;\n\
73 use etdl_core::BranchMonitor;\n\
74 use etdl_core::condition::{contains, matches};\n\
75 use etdl_core::publisher::Publisher;\n\
76 use etdl_core::retry::{RetryPolicy, BackoffStrategy};\n\
77 use etdl_core::WorkflowError;\n\
78 use serde::Serialize;\n",
79 );
80
81 let mut alias_set: BTreeMap<String, bool> = BTreeMap::new();
82
83 for tree in doc.event_trees.values() {
84 if let etdl_parser::ast::MessageRef::External(ref ext_ref) = tree.initiating_event.message
85 {
86 alias_set.insert(ext_ref.alias.clone(), true);
87 }
88
89 for node in tree.nodes.values() {
90 match node {
91 Node::Operation(op) => {
92 if let Some(etdl_parser::ast::MessageRef::External(ref ext_ref)) = op.emits {
93 alias_set.insert(ext_ref.alias.clone(), true);
94 }
95 }
96 Node::Consequence(cons) => {
97 if let Some(etdl_parser::ast::ChannelRef::External(ref ext_ref)) =
98 cons.channel
99 {
100 alias_set.insert(ext_ref.alias.clone(), true);
101 }
102 if let Some(etdl_parser::ast::MessageRef::External(ref ext_ref)) =
103 cons.message
104 {
105 alias_set.insert(ext_ref.alias.clone(), true);
106 }
107 }
108 _ => {}
109 }
110 }
111 }
112
113 for alias in alias_set.keys() {
114 let mod_name = alias.replace('-', "_");
115 imports.push_str(&format!("use {}::messages::*;\n", mod_name));
116 }
117
118 imports
119}
120
121fn generate_fault_tree_constants(
122 doc: &EtlDocument,
123 fault_tree_probs: &BTreeMap<String, f64>,
124) -> String {
125 let mut output = String::new();
126
127 for tree in doc.event_trees.values() {
128 for (node_id, node) in &tree.nodes {
129 if let Node::Operation(op) = node {
130 if let Some(ref ps) = op.on_failure_probability_source {
131 if let Some((ft_id, prob)) = find_fault_tree_prob(ps, fault_tree_probs) {
132 output.push_str(&format!(
133 "// Computed from faultTrees.{}.topEvent at build time (Section 5.16)\n",
134 ft_id
135 ));
136 let const_name =
137 to_upper_snake(&format!("{}_failure_probability", node_id));
138 output.push_str(&format!("const {}: f64 = {:.6};\n\n", const_name, prob));
139 }
140 }
141 }
142 }
143 }
144
145 output
146}
147
148fn find_fault_tree_prob(
152 ps: &etdl_parser::ast::InternalRef,
153 fault_tree_probs: &BTreeMap<String, f64>,
154) -> Option<(String, f64)> {
155 let ft_id = extract_ft_id(&ps.pointer);
156 fault_tree_probs.get(&ft_id).map(|&v| (ft_id.clone(), v))
157}
158
159fn generate_event_tree_handler(
160 doc: &EtlDocument,
161 tree: &EventTree,
162 fault_tree_probs: &BTreeMap<String, f64>,
163) -> Result<String, String> {
164 let mut output = String::new();
165
166 let fn_name = format!("handle_{}", to_snake_case(&tree.initiating_event.id));
167 let message_type = ref_to_rust_type(&tree.initiating_event.message);
168
169 output.push_str(&format!(
170 "pub async fn {}(message: {}, publisher: &dyn Publisher) -> Result<(), WorkflowError> {{\n",
171 fn_name, message_type
172 ));
173
174 let first_barrier = find_first_barrier(tree);
175 let monitor_name = first_barrier
176 .map(to_snake_case)
177 .unwrap_or_else(|| "monitor".to_string());
178
179 output.push_str(&format!(
180 " let mut {} = BranchMonitor::new(\"{}\");\n\n",
181 monitor_name,
182 first_barrier.unwrap_or("root")
183 ));
184
185 let start_node_id = &tree.initiating_event.next;
186 let body = generate_node_code(doc, tree, start_node_id, 1, fault_tree_probs, &monitor_name)?;
187 output.push_str(&body);
188
189 output.push_str(" Ok(())\n");
190 output.push_str("}\n");
191
192 Ok(output)
193}
194
195fn find_first_barrier(tree: &EventTree) -> Option<&str> {
196 let mut current = &tree.initiating_event.next;
197 loop {
198 match tree.nodes.get(current.as_str()) {
199 Some(Node::Barrier(_)) => return Some(current.as_str()),
200 Some(Node::Operation(op)) => {
201 current = &op.next;
202 }
203 Some(Node::Consequence(_)) => return None,
204 None => return None,
205 }
206 }
207}
208
209fn generate_node_code(
210 doc: &EtlDocument,
211 tree: &EventTree,
212 node_id: &str,
213 depth: usize,
214 fault_tree_probs: &BTreeMap<String, f64>,
215 monitor_name: &str,
216) -> Result<String, String> {
217 let node = tree
218 .nodes
219 .get(node_id)
220 .ok_or_else(|| format!("node '{}' not found", node_id))?;
221
222 match node {
223 Node::Barrier(barrier) => generate_barrier_code(
224 tree,
225 node_id,
226 barrier,
227 depth,
228 doc,
229 fault_tree_probs,
230 monitor_name,
231 ),
232 Node::Operation(op) => generate_operation_code(
233 tree,
234 node_id,
235 op,
236 depth,
237 doc,
238 fault_tree_probs,
239 monitor_name,
240 ),
241 Node::Consequence(cons) => generate_consequence_code(cons, depth),
242 }
243}
244
245fn generate_barrier_code(
246 tree: &EventTree,
247 node_id: &str,
248 barrier: &etdl_parser::ast::Barrier,
249 depth: usize,
250 doc: &EtlDocument,
251 fault_tree_probs: &BTreeMap<String, f64>,
252 monitor_name: &str,
253) -> Result<String, String> {
254 let indent = " ".repeat(depth);
255 let mut output = String::new();
256
257 for (i, branch) in barrier.branches.iter().enumerate() {
258 if i == 0 {
259 if branch.condition == Condition::Default {
260 let prob = get_branch_prob(branch, node_id, fault_tree_probs);
261 if let Some(p) = prob {
262 output.push_str(&format!(
263 "{} {}.record_branch(\"{}\", {:.6});\n",
264 indent, monitor_name, branch.outcome, p
265 ));
266 }
267 let body = generate_node_code(
268 doc,
269 tree,
270 &branch.next,
271 depth + 1,
272 fault_tree_probs,
273 monitor_name,
274 )?;
275 output.push_str(&body);
276 } else {
277 let cond = condition_to_rust_code(&branch.condition);
278 output.push_str(&format!("{}if {} {{\n", indent, cond));
279
280 let prob = get_branch_prob(branch, node_id, fault_tree_probs);
281 if let Some(p) = prob {
282 output.push_str(&format!(
283 "{} {}.record_branch(\"{}\", {:.6});\n",
284 indent, monitor_name, branch.outcome, p
285 ));
286 }
287
288 let body = generate_node_code(
289 doc,
290 tree,
291 &branch.next,
292 depth + 1,
293 fault_tree_probs,
294 monitor_name,
295 )?;
296 output.push_str(&body);
297 output.push_str(&format!("{}}}", indent));
298 }
299 } else if branch.condition == Condition::Default {
300 output.push_str(" else {\n");
301
302 let prob = get_branch_prob(branch, node_id, fault_tree_probs);
303 if let Some(p) = prob {
304 output.push_str(&format!(
305 "{} {}.record_branch(\"{}\", {:.6});\n",
306 indent, monitor_name, branch.outcome, p
307 ));
308 }
309
310 let body = generate_node_code(
311 doc,
312 tree,
313 &branch.next,
314 depth + 1,
315 fault_tree_probs,
316 monitor_name,
317 )?;
318 output.push_str(&body);
319 output.push_str(&format!("{}}}\n", indent));
320 } else {
321 let cond = condition_to_rust_code(&branch.condition);
322 output.push_str(&format!(" else if {} {{\n", cond));
323
324 let prob = get_branch_prob(branch, node_id, fault_tree_probs);
325 if let Some(p) = prob {
326 output.push_str(&format!(
327 "{} {}.record_branch(\"{}\", {:.6});\n",
328 indent, monitor_name, branch.outcome, p
329 ));
330 }
331
332 let body = generate_node_code(
333 doc,
334 tree,
335 &branch.next,
336 depth + 1,
337 fault_tree_probs,
338 monitor_name,
339 )?;
340 output.push_str(&body);
341 output.push_str(&format!("{}}}", indent));
342 }
343 }
344
345 Ok(output)
346}
347
348fn generate_operation_code(
349 tree: &EventTree,
350 node_id: &str,
351 op: &etdl_parser::ast::Operation,
352 depth: usize,
353 doc: &EtlDocument,
354 fault_tree_probs: &BTreeMap<String, f64>,
355 monitor_name: &str,
356) -> Result<String, String> {
357 let indent = " ".repeat(depth);
358 let mut output = String::new();
359
360 let handler_name = to_snake_case(&op.handler);
361 let timeout = op.timeout_ms.unwrap_or(5000);
362
363 if let Some(ref retry) = op.retry_policy {
364 let strategy = match retry
365 .backoff_strategy
366 .as_ref()
367 .unwrap_or(&BackoffStrategy::Fixed)
368 {
369 BackoffStrategy::Exponential => "BackoffStrategy::Exponential",
370 BackoffStrategy::Fixed => "BackoffStrategy::Fixed",
371 };
372 output.push_str(&format!(
373 "{}let retry = RetryPolicy {{\n\
374 {} max_attempts: {},\n\
375 {} backoff_ms: {},\n\
376 {} strategy: {},\n\
377 {}}};\n",
378 indent, indent, retry.max_attempts, indent, retry.backoff_ms, indent, strategy, indent
379 ));
380 output.push_str(&format!(
381 "{}match retry.execute(|| {}(&message), Duration::from_millis({})).await {{\n",
382 indent, handler_name, timeout
383 ));
384 } else {
385 output.push_str(&format!(
386 "{}match {}(&message).await {{\n",
387 indent, handler_name
388 ));
389 }
390
391 output.push_str(&format!("{} Ok(_result) => {{\n", indent));
392
393 if op.on_failure.is_some() {
401 if let Some(ref ps) = op.on_failure_probability_source {
402 let const_name = to_upper_snake(&format!("{}_failure_probability", node_id));
403 let prob_exists = find_fault_tree_prob(ps, fault_tree_probs).is_some();
404 if prob_exists {
405 output.push_str(&format!(
406 "{} {}.record_success(\"{}\", Some({}));\n",
407 indent, monitor_name, node_id, const_name
408 ));
409 } else {
410 output.push_str(&format!(
411 "{} {}.record_success(\"{}\", None);\n",
412 indent, monitor_name, node_id
413 ));
414 }
415 }
416 }
417
418 let next_node = tree.nodes.get(&op.next);
419 match next_node {
420 Some(Node::Consequence(cons)) => {
421 emit_send(cons, "_result", &indent, &mut output);
422 }
423 Some(_) => {
424 generate_node_code(
425 doc,
426 tree,
427 &op.next,
428 depth + 2,
429 fault_tree_probs,
430 monitor_name,
431 )
432 .map(|body| output.push_str(&body))?;
433 }
434 None => {}
435 }
436
437 output.push_str(&format!("{} }}\n", indent));
438
439 if let Some(ref _on_failure_id) = op.on_failure {
440 output.push_str(&format!("{} Err(err) => {{\n", indent));
441
442 if let Some(ref ps) = op.on_failure_probability_source {
443 let const_name = to_upper_snake(&format!("{}_failure_probability", node_id));
444 let prob_exists = find_fault_tree_prob(ps, fault_tree_probs).is_some();
445 if prob_exists {
446 output.push_str(&format!(
447 "{} {}.record_failure(\"{}\", &err, Some({}));\n",
448 indent, monitor_name, node_id, const_name
449 ));
450 } else {
451 output.push_str(&format!(
452 "{} {}.record_failure(\"{}\", &err, None);\n",
453 indent, monitor_name, node_id
454 ));
455 }
456 } else {
457 output.push_str(&format!(
458 "{} {}.record_failure(\"{}\", &err, None);\n",
459 indent, monitor_name, node_id
460 ));
461 }
462
463 let on_failure_id = op.on_failure.as_ref().unwrap();
464 match tree.nodes.get(on_failure_id) {
465 Some(Node::Consequence(cons)) => {
466 emit_send(cons, "message", &indent, &mut output);
467 }
468 _ => {
469 generate_node_code(
470 doc,
471 tree,
472 on_failure_id,
473 depth + 2,
474 fault_tree_probs,
475 monitor_name,
476 )
477 .map(|body| output.push_str(&body))?;
478 }
479 }
480
481 output.push_str(&format!("{} }}\n", indent));
482 } else {
483 output.push_str(&format!(
484 "{} Err(err) => return Err(WorkflowError::new(format!(\"{{}}\", err))),\n",
485 indent
486 ));
487 }
488
489 output.push_str(&format!("{}}}\n", indent));
490
491 Ok(output)
492}
493
494fn emit_send(
496 cons: &etdl_parser::ast::Consequence,
497 payload_expr: &str,
498 indent: &str,
499 output: &mut String,
500) {
501 match cons.consequence_operation {
502 etdl_parser::ast::ConsequenceOperation::Send => {
503 if let Some(ref channel_ref) = cons.channel {
504 let channel_name = channel_ref_name(channel_ref);
505 output.push_str(&format!(
506 "{} publisher.publish(\"{}\", &etdl_core::serde_json::to_value({}).map_err(|e| WorkflowError::new(format!(\"{{}}\", e)))?)?;\n",
507 indent, channel_name, payload_expr
508 ));
509 }
510 }
511 etdl_parser::ast::ConsequenceOperation::Terminate => {}
512 }
513}
514
515fn generate_consequence_code(
516 cons: &etdl_parser::ast::Consequence,
517 depth: usize,
518) -> Result<String, String> {
519 let indent = " ".repeat(depth);
520 let mut output = String::new();
521
522 match cons.consequence_operation {
523 etdl_parser::ast::ConsequenceOperation::Send => {
524 if let Some(ref channel_ref) = cons.channel {
525 let channel_name = channel_ref_name(channel_ref);
526 output.push_str(&format!(
527 "{}publisher.publish(\"{}\", &etdl_core::serde_json::to_value(message).map_err(|e| WorkflowError::new(format!(\"{{}}\", e)))?)?;\n",
528 indent, channel_name
529 ));
530 }
531 }
532 etdl_parser::ast::ConsequenceOperation::Terminate => {}
533 }
534
535 if depth == 1 {
536 output.push_str(&format!("{}Ok(())\n", indent));
537 }
538
539 Ok(output)
540}
541
542fn condition_to_rust_code(condition: &Condition) -> String {
543 match condition {
544 Condition::Default => "true".to_string(),
545 Condition::Comparison(cmp) => {
546 use ecel::Comparator as C;
547 match cmp.op {
548 C::In => {
550 let left = path_or_literal(&cmp.left);
551 let right = array_or_literal(&cmp.right);
552 format!("etdl_core::condition::contains(&{}, &{})", right, left)
553 }
554 C::Matches => {
555 let left = match &cmp.left {
567 ecel::Operand::Path(_) => format!("&{}", operand_to_path_expr(&cmp.left)),
568 ecel::Operand::Literal(lit) => literal_to_val_str(lit),
569 };
570 let right = literal_to_val_str(&literal_of(&cmp.right));
571 format!("etdl_core::condition::matches({}, {})", left, right)
572 }
573 _ => {
574 let (left_path, has_wildcard) = build_path_expression(&cmp.left);
575 let right = operand_to_val_str(&cmp.right);
576 let op = comparator_str(&cmp.op);
577
578 if has_wildcard {
579 format!(
581 "{}.iter().all(|item| item{} {} {})",
582 left_path.path_prefix, left_path.remaining_path, op, right
583 )
584 } else {
585 let l = if left_path.path_prefix.is_empty()
586 && left_path.remaining_path.is_empty()
587 {
588 operand_to_val_str(&cmp.left)
589 } else {
590 left_path.path_prefix + &left_path.remaining_path
591 };
592 let r = if right.is_empty() {
593 operand_to_path_expr(&cmp.right)
594 } else {
595 right
596 };
597 format!("{} {} {}", l, op, r)
598 }
599 }
600 }
601 }
602 }
603}
604
605fn path_or_literal(operand: &ecel::Operand) -> String {
608 match operand {
609 ecel::Operand::Path(_) => operand_to_path_expr(operand),
610 ecel::Operand::Literal(lit) => literal_to_val_str(lit),
611 }
612}
613
614fn array_or_literal(operand: &ecel::Operand) -> String {
616 match operand {
617 ecel::Operand::Path(_) => operand_to_path_expr(operand),
618 ecel::Operand::Literal(lit) => literal_to_val_str(lit),
619 }
620}
621
622fn literal_of(operand: &ecel::Operand) -> ecel::Literal {
623 match operand {
624 ecel::Operand::Literal(lit) => lit.clone(),
625 ecel::Operand::Path(_) => ecel::Literal::String(String::new()),
626 }
627}
628
629struct PathParts {
630 path_prefix: String,
631 remaining_path: String,
632}
633
634fn build_path_expression(operand: &ecel::Operand) -> (PathParts, bool) {
635 match operand {
636 ecel::Operand::Path(path_expr) => {
637 let segments = &path_expr.segments;
638 let mut pre_wildcard = Vec::new();
639 let mut post_wildcard = Vec::new();
640 let mut has_wildcard = false;
641
642 for (i, seg) in segments.iter().enumerate() {
643 if i == 0 {
644 continue;
645 }
646 if has_wildcard {
647 post_wildcard.push(seg.clone());
648 } else if matches!(seg, ecel::PathSegment::Wildcard) {
649 has_wildcard = true;
650 } else {
651 pre_wildcard.push(seg.clone());
652 }
653 }
654
655 let mut prefix = String::from("message");
656 for seg in &pre_wildcard {
657 match seg {
658 ecel::PathSegment::Field(name) => {
659 prefix.push('.');
660 prefix.push_str(&to_snake_case(name));
661 }
662 ecel::PathSegment::Index(idx) => {
663 prefix.push_str(&format!("[{}]", idx));
664 }
665 ecel::PathSegment::QuotedKey(name) => {
666 prefix.push_str(&format!("[\"{}\"]", name));
667 }
668 _ => {}
669 }
670 }
671
672 let mut suffix = String::new();
673 for seg in &post_wildcard {
674 match seg {
675 ecel::PathSegment::Field(name) => {
676 suffix.push('.');
677 suffix.push_str(&to_snake_case(name));
678 }
679 ecel::PathSegment::Index(idx) => {
680 suffix.push_str(&format!("[{}]", idx));
681 }
682 ecel::PathSegment::QuotedKey(name) => {
683 suffix.push_str(&format!("[\"{}\"]", name));
684 }
685 _ => {}
686 }
687 }
688
689 (
690 PathParts {
691 path_prefix: prefix,
692 remaining_path: suffix,
693 },
694 has_wildcard,
695 )
696 }
697 ecel::Operand::Literal(_) => (
698 PathParts {
699 path_prefix: String::new(),
700 remaining_path: String::new(),
701 },
702 false,
703 ),
704 }
705}
706
707fn operand_to_path_expr(operand: &ecel::Operand) -> String {
708 match operand {
709 ecel::Operand::Path(path) => {
710 let segments = &path.segments;
711 let mut out = String::from("message");
712 for seg in segments.iter().skip(1) {
713 match seg {
714 ecel::PathSegment::Field(name) => {
715 out.push('.');
716 out.push_str(&to_snake_case(name));
717 }
718 ecel::PathSegment::Wildcard => {}
719 ecel::PathSegment::Index(idx) => {
720 out.push_str(&format!("[{}]", idx));
721 }
722 ecel::PathSegment::QuotedKey(name) => {
723 out.push_str(&format!("[\"{}\"]", name));
724 }
725 }
726 }
727 out
728 }
729 ecel::Operand::Literal(_) => String::new(),
730 }
731}
732
733fn operand_to_val_str(operand: &ecel::Operand) -> String {
734 match operand {
735 ecel::Operand::Path(_) => "".to_string(),
736 ecel::Operand::Literal(lit) => literal_to_val_str(lit),
737 }
738}
739
740fn literal_to_val_str(lit: &ecel::Literal) -> String {
741 match lit {
742 ecel::Literal::Number(n) => n.to_string(),
743 ecel::Literal::String(s) => format!("\"{}\"", s),
744 ecel::Literal::Bool(b) => b.to_string(),
745 ecel::Literal::Null => "None".to_string(),
746 ecel::Literal::Array(items) => {
747 let inner: Vec<String> = items.iter().map(literal_to_val_str).collect();
748 format!("vec![{}]", inner.join(", "))
749 }
750 }
751}
752
753fn comparator_str(op: &ecel::Comparator) -> &str {
759 match op {
760 ecel::Comparator::Eq => "==",
761 ecel::Comparator::Neq => "!=",
762 ecel::Comparator::Gte => ">=",
763 ecel::Comparator::Lte => "<=",
764 ecel::Comparator::Gt => ">",
765 ecel::Comparator::Lt => "<",
766 ecel::Comparator::In => "in",
767 ecel::Comparator::Matches => "matches",
768 }
769}
770
771fn ref_to_rust_type(msg_ref: &etdl_parser::ast::MessageRef) -> String {
772 let pointer = match msg_ref {
773 etdl_parser::ast::MessageRef::External(r) => &r.pointer,
774 etdl_parser::ast::MessageRef::Internal(r) => &r.pointer,
775 };
776 extract_last_segment_str(pointer)
777}
778
779fn channel_ref_name(channel_ref: &etdl_parser::ast::ChannelRef) -> String {
786 match channel_ref {
787 etdl_parser::ast::ChannelRef::External(ext_ref) => {
788 extract_last_segment_str(&ext_ref.pointer)
789 }
790 etdl_parser::ast::ChannelRef::Bare(name) => name.clone(),
791 }
792}
793
794fn extract_last_segment_str(pointer: &str) -> String {
795 let parts: Vec<&str> = pointer.split('/').collect();
796 let last = parts.last().unwrap_or(&"Unknown");
797 to_pascal_case(last)
798}
799
800fn collect_internal_message_ids(doc: &EtlDocument) -> std::collections::BTreeSet<String> {
806 use etdl_parser::ast::MessageRef;
807
808 fn note(msg_ref: &MessageRef, ids: &mut std::collections::BTreeSet<String>) {
809 if let MessageRef::Internal(int_ref) = msg_ref {
810 if let Some(id) = int_ref.pointer.strip_prefix("#/components/messages/") {
811 ids.insert(id.to_string());
812 }
813 }
814 }
815
816 let mut ids = std::collections::BTreeSet::new();
817
818 for tree in doc.event_trees.values() {
819 note(&tree.initiating_event.message, &mut ids);
820
821 for node in tree.nodes.values() {
822 match node {
823 Node::Operation(op) => {
824 if let Some(ref m) = op.emits {
825 note(m, &mut ids);
826 }
827 }
828 Node::Consequence(cons) => {
829 if let Some(ref m) = cons.message {
830 note(m, &mut ids);
831 }
832 }
833 _ => {}
834 }
835 }
836 }
837
838 if let Some(ref fault_trees) = doc.fault_trees {
839 for ft in fault_trees.values() {
840 if let Some(ref m) = ft.top_event.message {
841 note(m, &mut ids);
842 }
843 for be in ft.basic_events.values() {
844 if let Some(ref m) = be.message {
845 note(m, &mut ids);
846 }
847 }
848 }
849 }
850
851 ids
852}
853
854fn generate_inline_message_types(doc: &EtlDocument) -> String {
860 let ids = collect_internal_message_ids(doc);
861 if ids.is_empty() {
862 return String::new();
863 }
864
865 let messages = doc.components.as_ref().and_then(|c| c.messages.as_ref());
866
867 let mut output = String::new();
868 for id in &ids {
869 let Some(message) = messages.and_then(|m| m.get(id)) else {
870 continue;
871 };
872 generate_message_envelope(id, message, &mut output);
873 }
874 output
875}
876
877fn generate_message_envelope(id: &str, message: &etdl_parser::ast::Message, output: &mut String) {
886 let type_name = to_pascal_case(id);
887 let payload_type_name = format!("{}Payload", type_name);
888 let payload: serde_json::Value =
889 serde_json::to_value(&message.payload).unwrap_or(serde_json::Value::Null);
890
891 let mut nested = String::new();
892 let payload_rust_type = schema_to_rust_type(&payload_type_name, &payload, &mut nested);
893 output.push_str(&nested);
894
895 output.push_str("#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n");
896 output.push_str(&format!("pub struct {} {{\n", type_name));
897 output.push_str(&format!(" pub payload: {},\n", payload_rust_type));
898 output.push_str(" #[serde(default)]\n");
899 output.push_str(" pub headers: Option<serde_json::Value>,\n");
900 output.push_str("}\n\n");
901}
902
903fn generate_struct_from_schema(name: &str, schema: &serde_json::Value, output: &mut String) {
911 let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) else {
912 output.push_str(&format!(
913 "#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct {}(pub serde_json::Value);\n\n",
914 name
915 ));
916 return;
917 };
918
919 let required: std::collections::BTreeSet<&str> = schema
920 .get("required")
921 .and_then(|r| r.as_array())
922 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
923 .unwrap_or_default();
924
925 let mut nested = String::new();
926 let mut fields = String::new();
927
928 for (field_name, field_schema) in properties {
929 let snake = to_snake_case(field_name);
930 let nested_type_name = format!("{}{}", name, to_pascal_case(field_name));
931 let rust_type = schema_to_rust_type(&nested_type_name, field_schema, &mut nested);
932 let is_required = required.contains(field_name.as_str());
933
934 if snake != *field_name {
935 fields.push_str(&format!(" #[serde(rename = \"{}\")]\n", field_name));
936 }
937
938 if is_required {
939 fields.push_str(&format!(" pub {}: {},\n", snake, rust_type));
940 } else {
941 fields.push_str(" #[serde(default)]\n");
942 fields.push_str(&format!(" pub {}: Option<{}>,\n", snake, rust_type));
943 }
944 }
945
946 output.push_str(&nested);
947 output.push_str("#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n");
948 output.push_str(&format!("pub struct {} {{\n", name));
949 output.push_str(&fields);
950 output.push_str("}\n\n");
951}
952
953fn schema_to_rust_type(candidate_name: &str, schema: &serde_json::Value, nested: &mut String) -> String {
958 match schema.get("type").and_then(|t| t.as_str()) {
959 Some("string") => "String".to_string(),
960 Some("integer") => "i64".to_string(),
961 Some("number") => "f64".to_string(),
962 Some("boolean") => "bool".to_string(),
963 Some("array") => {
964 let item_type = match schema.get("items") {
965 Some(items_schema) => {
966 schema_to_rust_type(&format!("{}Item", candidate_name), items_schema, nested)
967 }
968 None => "serde_json::Value".to_string(),
969 };
970 format!("Vec<{}>", item_type)
971 }
972 Some("object") => {
973 if schema.get("properties").is_some() {
974 generate_struct_from_schema(candidate_name, schema, nested);
975 candidate_name.to_string()
976 } else {
977 "serde_json::Value".to_string()
978 }
979 }
980 _ => "serde_json::Value".to_string(),
981 }
982}
983
984fn get_branch_prob(
985 branch: &etdl_parser::ast::Branch,
986 _node_id: &str,
987 fault_tree_probs: &BTreeMap<String, f64>,
988) -> Option<f64> {
989 if let Some(ref ps) = branch.probability_source {
990 let ft_id = extract_ft_id(&ps.pointer);
991 return fault_tree_probs.get(&ft_id).copied();
992 }
993 branch.effective_probability()
994}
995
996fn extract_ft_id(pointer: &str) -> String {
997 pointer
998 .trim_start_matches("#/faultTrees/")
999 .trim_end_matches("/topEvent")
1000 .to_string()
1001}
1002
1003fn to_snake_case(s: &str) -> String {
1004 let mut result = String::new();
1005 for (i, c) in s.chars().enumerate() {
1006 if c.is_uppercase() {
1007 if i > 0 {
1008 result.push('_');
1009 }
1010 result.push(c.to_lowercase().next().unwrap());
1011 } else {
1012 result.push(c);
1013 }
1014 }
1015 result
1016}
1017
1018fn to_upper_snake(s: &str) -> String {
1019 let snake = to_snake_case(s);
1020 snake.to_uppercase()
1021}
1022
1023fn to_pascal_case(s: &str) -> String {
1024 let mut result = String::new();
1025 let mut capitalize = true;
1026 for c in s.chars() {
1027 if c == '_' || c == '-' || c == ' ' {
1028 capitalize = true;
1029 } else if capitalize {
1030 result.extend(c.to_uppercase());
1031 capitalize = false;
1032 } else {
1033 result.push(c);
1034 }
1035 }
1036 result
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041 use super::*;
1042 use etdl_parser::ast::{EtlDocument, InternalRef};
1043
1044 fn parse(yaml: &str) -> EtlDocument {
1045 serde_yaml::from_str(yaml).expect("valid yaml")
1046 }
1047
1048 fn multi_ft_doc() -> EtlDocument {
1049 parse(
1050 r##"
1051etdl: "1.0.0"
1052info:
1053 title: "MultiFT"
1054 version: "1.0.0"
1055 domain: "D"
1056asyncapi_imports: {}
1057eventTrees:
1058 T:
1059 initiatingEvent:
1060 id: I
1061 message: "a#/m"
1062 next: O
1063 nodes:
1064 O:
1065 type: operation
1066 action: execute
1067 handler: "h"
1068 next: C
1069 onFailure: FC
1070 onFailureProbabilitySource: "#/faultTrees/B/topEvent"
1071 C:
1072 type: consequence
1073 operation: terminate
1074 FC:
1075 type: consequence
1076 operation: terminate
1077faultTrees:
1078 A:
1079 topEvent:
1080 id: A1
1081 description: "a"
1082 rootCause: AE
1083 basicEvents:
1084 AE:
1085 description: "ae"
1086 probability: 0.9
1087 B:
1088 topEvent:
1089 id: B1
1090 description: "b"
1091 rootCause: BE
1092 basicEvents:
1093 BE:
1094 description: "be"
1095 probability: 0.01
1096"##,
1097 )
1098 }
1099
1100 #[test]
1101 fn find_fault_tree_prob_selects_by_pointer() {
1102 let doc = multi_ft_doc();
1103 let probs = crate::fault_tree::resolve_fault_trees(&doc, &mut Vec::new());
1104 assert_eq!(probs["A"], 0.9);
1105 assert_eq!(probs["B"], 0.01);
1106
1107 let ps = InternalRef {
1109 pointer: "#/faultTrees/B/topEvent".to_string(),
1110 };
1111 let (id, prob) = find_fault_tree_prob(&ps, &probs).expect("resolves");
1112 assert_eq!(id, "B");
1113 assert!((prob - 0.01).abs() < 1e-9);
1114 }
1115
1116 #[test]
1117 fn generated_constants_use_correct_tree() {
1118 let doc = multi_ft_doc();
1119 let probs = crate::fault_tree::resolve_fault_trees(&doc, &mut Vec::new());
1120 let constants = generate_fault_tree_constants(&doc, &probs);
1121 assert!(constants.contains("faultTrees.B.topEvent"));
1122 assert!(constants.contains("= 0.010000"));
1123 assert!(!constants.contains("faultTrees.A.topEvent"));
1124 }
1125
1126 #[test]
1127 fn in_operator_lowers_to_contains() {
1128 let cond = etdl_parser::ecel::parse_condition(
1129 "message.payload.status in [\"PAID\", \"AUTHORIZED\"]",
1130 )
1131 .unwrap();
1132 let code = condition_to_rust_code(&cond);
1133 assert!(
1134 code.contains("etdl_core::condition::contains"),
1135 "got: {}",
1136 code
1137 );
1138 assert!(code.contains("\"PAID\""));
1139 }
1140
1141 #[test]
1142 fn matches_operator_lowers_to_regex() {
1143 let cond = etdl_parser::ecel::parse_condition(
1144 "message.payload.reference matches \"^ORD-[0-9]{8}$\"",
1145 )
1146 .unwrap();
1147 let code = condition_to_rust_code(&cond);
1148 assert!(
1149 code.contains("etdl_core::condition::matches"),
1150 "got: {}",
1151 code
1152 );
1153 }
1154
1155 #[test]
1156 fn comparison_emits_valid_rust() {
1157 let cond = etdl_parser::ecel::parse_condition("message.payload.amount >= 10000").unwrap();
1158 let code = condition_to_rust_code(&cond);
1159 assert_eq!(code, "message.payload.amount >= 10000");
1160 }
1161}