1use std::collections::HashMap;
19
20const BUILTIN_VARS: &[&str] = &[
25 "flow_name",
26 "persona_name",
27 "unit_index",
28 "result",
29 "step_name",
30 "step_type",
31 "step_index",
32];
33
34#[derive(Debug, Clone)]
36pub struct ExecContext {
37 vars: HashMap<String, String>,
38}
39
40impl ExecContext {
41 pub fn new(flow_name: &str, persona_name: &str, unit_index: usize) -> Self {
43 let mut vars = HashMap::new();
44 vars.insert("flow_name".to_string(), flow_name.to_string());
45 vars.insert("persona_name".to_string(), persona_name.to_string());
46 vars.insert("unit_index".to_string(), format!("{}", unit_index + 1));
47 vars.insert("result".to_string(), String::new());
48 ExecContext { vars }
49 }
50
51 pub fn set(&mut self, key: &str, value: &str) {
53 self.vars.insert(key.to_string(), value.to_string());
54 }
55
56 pub fn get(&self, key: &str) -> Option<&str> {
58 self.vars.get(key).map(|s| s.as_str())
59 }
60
61 pub fn vars(&self) -> &HashMap<String, String> {
65 &self.vars
66 }
67
68 pub fn set_step(&mut self, step_name: &str, step_type: &str, step_index: usize) {
70 self.vars.insert("step_name".to_string(), step_name.to_string());
71 self.vars.insert("step_type".to_string(), step_type.to_string());
72 self.vars.insert("step_index".to_string(), format!("{}", step_index + 1));
73 }
74
75 pub fn set_result(&mut self, step_name: &str, result: &str) {
77 self.vars.insert("result".to_string(), result.to_string());
78 self.vars.insert(step_name.to_string(), result.to_string());
79 }
80
81 pub fn interpolate(&self, text: &str) -> String {
88 interpolate_vars(text, &self.vars)
89 }
90
91 pub fn resolve_named_arg(&self, value: &str, value_kind: &str) -> String {
96 resolve_named_arg_value(value, value_kind, &self.vars)
97 }
98
99 pub fn var_count(&self) -> usize {
101 self.vars.len()
102 }
103
104 pub fn user_bindings(&self) -> Vec<(String, String)> {
110 let mut out: Vec<(String, String)> = self
111 .vars
112 .iter()
113 .filter(|(k, _)| !BUILTIN_VARS.contains(&k.as_str()))
114 .map(|(k, v)| (k.clone(), v.clone()))
115 .collect();
116 out.sort_by(|a, b| a.0.cmp(&b.0));
117 out
118 }
119}
120
121pub fn interpolate_vars(text: &str, vars: &HashMap<String, String>) -> String {
129 let bytes = text.as_bytes();
130 let mut out = String::with_capacity(text.len());
131 let mut i = 0;
132
133 while i < bytes.len() {
134 if bytes[i] == b'$' && i + 1 < bytes.len() {
135 if bytes[i + 1] == b'{' {
136 if let Some(close) = text[i + 2..].find('}') {
138 let var_name = &text[i + 2..i + 2 + close];
139 if let Some(val) = vars.get(var_name) {
140 out.push_str(val);
141 } else {
142 out.push_str(&text[i..i + 3 + close]);
144 }
145 i += 3 + close;
146 continue;
147 }
148 } else if bytes[i + 1].is_ascii_alphabetic() || bytes[i + 1] == b'_' {
149 let start = i + 1;
151 let mut end = start;
152 while end < bytes.len()
153 && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_')
154 {
155 end += 1;
156 }
157 let var_name = &text[start..end];
158 if let Some(val) = vars.get(var_name) {
159 out.push_str(val);
160 } else {
161 out.push_str(&text[i..end]);
162 }
163 i = end;
164 continue;
165 }
166 }
167 out.push(bytes[i] as char);
168 i += 1;
169 }
170
171 out
172}
173
174pub fn resolve_named_arg_value(
189 value: &str,
190 value_kind: &str,
191 vars: &HashMap<String, String>,
192) -> String {
193 if value_kind == "reference" {
194 vars.get(value)
195 .or_else(|| value.strip_suffix(".output").and_then(|step| vars.get(step)))
196 .cloned()
197 .unwrap_or_default()
198 } else {
199 interpolate_vars(value, vars)
200 }
201}
202
203#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn bindings() -> HashMap<String, String> {
212 let mut m = HashMap::new();
213 m.insert("user_input".to_string(), "analiza https://acme.com".to_string());
214 m.insert("company".to_string(), "Acme".to_string());
215 m.insert("ExtractUrl".to_string(), "https://acme.com".to_string());
217 m
218 }
219
220 #[test]
221 fn reference_resolves_bare_flow_param() {
222 assert_eq!(
224 resolve_named_arg_value("company", "reference", &bindings()),
225 "Acme"
226 );
227 }
228
229 #[test]
230 fn reference_resolves_step_output_dotted_to_step_name_key() {
231 assert_eq!(
233 resolve_named_arg_value("ExtractUrl.output", "reference", &bindings()),
234 "https://acme.com"
235 );
236 }
237
238 #[test]
239 fn reference_resolves_bare_step_name() {
240 assert_eq!(
241 resolve_named_arg_value("ExtractUrl", "reference", &bindings()),
242 "https://acme.com"
243 );
244 }
245
246 #[test]
247 fn reference_unbound_is_empty_not_literal_name() {
248 assert_eq!(resolve_named_arg_value("nope", "reference", &bindings()), "");
250 }
251
252 #[test]
253 fn literal_keeps_interpolation_and_verbatim() {
254 assert_eq!(
256 resolve_named_arg_value("${company}", "literal", &bindings()),
257 "Acme"
258 );
259 assert_eq!(
261 resolve_named_arg_value("Acme", "literal", &bindings()),
262 "Acme"
263 );
264 }
265
266 #[test]
267 fn new_context_has_unit_vars() {
268 let ctx = ExecContext::new("Analyze", "Expert", 0);
269 assert_eq!(ctx.get("flow_name"), Some("Analyze"));
270 assert_eq!(ctx.get("persona_name"), Some("Expert"));
271 assert_eq!(ctx.get("unit_index"), Some("1"));
272 assert_eq!(ctx.get("result"), Some(""));
273 }
274
275 #[test]
276 fn set_step_updates_vars() {
277 let mut ctx = ExecContext::new("F", "P", 0);
278 ctx.set_step("Gather", "step", 0);
279 assert_eq!(ctx.get("step_name"), Some("Gather"));
280 assert_eq!(ctx.get("step_type"), Some("step"));
281 assert_eq!(ctx.get("step_index"), Some("1"));
282 }
283
284 #[test]
285 fn set_result_updates_both() {
286 let mut ctx = ExecContext::new("F", "P", 0);
287 ctx.set_result("Analyze", "The answer is 42");
288 assert_eq!(ctx.get("result"), Some("The answer is 42"));
289 assert_eq!(ctx.get("Analyze"), Some("The answer is 42"));
290 }
291
292 #[test]
293 fn interpolate_dollar_name() {
294 let mut ctx = ExecContext::new("F", "P", 0);
295 ctx.set_result("Analyze", "42");
296 let out = ctx.interpolate("The result is $result from step $step_name");
297 assert!(out.contains("The result is 42"));
299 }
300
301 #[test]
302 fn interpolate_braced() {
303 let mut ctx = ExecContext::new("F", "P", 0);
304 ctx.set_result("Analyze", "42");
305 let out = ctx.interpolate("Previous: ${Analyze}, flow: ${flow_name}");
306 assert_eq!(out, "Previous: 42, flow: F");
307 }
308
309 #[test]
310 fn interpolate_unknown_kept_literal() {
311 let ctx = ExecContext::new("F", "P", 0);
312 let out = ctx.interpolate("Value: $unknown and ${also_unknown}");
313 assert_eq!(out, "Value: $unknown and ${also_unknown}");
314 }
315
316 #[test]
317 fn interpolate_no_vars() {
318 let ctx = ExecContext::new("F", "P", 0);
319 let out = ctx.interpolate("No variables here.");
320 assert_eq!(out, "No variables here.");
321 }
322
323 #[test]
324 fn interpolate_adjacent_vars() {
325 let mut ctx = ExecContext::new("F", "P", 0);
326 ctx.set("a", "hello");
327 ctx.set("b", "world");
328 let out = ctx.interpolate("$a$b");
329 assert_eq!(out, "helloworld");
330 }
331
332 #[test]
333 fn interpolate_dollar_at_end() {
334 let ctx = ExecContext::new("F", "P", 0);
335 let out = ctx.interpolate("price is $");
336 assert_eq!(out, "price is $");
337 }
338
339 #[test]
340 fn interpolate_dollar_number() {
341 let ctx = ExecContext::new("F", "P", 0);
342 let out = ctx.interpolate("cost: $100");
343 assert_eq!(out, "cost: $100");
344 }
345
346 #[test]
347 fn set_and_get_custom() {
348 let mut ctx = ExecContext::new("F", "P", 0);
349 ctx.set("custom_key", "custom_value");
350 assert_eq!(ctx.get("custom_key"), Some("custom_value"));
351 }
352
353 #[test]
354 fn var_count() {
355 let ctx = ExecContext::new("F", "P", 0);
356 assert_eq!(ctx.var_count(), 4);
358 }
359
360 #[test]
361 fn user_bindings_excludes_builtins() {
362 let mut ctx = ExecContext::new("F", "P", 0);
363 ctx.set_step("Gather", "step", 0);
364 ctx.set_result("Gather", "data");
365 ctx.set("tenant_id", "acme");
366 let bindings = ctx.user_bindings();
370 assert_eq!(
371 bindings,
372 vec![
373 ("Gather".to_string(), "data".to_string()),
374 ("tenant_id".to_string(), "acme".to_string()),
375 ]
376 );
377 }
378
379 #[test]
380 fn user_bindings_empty_for_fresh_context() {
381 let ctx = ExecContext::new("F", "P", 0);
382 assert!(ctx.user_bindings().is_empty());
383 }
384}