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(&self, template: &str, variables: &HashMap<String, serde_json::Value>) -> Result<String> {
43 let mut result = template.to_string();
44
45 result = self.render_nested(&result, variables)?;
47
48 result = self.render_with_default(&result, variables)?;
50
51 result = self.render_simple(&result, variables)?;
53
54 Ok(result)
55 }
56
57 fn render_simple(&self, template: &str, variables: &HashMap<String, serde_json::Value>) -> Result<String> {
59 let mut result = template.to_string();
60
61 for cap in self.var_pattern.captures_iter(template) {
62 let full_match = cap.get(0).unwrap().as_str();
63 let var_name = cap.get(1).unwrap().as_str();
64
65 if let Some(value) = variables.get(var_name) {
66 let replacement = self.value_to_string(value)?;
67 result = result.replace(full_match, &replacement);
68 }
69 }
70
71 Ok(result)
72 }
73
74 fn render_nested(&self, template: &str, variables: &HashMap<String, serde_json::Value>) -> Result<String> {
76 let mut result = template.to_string();
77
78 for cap in self.nested_pattern.captures_iter(template) {
79 let full_match = cap.get(0).unwrap().as_str();
80 let path = cap.get(1).unwrap().as_str();
81
82 if let Some(value) = self.resolve_path(path, variables)? {
83 let replacement = self.value_to_string(&value)?;
84 result = result.replace(full_match, &replacement);
85 }
86 }
87
88 Ok(result)
89 }
90
91 fn render_with_default(&self, template: &str, variables: &HashMap<String, serde_json::Value>) -> Result<String> {
93 let mut result = template.to_string();
94
95 for cap in self.default_pattern.captures_iter(template) {
96 let full_match = cap.get(0).unwrap().as_str();
97 let var_name = cap.get(1).unwrap().as_str();
98 let default_value = cap.get(2).unwrap().as_str();
99
100 let replacement = match variables.get(var_name) {
101 Some(value) => self.value_to_string(value)?,
102 None => default_value.to_string(),
103 };
104 result = result.replace(full_match, &replacement);
105 }
106
107 Ok(result)
108 }
109
110 fn resolve_path(&self, path: &str, variables: &HashMap<String, serde_json::Value>) -> Result<Option<serde_json::Value>> {
112 let parts: Vec<&str> = path.split('.').collect();
113 if parts.is_empty() {
114 return Ok(None);
115 }
116
117 let first = parts[0];
118 let mut current = variables.get(first).cloned();
119
120 for part in parts.iter().skip(1) {
121 match current {
122 Some(serde_json::Value::Object(map)) => {
123 current = map.get(*part).cloned();
124 }
125 _ => return Ok(None),
126 }
127 }
128
129 Ok(current)
130 }
131
132 fn value_to_string(&self, value: &serde_json::Value) -> Result<String> {
134 match value {
135 serde_json::Value::Null => Ok(String::new()),
136 serde_json::Value::Bool(b) => Ok(b.to_string()),
137 serde_json::Value::Number(n) => Ok(n.to_string()),
138 serde_json::Value::String(s) => Ok(s.clone()),
139 serde_json::Value::Array(arr) => {
140 serde_json::to_string(arr).with_context(|| "Failed to stringify array")
141 }
142 serde_json::Value::Object(obj) => {
143 serde_json::to_string(obj).with_context(|| "Failed to stringify object")
144 }
145 }
146 }
147
148 pub fn extract_variables(&self, template: &str) -> Vec<String> {
150 let mut vars = Vec::new();
151
152 for cap in self.var_pattern.captures_iter(template) {
153 vars.push(cap.get(1).unwrap().as_str().to_string());
154 }
155
156 for cap in self.nested_pattern.captures_iter(template) {
157 let path = cap.get(1).unwrap().as_str();
158 if let Some(first) = path.split('.').next() {
159 vars.push(first.to_string());
160 }
161 }
162
163 for cap in self.default_pattern.captures_iter(template) {
164 vars.push(cap.get(1).unwrap().as_str().to_string());
165 }
166
167 vars.sort();
168 vars.dedup();
169 vars
170 }
171
172 pub fn has_unresolved(&self, rendered: &str) -> bool {
174 self.var_pattern.is_match(rendered)
175 || self.nested_pattern.is_match(rendered)
176 || self.default_pattern.is_match(rendered)
177 }
178
179 pub fn render_params(
183 &self,
184 params: &HashMap<String, serde_json::Value>,
185 variables: &HashMap<String, serde_json::Value>,
186 ) -> Result<serde_json::Value> {
187 let mut rendered = HashMap::new();
188 for (key, value) in params {
189 let rendered_value = if let serde_json::Value::String(s) = value {
190 let rendered_str = self.render(s, variables)?;
191 serde_json::Value::String(rendered_str)
192 } else {
193 value.clone()
194 };
195 rendered.insert(key.clone(), rendered_value);
196 }
197 Ok(serde_json::Value::Object(rendered.into_iter().collect()))
198 }
199}
200
201pub fn render(template: &str, variables: &HashMap<String, serde_json::Value>) -> Result<String> {
203 let renderer = TemplateRenderer::new();
204 renderer.render(template, variables)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use serde_json::json;
211
212 #[test]
213 fn test_simple_variable() {
214 let renderer = TemplateRenderer::new();
215 let mut vars = HashMap::new();
216 vars.insert("name".to_string(), json!("Alice"));
217
218 let result = renderer.render("Hello, {{name}}!", &vars).unwrap();
219 assert_eq!(result, "Hello, Alice!");
220 }
221
222 #[test]
223 fn test_multiple_variables() {
224 let renderer = TemplateRenderer::new();
225 let mut vars = HashMap::new();
226 vars.insert("first".to_string(), json!("John"));
227 vars.insert("last".to_string(), json!("Doe"));
228
229 let result = renderer.render("{{first}} {{last}}", &vars).unwrap();
230 assert_eq!(result, "John Doe");
231 }
232
233 #[test]
234 fn test_nested_access() {
235 let renderer = TemplateRenderer::new();
236 let mut vars = HashMap::new();
237 vars.insert("user".to_string(), json!({
238 "name": "Bob",
239 "age": 30
240 }));
241
242 let result = renderer.render("Name: {{user.name}}, Age: {{user.age}}", &vars).unwrap();
243 assert_eq!(result, "Name: Bob, Age: 30");
244 }
245
246 #[test]
247 fn test_default_value() {
248 let renderer = TemplateRenderer::new();
249 let vars = HashMap::new();
250
251 let result = renderer.render("Hello, {{name|Guest}}!", &vars).unwrap();
252 assert_eq!(result, "Hello, Guest!");
253 }
254
255 #[test]
256 fn test_extract_variables() {
257 let renderer = TemplateRenderer::new();
258 let template = "{{name}} is {{age}} years old, user: {{user.name}}";
259
260 let vars = renderer.extract_variables(template);
261 assert!(vars.contains(&"name".to_string()));
262 assert!(vars.contains(&"age".to_string()));
263 assert!(vars.contains(&"user".to_string()));
264 }
265
266 #[test]
267 fn test_number_value() {
268 let renderer = TemplateRenderer::new();
269 let mut vars = HashMap::new();
270 vars.insert("count".to_string(), json!(42));
271
272 let result = renderer.render("Count: {{count}}", &vars).unwrap();
273 assert_eq!(result, "Count: 42");
274 }
275}