1use crate::error::WorkflowError;
7use serde_json::Value;
8use std::collections::HashMap;
9
10pub struct TemplateContext {
12 pub inputs: HashMap<String, Value>,
13 pub step_outputs: HashMap<String, Value>,
14}
15
16impl TemplateContext {
17 pub fn new(inputs: HashMap<String, Value>, step_outputs: HashMap<String, Value>) -> Self {
18 Self {
19 inputs,
20 step_outputs,
21 }
22 }
23}
24
25pub fn render_string(template: &str, ctx: &TemplateContext) -> Result<String, WorkflowError> {
27 let mut result = String::new();
28 let mut rest = template;
29
30 while let Some(start) = rest.find("{{") {
31 result.push_str(&rest[..start]);
32 let after_open = &rest[start + 2..];
33 let end = after_open
34 .find("}}")
35 .ok_or_else(|| WorkflowError::TemplateError {
36 message: format!("Unclosed template expression in: {}", template),
37 })?;
38 let expr = after_open[..end].trim();
39 let value = resolve_expression(expr, ctx)?;
40 result.push_str(&value_to_string(&value));
41 rest = &after_open[end + 2..];
42 }
43 result.push_str(rest);
44
45 Ok(result)
46}
47
48pub fn render_value(value: &Value, ctx: &TemplateContext) -> Result<Value, WorkflowError> {
51 match value {
52 Value::String(s) => {
53 if s.contains("{{") {
54 let rendered = render_string(s, ctx)?;
55 Ok(Value::String(rendered))
56 } else {
57 Ok(value.clone())
58 }
59 }
60 Value::Object(map) => {
61 let mut new_map = serde_json::Map::new();
62 for (k, v) in map {
63 new_map.insert(k.clone(), render_value(v, ctx)?);
64 }
65 Ok(Value::Object(new_map))
66 }
67 Value::Array(arr) => {
68 let new_arr: Result<Vec<Value>, WorkflowError> =
69 arr.iter().map(|v| render_value(v, ctx)).collect();
70 Ok(Value::Array(new_arr?))
71 }
72 _ => Ok(value.clone()),
73 }
74}
75
76pub fn evaluate_condition(condition: &str, ctx: &TemplateContext) -> Result<bool, WorkflowError> {
80 let rendered = render_string(condition, ctx)?;
81
82 if let Some((left, right)) = rendered.split_once("!=") {
83 let left = left.trim().trim_matches('\'').trim_matches('"');
84 let right = right.trim().trim_matches('\'').trim_matches('"');
85 return Ok(left != right);
86 }
87
88 if let Some((left, right)) = rendered.split_once("==") {
89 let left = left.trim().trim_matches('\'').trim_matches('"');
90 let right = right.trim().trim_matches('\'').trim_matches('"');
91 return Ok(left == right);
92 }
93
94 let trimmed = rendered.trim();
96 Ok(!trimmed.is_empty() && trimmed != "false" && trimmed != "0")
97}
98
99fn resolve_expression(expr: &str, ctx: &TemplateContext) -> Result<Value, WorkflowError> {
101 let parts: Vec<&str> = expr.splitn(3, '.').collect();
102
103 match parts.first() {
104 Some(&"inputs") => {
105 let key = parts.get(1).ok_or_else(|| WorkflowError::TemplateError {
106 message: format!("Invalid input reference: {}", expr),
107 })?;
108 ctx.inputs
109 .get(*key)
110 .cloned()
111 .ok_or_else(|| WorkflowError::TemplateError {
112 message: format!("Input '{}' not found", key),
113 })
114 }
115 Some(&"steps") => {
116 let step_id = parts.get(1).ok_or_else(|| WorkflowError::TemplateError {
117 message: format!("Invalid step reference: {}", expr),
118 })?;
119 ctx.step_outputs
121 .get(*step_id)
122 .cloned()
123 .ok_or_else(|| WorkflowError::TemplateError {
124 message: format!("Step output '{}' not found", step_id),
125 })
126 }
127 _ => Err(WorkflowError::TemplateError {
128 message: format!("Unknown template variable: {}", expr),
129 }),
130 }
131}
132
133fn value_to_string(value: &Value) -> String {
135 match value {
136 Value::String(s) => s.clone(),
137 Value::Null => String::new(),
138 other => other.to_string(),
139 }
140}
141
142pub fn extract_references(template: &str) -> Vec<(String, String)> {
145 let mut refs = Vec::new();
146 let mut rest = template;
147
148 while let Some(start) = rest.find("{{") {
149 let after_open = &rest[start + 2..];
150 if let Some(end) = after_open.find("}}") {
151 let expr = after_open[..end].trim();
152 let parts: Vec<&str> = expr.splitn(3, '.').collect();
153 if parts.len() >= 2 {
154 refs.push((parts[0].to_string(), parts[1].to_string()));
155 }
156 rest = &after_open[end + 2..];
157 } else {
158 break;
159 }
160 }
161
162 refs
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn make_ctx(inputs: Vec<(&str, &str)>, step_outputs: Vec<(&str, &str)>) -> TemplateContext {
170 let inputs_map: HashMap<String, Value> = inputs
171 .into_iter()
172 .map(|(k, v)| (k.to_string(), Value::String(v.to_string())))
173 .collect();
174 let step_map: HashMap<String, Value> = step_outputs
175 .into_iter()
176 .map(|(k, v)| (k.to_string(), Value::String(v.to_string())))
177 .collect();
178 TemplateContext::new(inputs_map, step_map)
179 }
180
181 #[test]
182 fn test_render_simple_substitution() {
183 let ctx = make_ctx(vec![("path", "/home/user/file.rs")], vec![]);
184 let result = render_string("{{ inputs.path }}", &ctx).unwrap();
185 assert_eq!(result, "/home/user/file.rs");
186 }
187
188 #[test]
189 fn test_render_step_output_reference() {
190 let ctx = make_ctx(vec![], vec![("read_file", "file contents here")]);
191 let result = render_string("Content: {{ steps.read_file.output }}", &ctx).unwrap();
192 assert_eq!(result, "Content: file contents here");
193 }
194
195 #[test]
196 fn test_render_no_substitution_needed() {
197 let ctx = make_ctx(vec![], vec![]);
198 let result = render_string("plain text with no templates", &ctx).unwrap();
199 assert_eq!(result, "plain text with no templates");
200 }
201
202 #[test]
203 fn test_render_missing_variable_returns_error() {
204 let ctx = make_ctx(vec![], vec![]);
205 let result = render_string("{{ inputs.missing }}", &ctx);
206 assert!(result.is_err());
207 let err = result.unwrap_err();
208 assert!(err.to_string().contains("not found"));
209 }
210
211 #[test]
212 fn test_render_nested_json_value() {
213 let ctx = make_ctx(vec![("url", "https://example.com")], vec![]);
214 let value = serde_json::json!({
215 "url": "{{ inputs.url }}",
216 "headers": {
217 "host": "{{ inputs.url }}"
218 }
219 });
220 let rendered = render_value(&value, &ctx).unwrap();
221 assert_eq!(rendered["url"].as_str().unwrap(), "https://example.com");
222 assert_eq!(
223 rendered["headers"]["host"].as_str().unwrap(),
224 "https://example.com"
225 );
226 }
227
228 #[test]
229 fn test_evaluate_condition_true() {
230 let ctx = make_ctx(vec![], vec![("check", "pass")]);
231 let result = evaluate_condition("{{ steps.check.output }} == 'pass'", &ctx).unwrap();
232 assert!(result);
233 }
234
235 #[test]
236 fn test_evaluate_condition_false() {
237 let ctx = make_ctx(vec![], vec![("check", "fail")]);
238 let result = evaluate_condition("{{ steps.check.output }} == 'pass'", &ctx).unwrap();
239 assert!(!result);
240 }
241
242 #[test]
243 fn test_evaluate_condition_not_equals() {
244 let ctx = make_ctx(vec![], vec![("check", "fail")]);
245 let result = evaluate_condition("{{ steps.check.output }} != 'pass'", &ctx).unwrap();
246 assert!(result);
247
248 let ctx2 = make_ctx(vec![], vec![("check", "pass")]);
249 let result2 = evaluate_condition("{{ steps.check.output }} != 'pass'", &ctx2).unwrap();
250 assert!(!result2);
251 }
252}