ricecoder_commands/
template.rs

1use crate::error::{CommandError, Result};
2use regex::Regex;
3use std::collections::HashMap;
4
5/// Template processor for command substitution
6pub struct TemplateProcessor;
7
8impl TemplateProcessor {
9    /// Process a template string with variable substitution
10    ///
11    /// Supports the following placeholder formats:
12    /// - {{variable}} - simple substitution
13    /// - {{variable:default}} - substitution with default value
14    pub fn process(template: &str, variables: &HashMap<String, String>) -> Result<String> {
15        let mut result = template.to_string();
16
17        // Pattern for {{variable}} or {{variable:default}}
18        let pattern = Regex::new(r"\{\{([a-zA-Z_][a-zA-Z0-9_]*(?::[^}]*)?)\}\}")?;
19
20        for cap in pattern.captures_iter(template) {
21            let full_match = cap.get(0).unwrap().as_str();
22            let var_spec = cap.get(1).unwrap().as_str();
23
24            // Parse variable name and default value
25            let (var_name, default_value) = if let Some(colon_pos) = var_spec.find(':') {
26                let (name, default) = var_spec.split_at(colon_pos);
27                (name, Some(&default[1..]))
28            } else {
29                (var_spec, None)
30            };
31
32            // Get the value from variables or use default
33            let value = variables
34                .get(var_name)
35                .map(|s| s.as_str())
36                .or(default_value)
37                .ok_or_else(|| {
38                    CommandError::TemplateError(format!("Missing variable: {}", var_name))
39                })?;
40
41            result = result.replace(full_match, value);
42        }
43
44        Ok(result)
45    }
46
47    /// Extract all variable names from a template
48    pub fn extract_variables(template: &str) -> Result<Vec<String>> {
49        let pattern = Regex::new(r"\{\{([a-zA-Z_][a-zA-Z0-9_]*(?::[^}]*)?)\}\}")?;
50        let mut variables = Vec::new();
51
52        for cap in pattern.captures_iter(template) {
53            let var_spec = cap.get(1).unwrap().as_str();
54            let var_name = if let Some(colon_pos) = var_spec.find(':') {
55                &var_spec[..colon_pos]
56            } else {
57                var_spec
58            };
59            if !variables.contains(&var_name.to_string()) {
60                variables.push(var_name.to_string());
61            }
62        }
63
64        Ok(variables)
65    }
66
67    /// Check if a template has all required variables
68    pub fn validate_variables(template: &str, provided: &HashMap<String, String>) -> Result<()> {
69        let required = Self::extract_variables(template)?;
70
71        for var in required {
72            if !provided.contains_key(&var) {
73                // Check if variable has a default value in template
74                let pattern = Regex::new(&format!(r"\{{\{{{var}:([^}}]*)\}}\}}"))?;
75                if !pattern.is_match(template) {
76                    return Err(CommandError::TemplateError(format!(
77                        "Missing required variable: {}",
78                        var
79                    )));
80                }
81            }
82        }
83
84        Ok(())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_simple_substitution() {
94        let mut vars = HashMap::new();
95        vars.insert("name".to_string(), "Alice".to_string());
96        vars.insert("age".to_string(), "30".to_string());
97
98        let template = "Hello {{name}}, you are {{age}} years old";
99        let result = TemplateProcessor::process(template, &vars).unwrap();
100        assert_eq!(result, "Hello Alice, you are 30 years old");
101    }
102
103    #[test]
104    fn test_substitution_with_default() {
105        let mut vars = HashMap::new();
106        vars.insert("name".to_string(), "Bob".to_string());
107
108        let template = "Hello {{name}}, you are {{age:unknown}} years old";
109        let result = TemplateProcessor::process(template, &vars).unwrap();
110        assert_eq!(result, "Hello Bob, you are unknown years old");
111    }
112
113    #[test]
114    fn test_missing_variable_error() {
115        let vars = HashMap::new();
116        let template = "Hello {{name}}";
117        let result = TemplateProcessor::process(template, &vars);
118        assert!(result.is_err());
119    }
120
121    #[test]
122    fn test_extract_variables() {
123        let template = "Hello {{name}}, you are {{age:unknown}} years old";
124        let vars = TemplateProcessor::extract_variables(template).unwrap();
125        assert_eq!(vars.len(), 2);
126        assert!(vars.contains(&"name".to_string()));
127        assert!(vars.contains(&"age".to_string()));
128    }
129
130    #[test]
131    fn test_validate_variables_success() {
132        let mut vars = HashMap::new();
133        vars.insert("name".to_string(), "Charlie".to_string());
134
135        let template = "Hello {{name}}";
136        let result = TemplateProcessor::validate_variables(template, &vars);
137        assert!(result.is_ok());
138    }
139
140    #[test]
141    fn test_validate_variables_with_default() {
142        let vars = HashMap::new();
143        let template = "Hello {{name:Guest}}";
144        let result = TemplateProcessor::validate_variables(template, &vars);
145        assert!(result.is_ok());
146    }
147
148    #[test]
149    fn test_validate_variables_missing() {
150        let vars = HashMap::new();
151        let template = "Hello {{name}}";
152        let result = TemplateProcessor::validate_variables(template, &vars);
153        assert!(result.is_err());
154    }
155
156    #[test]
157    fn test_no_variables() {
158        let vars = HashMap::new();
159        let template = "Hello world";
160        let result = TemplateProcessor::process(template, &vars).unwrap();
161        assert_eq!(result, "Hello world");
162    }
163}