1use std::collections::BTreeMap;
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6
7use crate::{
8 error::{FlowError, FlowErrorLocation, Result},
9 loader::load_ygtc_from_str,
10 model::{FlowDoc, NodeDoc},
11};
12
13#[derive(Debug, Clone)]
16pub struct FlowIr {
17 pub id: String,
18 pub kind: String,
19 pub entrypoints: IndexMap<String, String>,
20 pub nodes: IndexMap<String, NodeIr>,
21}
22
23#[derive(Debug, Clone)]
24pub struct NodeIr {
25 pub id: String,
26 pub kind: NodeKind,
27 pub routing: Vec<Route>,
28}
29
30#[derive(Debug, Clone)]
31pub enum NodeKind {
32 Component(ComponentRef),
33 Questions {
34 fields: Value,
35 },
36 Template {
37 template: String,
38 },
39 Other {
40 component_id: String,
41 payload: Value,
42 },
43}
44
45#[derive(Debug, Clone)]
46pub struct ComponentRef {
47 pub component_id: String,
48 pub pack_alias: Option<String>,
49 pub operation: Option<String>,
50 pub payload: Value,
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
54pub struct Route {
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub to: Option<String>,
57 #[serde(default, skip_serializing_if = "is_false")]
58 pub out: bool,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub status: Option<String>,
61 #[serde(default, skip_serializing_if = "is_false")]
62 pub reply: bool,
63}
64
65fn is_false(value: &bool) -> bool {
66 !*value
67}
68
69impl FlowIr {
70 pub fn from_doc(doc: FlowDoc) -> Result<Self> {
71 let entrypoints = resolve_entrypoints(&doc);
72 let mut nodes = IndexMap::new();
73 for (id, node_doc) in doc.nodes {
74 let routing = parse_routing(&node_doc, &id)?;
75 let kind = match node_doc.component.as_str() {
76 "questions" => NodeKind::Questions {
77 fields: node_doc.payload.clone(),
78 },
79 "template" => {
80 let template =
81 node_doc
82 .payload
83 .as_str()
84 .ok_or_else(|| FlowError::Internal {
85 message: "template node payload must be a string".to_string(),
86 location: FlowErrorLocation::at_path(format!(
87 "nodes.{id}.template"
88 )),
89 })?;
90 NodeKind::Template {
91 template: template.to_string(),
92 }
93 }
94 other => NodeKind::Component(ComponentRef {
95 component_id: other.to_string(),
96 pack_alias: node_doc.pack_alias.clone(),
97 operation: node_doc.operation.clone(),
98 payload: node_doc.payload.clone(),
99 }),
100 };
101 nodes.insert(
102 id.clone(),
103 NodeIr {
104 id: id.clone(),
105 kind,
106 routing,
107 },
108 );
109 }
110
111 Ok(FlowIr {
112 id: doc.id,
113 kind: doc.flow_type,
114 entrypoints,
115 nodes,
116 })
117 }
118
119 pub fn to_doc(&self) -> Result<FlowDoc> {
120 let mut nodes: BTreeMap<String, NodeDoc> = BTreeMap::new();
121 for (id, node_ir) in &self.nodes {
122 let (component, payload, pack_alias, operation, raw) = match &node_ir.kind {
123 NodeKind::Component(comp) => {
124 let mut raw = BTreeMap::new();
125 raw.insert(comp.component_id.clone(), comp.payload.clone());
126 if let Some(alias) = &comp.pack_alias {
127 raw.insert("pack_alias".to_string(), Value::String(alias.clone()));
128 }
129 if let Some(op) = &comp.operation {
130 raw.insert("operation".to_string(), Value::String(op.clone()));
131 }
132 (
133 comp.component_id.clone(),
134 comp.payload.clone(),
135 comp.pack_alias.clone(),
136 comp.operation.clone(),
137 raw,
138 )
139 }
140 NodeKind::Questions { fields } => {
141 let mut raw = BTreeMap::new();
142 raw.insert("questions".to_string(), fields.clone());
143 ("questions".to_string(), fields.clone(), None, None, raw)
144 }
145 NodeKind::Template { template } => {
146 let mut raw = BTreeMap::new();
147 raw.insert("template".to_string(), Value::String(template.clone()));
148 (
149 "template".to_string(),
150 Value::String(template.clone()),
151 None,
152 None,
153 raw,
154 )
155 }
156 NodeKind::Other {
157 component_id,
158 payload,
159 } => {
160 let mut raw = BTreeMap::new();
161 raw.insert(component_id.clone(), payload.clone());
162 (component_id.clone(), payload.clone(), None, None, raw)
163 }
164 };
165
166 let routing_value =
167 serde_json::to_value(&node_ir.routing).map_err(|e| FlowError::Internal {
168 message: format!("serialize routing for node '{id}': {e}"),
169 location: FlowErrorLocation::at_path(format!("nodes.{id}.routing")),
170 })?;
171
172 nodes.insert(
173 id.clone(),
174 NodeDoc {
175 component,
176 pack_alias,
177 operation,
178 payload,
179 routing: routing_value,
180 output: None,
181 telemetry: None,
182 raw,
183 },
184 );
185 }
186
187 Ok(FlowDoc {
188 id: self.id.clone(),
189 title: None,
190 description: None,
191 flow_type: self.kind.clone(),
192 start: self.entrypoints.get("default").cloned(),
193 parameters: Value::Object(Map::new()),
194 tags: Vec::new(),
195 entrypoints: BTreeMap::new(),
196 nodes,
197 })
198 }
199}
200
201fn resolve_entrypoints(doc: &FlowDoc) -> IndexMap<String, String> {
202 let mut entries = IndexMap::new();
203 if let Some(start) = &doc.start {
204 entries.insert("default".to_string(), start.clone());
205 } else if doc.nodes.contains_key("in") {
206 entries.insert("default".to_string(), "in".to_string());
207 } else if let Some(first) = doc.nodes.keys().next() {
208 entries.insert("default".to_string(), first.clone());
209 }
210 for (k, v) in &doc.entrypoints {
211 if let Some(target) = v.as_str() {
212 entries.insert(k.clone(), target.to_string());
213 }
214 }
215 entries
216}
217
218fn parse_routing(node: &NodeDoc, node_id: &str) -> Result<Vec<Route>> {
219 #[derive(serde::Deserialize)]
220 struct RouteDoc {
221 #[serde(default)]
222 to: Option<String>,
223 #[serde(default)]
224 out: Option<bool>,
225 #[serde(default)]
226 status: Option<String>,
227 #[serde(default)]
228 reply: Option<bool>,
229 }
230
231 let routes: Vec<RouteDoc> = if node.routing.is_null() {
232 Vec::new()
233 } else {
234 serde_json::from_value(node.routing.clone()).map_err(|e| FlowError::Internal {
235 message: format!("routing decode for node '{node_id}': {e}"),
236 location: FlowErrorLocation::at_path(format!("nodes.{node_id}.routing")),
237 })?
238 };
239
240 Ok(routes
241 .into_iter()
242 .map(|r| Route {
243 to: r.to,
244 out: r.out.unwrap_or(false),
245 status: r.status,
246 reply: r.reply.unwrap_or(false),
247 })
248 .collect())
249}
250
251pub fn parse_flow_to_ir(yaml: &str) -> Result<FlowIr> {
253 let doc = load_ygtc_from_str(yaml)?;
254 FlowIr::from_doc(doc)
255}