matrixcode_core/workflow/
template.rs1use anyhow::{Context, Result};
6use regex::Regex;
7use std::collections::HashMap;
8
9#[derive(Debug)]
11pub struct TemplateRenderer {
12 var_pattern: Regex,
14 nested_pattern: Regex,
16 default_pattern: Regex,
18}
19
20impl Default for TemplateRenderer {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl TemplateRenderer {
27 pub fn new() -> Self {
29 Self {
30 var_pattern: Regex::new(r"\{\{(\w+)\}\}").unwrap(),
31 nested_pattern: Regex::new(r"\{\{(\w+(?:\.\w+)+)\}\}").unwrap(),
32 default_pattern: Regex::new(r"\{\{(\w+)\|([^}]+)\}\}").unwrap(),
33 }
34 }
35
36 pub fn render(
43 &self,
44 template: &str,
45 variables: &HashMap<String, serde_json::Value>,
46 ) -> Result<String> {
47 let mut result = template.to_string();
48
49 result = self.render_nested(&result, variables)?;
51
52 result = self.render_with_default(&result, variables)?;
54
55 result = self.render_simple(&result, variables)?;
57
58 Ok(result)
59 }
60
61 fn render_simple(
63 &self,
64 template: &str,
65 variables: &HashMap<String, serde_json::Value>,
66 ) -> Result<String> {
67 let mut result = template.to_string();
68
69 for cap in self.var_pattern.captures_iter(template) {
70 let full_match = cap.get(0).unwrap().as_str();
71 let var_name = cap.get(1).unwrap().as_str();
72
73 if let Some(value) = variables.get(var_name) {
74 let replacement = self.value_to_string(value)?;
75 result = result.replace(full_match, &replacement);
76 }
77 }
78
79 Ok(result)
80 }
81
82 fn render_nested(
84 &self,
85 template: &str,
86 variables: &HashMap<String, serde_json::Value>,
87 ) -> Result<String> {
88 let mut result = template.to_string();
89
90 for cap in self.nested_pattern.captures_iter(template) {
91 let full_match = cap.get(0).unwrap().as_str();
92 let path = cap.get(1).unwrap().as_str();
93
94 if let Some(value) = self.resolve_path(path, variables)? {
95 let replacement = self.value_to_string(&value)?;
96 result = result.replace(full_match, &replacement);
97 }
98 }
99
100 Ok(result)
101 }
102
103 fn render_with_default(
105 &self,
106 template: &str,
107 variables: &HashMap<String, serde_json::Value>,
108 ) -> Result<String> {
109 let mut result = template.to_string();
110
111 for cap in self.default_pattern.captures_iter(template) {
112 let full_match = cap.get(0).unwrap().as_str();
113 let var_name = cap.get(1).unwrap().as_str();
114 let default_value = cap.get(2).unwrap().as_str();
115
116 let replacement = match variables.get(var_name) {
117 Some(value) => self.value_to_string(value)?,
118 None => default_value.to_string(),
119 };
120 result = result.replace(full_match, &replacement);
121 }
122
123 Ok(result)
124 }
125
126 fn resolve_path(
128 &self,
129 path: &str,
130 variables: &HashMap<String, serde_json::Value>,
131 ) -> Result<Option<serde_json::Value>> {
132 let parts: Vec<&str> = path.split('.').collect();
133 if parts.is_empty() {
134 return Ok(None);
135 }
136
137 let first = parts[0];
138 let mut current = variables.get(first).cloned();
139
140 for part in parts.iter().skip(1) {
141 match current {
142 Some(serde_json::Value::Object(map)) => {
143 current = map.get(*part).cloned();
144 }
145 _ => return Ok(None),
146 }
147 }
148
149 Ok(current)
150 }
151
152 fn value_to_string(&self, value: &serde_json::Value) -> Result<String> {
154 match value {
155 serde_json::Value::Null => Ok(String::new()),
156 serde_json::Value::Bool(b) => Ok(b.to_string()),
157 serde_json::Value::Number(n) => Ok(n.to_string()),
158 serde_json::Value::String(s) => Ok(s.clone()),
159 serde_json::Value::Array(arr) => {
160 serde_json::to_string(arr).with_context(|| "Failed to stringify array")
161 }
162 serde_json::Value::Object(obj) => {
163 serde_json::to_string(obj).with_context(|| "Failed to stringify object")
164 }
165 }
166 }
167
168 pub fn extract_variables(&self, template: &str) -> Vec<String> {
170 let mut vars = Vec::new();
171
172 for cap in self.var_pattern.captures_iter(template) {
173 vars.push(cap.get(1).unwrap().as_str().to_string());
174 }
175
176 for cap in self.nested_pattern.captures_iter(template) {
177 let path = cap.get(1).unwrap().as_str();
178 if let Some(first) = path.split('.').next() {
179 vars.push(first.to_string());
180 }
181 }
182
183 for cap in self.default_pattern.captures_iter(template) {
184 vars.push(cap.get(1).unwrap().as_str().to_string());
185 }
186
187 vars.sort();
188 vars.dedup();
189 vars
190 }
191
192 pub fn has_unresolved(&self, rendered: &str) -> bool {
194 self.var_pattern.is_match(rendered)
195 || self.nested_pattern.is_match(rendered)
196 || self.default_pattern.is_match(rendered)
197 }
198
199 pub fn render_params(
203 &self,
204 params: &HashMap<String, serde_json::Value>,
205 variables: &HashMap<String, serde_json::Value>,
206 ) -> Result<serde_json::Value> {
207 let mut rendered = HashMap::new();
208 for (key, value) in params {
209 let rendered_value = if let serde_json::Value::String(s) = value {
210 let rendered_str = self.render(s, variables)?;
211 serde_json::Value::String(rendered_str)
212 } else {
213 value.clone()
214 };
215 rendered.insert(key.clone(), rendered_value);
216 }
217 Ok(serde_json::Value::Object(rendered.into_iter().collect()))
218 }
219}
220
221pub fn render(template: &str, variables: &HashMap<String, serde_json::Value>) -> Result<String> {
223 let renderer = TemplateRenderer::new();
224 renderer.render(template, variables)
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use serde_json::json;
231
232 #[test]
233 fn test_simple_variable() {
234 let renderer = TemplateRenderer::new();
235 let mut vars = HashMap::new();
236 vars.insert("name".to_string(), json!("Alice"));
237
238 let result = renderer.render("Hello, {{name}}!", &vars).unwrap();
239 assert_eq!(result, "Hello, Alice!");
240 }
241
242 #[test]
243 fn test_multiple_variables() {
244 let renderer = TemplateRenderer::new();
245 let mut vars = HashMap::new();
246 vars.insert("first".to_string(), json!("John"));
247 vars.insert("last".to_string(), json!("Doe"));
248
249 let result = renderer.render("{{first}} {{last}}", &vars).unwrap();
250 assert_eq!(result, "John Doe");
251 }
252
253 #[test]
254 fn test_nested_access() {
255 let renderer = TemplateRenderer::new();
256 let mut vars = HashMap::new();
257 vars.insert(
258 "user".to_string(),
259 json!({
260 "name": "Bob",
261 "age": 30
262 }),
263 );
264
265 let result = renderer
266 .render("Name: {{user.name}}, Age: {{user.age}}", &vars)
267 .unwrap();
268 assert_eq!(result, "Name: Bob, Age: 30");
269 }
270
271 #[test]
272 fn test_default_value() {
273 let renderer = TemplateRenderer::new();
274 let vars = HashMap::new();
275
276 let result = renderer.render("Hello, {{name|Guest}}!", &vars).unwrap();
277 assert_eq!(result, "Hello, Guest!");
278 }
279
280 #[test]
281 fn test_extract_variables() {
282 let renderer = TemplateRenderer::new();
283 let template = "{{name}} is {{age}} years old, user: {{user.name}}";
284
285 let vars = renderer.extract_variables(template);
286 assert!(vars.contains(&"name".to_string()));
287 assert!(vars.contains(&"age".to_string()));
288 assert!(vars.contains(&"user".to_string()));
289 }
290
291 #[test]
292 fn test_number_value() {
293 let renderer = TemplateRenderer::new();
294 let mut vars = HashMap::new();
295 vars.insert("count".to_string(), json!(42));
296
297 let result = renderer.render("Count: {{count}}", &vars).unwrap();
298 assert_eq!(result, "Count: 42");
299 }
300}