1use async_trait::async_trait;
57use serde_json::Value;
58
59use crate::error::{FlowError, Result};
60use crate::node::{ExecContext, Node};
61
62use super::llm::{build_jinja_context, render};
63
64pub struct AssignNode;
66
67#[async_trait]
68impl Node for AssignNode {
69 fn node_type(&self) -> &str {
70 "assign"
71 }
72
73 async fn execute(&self, ctx: ExecContext) -> Result<Value> {
74 let assigns = ctx.data["assigns"].as_object().ok_or_else(|| {
75 FlowError::InvalidDefinition("assign: missing or non-object data.assigns".into())
76 })?;
77
78 let jinja_ctx = build_jinja_context(&ctx);
79 let mut out = serde_json::Map::new();
80
81 for (key, template_val) in assigns {
82 let resolved = if let Some(s) = template_val.as_str() {
83 Value::String(render(s, &jinja_ctx)?)
85 } else {
86 template_val.clone()
88 };
89 out.insert(key.clone(), resolved);
90 }
91
92 Ok(Value::Object(out))
93 }
94}
95
96#[cfg(test)]
99mod tests {
100 use super::*;
101 use serde_json::json;
102 use std::collections::HashMap;
103
104 fn ctx(data: Value) -> ExecContext {
105 ExecContext {
106 data,
107 ..Default::default()
108 }
109 }
110
111 fn ctx_with_vars(data: Value, variables: HashMap<String, Value>) -> ExecContext {
112 ExecContext {
113 data,
114 variables,
115 ..Default::default()
116 }
117 }
118
119 fn ctx_with_inputs(
120 data: Value,
121 inputs: HashMap<String, Value>,
122 variables: HashMap<String, Value>,
123 ) -> ExecContext {
124 ExecContext {
125 data,
126 inputs,
127 variables,
128 ..Default::default()
129 }
130 }
131
132 #[tokio::test]
133 async fn assigns_string_template() {
134 let node = AssignNode;
135 let out = node
136 .execute(ctx_with_vars(
137 json!({ "assigns": { "greeting": "Hello, {{ name }}!" } }),
138 HashMap::from([("name".into(), json!("Alice"))]),
139 ))
140 .await
141 .unwrap();
142 assert_eq!(out["greeting"], json!("Hello, Alice!"));
143 }
144
145 #[tokio::test]
146 async fn assigns_literal_non_string_values() {
147 let node = AssignNode;
148 let out = node
149 .execute(ctx(json!({
150 "assigns": {
151 "count": 0,
152 "flag": true,
153 "tags": ["a", "b"],
154 "nested": { "x": 1 }
155 }
156 })))
157 .await
158 .unwrap();
159 assert_eq!(out["count"], json!(0));
160 assert_eq!(out["flag"], json!(true));
161 assert_eq!(out["tags"], json!(["a", "b"]));
162 assert_eq!(out["nested"], json!({ "x": 1 }));
163 }
164
165 #[tokio::test]
166 async fn template_reads_upstream_input() {
167 let node = AssignNode;
168 let out = node
169 .execute(ctx_with_inputs(
170 json!({ "assigns": { "msg": "status={{ fetch.status }}" } }),
171 HashMap::from([("fetch".into(), json!({ "status": 200 }))]),
172 HashMap::new(),
173 ))
174 .await
175 .unwrap();
176 assert_eq!(out["msg"], json!("status=200"));
177 }
178
179 #[tokio::test]
180 async fn inputs_shadow_variables_in_template() {
181 let node = AssignNode;
182 let out = node
183 .execute(ctx_with_inputs(
184 json!({ "assigns": { "val": "{{ x }}" } }),
185 HashMap::from([("x".into(), json!("from_input"))]),
186 HashMap::from([("x".into(), json!("from_var"))]),
187 ))
188 .await
189 .unwrap();
190 assert_eq!(out["val"], json!("from_input"));
191 }
192
193 #[tokio::test]
194 async fn empty_assigns_returns_empty_object() {
195 let node = AssignNode;
196 let out = node.execute(ctx(json!({ "assigns": {} }))).await.unwrap();
197 assert_eq!(out, json!({}));
198 }
199
200 #[tokio::test]
201 async fn missing_assigns_returns_error() {
202 let node = AssignNode;
203 let err = node.execute(ctx(json!({}))).await.unwrap_err();
204 assert!(matches!(err, FlowError::InvalidDefinition(_)));
205 }
206
207 #[tokio::test]
208 async fn non_object_assigns_returns_error() {
209 let node = AssignNode;
210 let err = node
211 .execute(ctx(json!({ "assigns": ["not", "an", "object"] })))
212 .await
213 .unwrap_err();
214 assert!(matches!(err, FlowError::InvalidDefinition(_)));
215 }
216
217 #[tokio::test]
218 async fn invalid_template_returns_error() {
219 let node = AssignNode;
220 let err = node
221 .execute(ctx(
222 json!({ "assigns": { "x": "{{ bad | unknown_filter }}" } }),
223 ))
224 .await
225 .unwrap_err();
226 assert!(matches!(err, FlowError::Internal(_)));
227 }
228}