cards2pack_core/
convert.rs1use crate::emit::emit_ygtc;
4use crate::entry::detect_entry;
5use crate::errors::ConvertError;
6use crate::http_inject::inject_http_nodes;
7use crate::routing::build_routing;
8use crate::types::{CardEntry, ConvertOptions, ConvertResult};
9
10pub fn convert(cards: &[CardEntry], opts: &ConvertOptions) -> Result<ConvertResult, ConvertError> {
11 if cards.is_empty() {
12 return Err(ConvertError::NoCards);
13 }
14
15 let entry = detect_entry(cards)?;
16 let (mut routing, mut diagnostics) = build_routing(cards, opts.strict)?;
17 let http_nodes = inject_http_nodes(cards, &mut routing);
18
19 let mut reachable: std::collections::HashSet<&str> = std::collections::HashSet::new();
21 reachable.insert(entry.as_str());
22 let mut frontier = vec![entry.as_str()];
23 while let Some(node) = frontier.pop() {
24 if let Some(edges) = routing.edges.get(node) {
25 for e in edges {
26 if reachable.insert(e.target.as_str()) {
27 frontier.push(e.target.as_str());
28 }
29 }
30 }
31 }
32 for card in cards {
33 if !reachable.contains(card.id.as_str()) {
34 diagnostics.push(crate::types::Diagnostic {
35 kind: crate::types::DiagnosticKind::UnreachableCard,
36 message: format!("card '{}' is unreachable from entry '{}'", card.id, entry),
37 });
38 }
39 }
40
41 let flow_yaml = emit_ygtc(cards, &entry, &routing, &http_nodes, &opts.flow_name)?;
42
43 Ok(ConvertResult {
44 flow_yaml,
45 diagnostics,
46 })
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use crate::types::{CardKind, ConvertOptions};
53 use serde_json::json;
54
55 #[test]
56 fn happy_path_two_cards() {
57 let cards = vec![
58 CardEntry {
59 id: "welcome".into(),
60 kind: CardKind::AdaptiveCard(json!({
61 "actions":[{"type":"Action.Submit","data":{"routeToCardId":"thanks","action_id":"go"}}]
62 })),
63 },
64 CardEntry {
65 id: "thanks".into(),
66 kind: CardKind::AdaptiveCard(json!({})),
67 },
68 ];
69 let res = convert(
70 &cards,
71 &ConvertOptions {
72 flow_name: "demo".into(),
73 strict: false,
74 },
75 )
76 .unwrap();
77 assert!(res.flow_yaml.contains("start: welcome"));
78 assert!(res.diagnostics.is_empty());
79 }
80
81 #[test]
82 fn empty_cards_errors() {
83 let err = convert(
84 &[],
85 &ConvertOptions {
86 flow_name: "x".into(),
87 strict: false,
88 },
89 )
90 .unwrap_err();
91 assert_eq!(err.code(), "E_NO_CARDS");
92 }
93
94 #[test]
95 fn unreachable_card_emits_diagnostic() {
96 let cards = vec![
97 CardEntry {
98 id: "a".into(),
99 kind: CardKind::AdaptiveCard(json!({})),
100 },
101 CardEntry {
102 id: "orphan".into(),
103 kind: CardKind::AdaptiveCard(json!({})),
104 },
105 ];
106 let res = convert(
107 &cards,
108 &ConvertOptions {
109 flow_name: "demo".into(),
110 strict: false,
111 },
112 )
113 .unwrap();
114 assert!(
115 res.diagnostics
116 .iter()
117 .any(|d| matches!(d.kind, crate::types::DiagnosticKind::UnreachableCard))
118 );
119 }
120}