Skip to main content

matrixcode_core/workflow/
template.rs

1//! Template Rendering Engine
2//!
3//! 简单的模板渲染引擎,支持 {{var}} 格式的变量替换。
4
5use anyhow::{Context, Result};
6use regex::Regex;
7use std::collections::HashMap;
8
9/// 模板渲染器
10#[derive(Debug)]
11pub struct TemplateRenderer {
12    /// 变量模式:{{var}}
13    var_pattern: Regex,
14    /// 嵌套访问模式:{{var.field}}
15    nested_pattern: Regex,
16    /// 默认值模式:{{var|default}}
17    default_pattern: Regex,
18}
19
20impl Default for TemplateRenderer {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl TemplateRenderer {
27    /// 创建新的模板渲染器
28    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    /// 渲染模板字符串
37    ///
38    /// 支持以下语法:
39    /// - `{{var}}` - 简单变量替换
40    /// - `{{var.field}}` - 嵌套访问
41    /// - `{{var|default}}` - 带默认值
42    pub fn render(&self, template: &str, variables: &HashMap<String, serde_json::Value>) -> Result<String> {
43        let mut result = template.to_string();
44
45        // 先处理嵌套访问(更具体的模式)
46        result = self.render_nested(&result, variables)?;
47
48        // 处理带默认值的情况
49        result = self.render_with_default(&result, variables)?;
50
51        // 最后处理简单变量
52        result = self.render_simple(&result, variables)?;
53
54        Ok(result)
55    }
56
57    /// 渲染简单变量 {{var}}
58    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    /// 渲染嵌套访问 {{var.field}}
75    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    /// 渲染带默认值 {{var|default}}
92    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    /// 解析路径(如 var.field.subfield)
111    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    /// 将 JSON 值转换为字符串
133    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    /// 从模板中提取所有变量名
149    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    /// 检查模板是否包含未解析的变量
173    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    /// 渲染参数 HashMap
180    ///
181    /// 对每个参数值,如果是字符串则渲染模板,否则保持原值
182    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
201/// 便捷渲染函数
202pub 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}