a3s_flow/nodes/
template_transform.rs1use async_trait::async_trait;
30use serde_json::{json, Value};
31
32use crate::error::{FlowError, Result};
33use crate::node::{ExecContext, Node};
34
35pub struct TemplateTransformNode;
37
38#[async_trait]
39impl Node for TemplateTransformNode {
40 fn node_type(&self) -> &str {
41 "template-transform"
42 }
43
44 async fn execute(&self, ctx: ExecContext) -> Result<Value> {
45 let template_str = ctx.data["template"].as_str().ok_or_else(|| {
46 FlowError::InvalidDefinition("template-transform: missing data.template".into())
47 })?;
48
49 let mut context: std::collections::HashMap<String, Value> =
51 ctx.variables.into_iter().collect();
52 for (k, v) in ctx.inputs {
53 context.insert(k, v);
54 }
55
56 let env = minijinja::Environment::new();
57 let rendered = env
58 .render_str(template_str, &context)
59 .map_err(|e| FlowError::Internal(format!("template-transform: {e}")))?;
60
61 Ok(json!({ "output": rendered }))
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use std::collections::HashMap;
69
70 fn ctx(
71 inputs: HashMap<String, Value>,
72 variables: HashMap<String, Value>,
73 template: &str,
74 ) -> ExecContext {
75 ExecContext {
76 data: json!({ "template": template }),
77 inputs,
78 variables,
79 ..Default::default()
80 }
81 }
82
83 #[tokio::test]
84 async fn renders_simple_variable() {
85 let node = TemplateTransformNode;
86 let out = node
87 .execute(ctx(
88 HashMap::from([("user".into(), json!({ "name": "Alice" }))]),
89 HashMap::new(),
90 "Hello {{ user.name }}!",
91 ))
92 .await
93 .unwrap();
94 assert_eq!(out["output"], "Hello Alice!");
95 }
96
97 #[tokio::test]
98 async fn global_variables_available() {
99 let node = TemplateTransformNode;
100 let out = node
101 .execute(ctx(
102 HashMap::new(),
103 HashMap::from([("env".into(), json!("production"))]),
104 "env={{ env }}",
105 ))
106 .await
107 .unwrap();
108 assert_eq!(out["output"], "env=production");
109 }
110
111 #[tokio::test]
112 async fn inputs_shadow_variables() {
113 let node = TemplateTransformNode;
114 let out = node
115 .execute(ctx(
116 HashMap::from([("key".into(), json!("from_input"))]),
117 HashMap::from([("key".into(), json!("from_var"))]),
118 "{{ key }}",
119 ))
120 .await
121 .unwrap();
122 assert_eq!(out["output"], "from_input");
123 }
124
125 #[tokio::test]
126 async fn jinja2_if_block() {
127 let node = TemplateTransformNode;
128 let out = node
129 .execute(ctx(
130 HashMap::from([("fetch".into(), json!({ "ok": true }))]),
131 HashMap::new(),
132 "{% if fetch.ok %}ok{% else %}fail{% endif %}",
133 ))
134 .await
135 .unwrap();
136 assert_eq!(out["output"], "ok");
137 }
138
139 #[tokio::test]
140 async fn rejects_missing_template() {
141 let node = TemplateTransformNode;
142 let err = node
143 .execute(ExecContext {
144 data: json!({}),
145 inputs: HashMap::new(),
146 variables: HashMap::new(),
147 ..Default::default()
148 })
149 .await
150 .unwrap_err();
151 assert!(matches!(err, FlowError::InvalidDefinition(_)));
152 }
153}