ricecoder_generation/templates/
engine.rs

1//! Template engine for rendering templates with variable substitution
2//!
3//! Provides template rendering with support for:
4//! - Variable substitution with case transformations
5//! - Conditional blocks ({{#if}}...{{/if}})
6//! - Loops ({{#each}}...{{/each}})
7//! - Includes/partials ({{> partial}})
8
9use crate::models::{RenderResult, TemplateContext};
10use crate::templates::error::TemplateError;
11use crate::templates::parser::{TemplateElement, TemplateParser};
12use crate::templates::resolver::{CaseTransform, PlaceholderResolver};
13use std::collections::HashMap;
14
15/// Template engine for rendering templates with variable substitution
16pub struct TemplateEngine {
17    /// Placeholder resolver for case transformations
18    resolver: PlaceholderResolver,
19}
20
21impl TemplateEngine {
22    /// Create a new template engine
23    pub fn new() -> Self {
24        Self {
25            resolver: PlaceholderResolver::new(),
26        }
27    }
28
29    /// Create a new template engine with custom resolver
30    pub fn with_resolver(resolver: PlaceholderResolver) -> Self {
31        Self { resolver }
32    }
33
34    /// Add a value for placeholder substitution
35    pub fn add_value(&mut self, name: impl Into<String>, value: impl Into<String>) {
36        self.resolver.add_value(name, value);
37    }
38
39    /// Add multiple values at once
40    pub fn add_values(&mut self, values: HashMap<String, String>) {
41        self.resolver.add_values(values);
42    }
43
44    /// Mark a placeholder as required
45    pub fn require(&mut self, name: impl Into<String>) {
46        self.resolver.require(name);
47    }
48
49    /// Render a template with the provided context
50    ///
51    /// # Arguments
52    /// * `template_content` - The template content to render
53    /// * `context` - The rendering context with values and options
54    ///
55    /// # Returns
56    /// Rendered content or error
57    pub fn render(
58        &self,
59        template_content: &str,
60        context: &TemplateContext,
61    ) -> Result<RenderResult, TemplateError> {
62        // Parse the template
63        let parsed = TemplateParser::parse(template_content)?;
64
65        // Validate all required placeholders are provided
66        self.resolver.validate()?;
67
68        // Render the template
69        let rendered = self.render_elements(&parsed.elements, context)?;
70
71        Ok(RenderResult {
72            content: rendered,
73            warnings: Vec::new(),
74            placeholders_used: self.resolver.provided_names(),
75        })
76    }
77
78    /// Render template elements recursively
79    fn render_elements(
80        &self,
81        elements: &[TemplateElement],
82        context: &TemplateContext,
83    ) -> Result<String, TemplateError> {
84        let mut result = String::new();
85
86        for element in elements {
87            match element {
88                TemplateElement::Text(text) => {
89                    result.push_str(text);
90                }
91                TemplateElement::Placeholder(placeholder_name) => {
92                    let rendered = self.render_placeholder(placeholder_name, context)?;
93                    result.push_str(&rendered);
94                }
95                TemplateElement::Conditional { condition, content } => {
96                    if self.evaluate_condition(condition, context)? {
97                        let rendered = self.render_elements(content, context)?;
98                        result.push_str(&rendered);
99                    }
100                }
101                TemplateElement::Loop { variable, content } => {
102                    let rendered = self.render_loop(variable, content, context)?;
103                    result.push_str(&rendered);
104                }
105                TemplateElement::Include(partial_name) => {
106                    // For now, includes are not supported in the basic implementation
107                    // This would require a template loader
108                    return Err(TemplateError::RenderError(format!(
109                        "Includes not yet supported: {}",
110                        partial_name
111                    )));
112                }
113            }
114        }
115
116        Ok(result)
117    }
118
119    /// Render a placeholder with case transformation
120    fn render_placeholder(
121        &self,
122        placeholder_name: &str,
123        _context: &TemplateContext,
124    ) -> Result<String, TemplateError> {
125        // Parse the placeholder syntax to extract name and case transform
126        let (name, case_transform) = self.parse_placeholder_syntax(placeholder_name)?;
127
128        // Resolve the placeholder with the case transformation
129        self.resolver.resolve(&name, case_transform)
130    }
131
132    /// Parse placeholder syntax to extract name and case transform
133    fn parse_placeholder_syntax(
134        &self,
135        content: &str,
136    ) -> Result<(String, CaseTransform), TemplateError> {
137        let content = content.trim();
138
139        // Determine case transform based on suffix
140        if content.ends_with("_snake") {
141            let name = content.trim_end_matches("_snake").to_lowercase();
142            Ok((name, CaseTransform::SnakeCase))
143        } else if content.ends_with("-kebab") {
144            let name = content.trim_end_matches("-kebab").to_lowercase();
145            Ok((name, CaseTransform::KebabCase))
146        } else if content.ends_with("Camel") {
147            let name = content.trim_end_matches("Camel").to_lowercase();
148            Ok((name, CaseTransform::CamelCase))
149        } else if content.chars().all(|c| c.is_uppercase() || c == '_') && content.len() > 1 {
150            // All uppercase = UPPERCASE transform
151            let name = content.to_lowercase();
152            Ok((name, CaseTransform::UpperCase))
153        } else if content.chars().next().is_some_and(|c| c.is_uppercase()) {
154            // Starts with uppercase = PascalCase
155            let name = content.to_lowercase();
156            Ok((name, CaseTransform::PascalCase))
157        } else {
158            // Default to lowercase
159            Ok((content.to_string(), CaseTransform::LowerCase))
160        }
161    }
162
163    /// Evaluate a condition expression
164    fn evaluate_condition(
165        &self,
166        condition: &str,
167        _context: &TemplateContext,
168    ) -> Result<bool, TemplateError> {
169        // Simple condition evaluation
170        // For now, just check if the condition is a non-empty placeholder name
171        let condition = condition.trim();
172
173        // Check if placeholder exists and has a truthy value
174        if self.resolver.has_value(condition) {
175            Ok(true)
176        } else {
177            Ok(false)
178        }
179    }
180
181    /// Render a loop block
182    fn render_loop(
183        &self,
184        variable: &str,
185        _content: &[TemplateElement],
186        _context: &TemplateContext,
187    ) -> Result<String, TemplateError> {
188        // For now, loops are not fully supported
189        // This would require iterating over array values
190        Err(TemplateError::RenderError(format!(
191            "Loops not yet fully supported: {}",
192            variable
193        )))
194    }
195
196    /// Render a template string directly with simple variable substitution
197    ///
198    /// This is a simpler alternative to the full render method that just does
199    /// basic placeholder substitution without parsing complex template syntax.
200    pub fn render_simple(&self, template_content: &str) -> Result<String, TemplateError> {
201        let mut result = template_content.to_string();
202
203        // Find and replace all placeholders
204        while let Some(start) = result.find("{{") {
205            if let Some(end_offset) = result[start..].find("}}") {
206                let end = start + end_offset + 1; // +1 to include the second }
207                let placeholder_content = &result[start + 2..start + end_offset];
208
209                // Parse placeholder syntax
210                let (name, case_transform) = self.parse_placeholder_syntax(placeholder_content)?;
211
212                // Resolve the placeholder
213                let resolved = self.resolver.resolve(&name, case_transform)?;
214
215                // Replace the placeholder (including the {{ and }})
216                result.replace_range(start..=end, &resolved);
217            } else {
218                break;
219            }
220        }
221
222        Ok(result)
223    }
224}
225
226impl Default for TemplateEngine {
227    fn default() -> Self {
228        Self::new()
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use crate::models::RenderOptions;
236
237    #[test]
238    fn test_template_engine_creation() {
239        let engine = TemplateEngine::new();
240        assert_eq!(engine.resolver.provided_names().len(), 0);
241    }
242
243    #[test]
244    fn test_add_value() {
245        let mut engine = TemplateEngine::new();
246        engine.add_value("name", "my_project");
247        assert_eq!(engine.resolver.provided_names().len(), 1);
248    }
249
250    #[test]
251    fn test_render_simple_placeholder() {
252        let mut engine = TemplateEngine::new();
253        engine.add_value("name", "my_project");
254        let result = engine.render_simple("Hello {{name}}");
255        assert_eq!(result.unwrap(), "Hello my_project");
256    }
257
258    #[test]
259    fn test_render_simple_pascal_case() {
260        let mut engine = TemplateEngine::new();
261        engine.add_value("name", "my_project");
262        let result = engine.render_simple("struct {{Name}} {}");
263        assert_eq!(result.unwrap(), "struct MyProject {}");
264    }
265
266    #[test]
267    fn test_render_simple_snake_case() {
268        let mut engine = TemplateEngine::new();
269        engine.add_value("name", "MyProject");
270        let result = engine.render_simple("let {{name_snake}} = 42;");
271        assert_eq!(result.unwrap(), "let my_project = 42;");
272    }
273
274    #[test]
275    fn test_render_simple_kebab_case() {
276        let mut engine = TemplateEngine::new();
277        engine.add_value("name", "MyProject");
278        let result = engine.render_simple("package-name: {{name-kebab}}");
279        assert_eq!(result.unwrap(), "package-name: my-project");
280    }
281
282    #[test]
283    fn test_render_simple_uppercase() {
284        let mut engine = TemplateEngine::new();
285        engine.add_value("name", "my_project");
286        let result = engine.render_simple("const {{NAME}} = 1;");
287        assert_eq!(result.unwrap(), "const MY_PROJECT = 1;");
288    }
289
290    #[test]
291    fn test_render_simple_camel_case() {
292        let mut engine = TemplateEngine::new();
293        engine.add_value("name", "my_project");
294        let result = engine.render_simple("function {{nameCamel}}() {}");
295        assert_eq!(result.unwrap(), "function myProject() {}");
296    }
297
298    #[test]
299    fn test_render_simple_multiple_placeholders() {
300        let mut engine = TemplateEngine::new();
301        engine.add_value("name", "my_project");
302        engine.add_value("author", "john_doe");
303        let result = engine.render_simple("Project: {{Name}}, Author: {{author}}");
304        assert_eq!(result.unwrap(), "Project: MyProject, Author: john_doe");
305    }
306
307    #[test]
308    fn test_render_simple_missing_placeholder() {
309        let engine = TemplateEngine::new();
310        let result = engine.render_simple("Hello {{name}}");
311        assert!(result.is_err());
312    }
313
314    #[test]
315    fn test_parse_placeholder_syntax_pascal_case() {
316        let engine = TemplateEngine::new();
317        let (name, transform) = engine.parse_placeholder_syntax("Name").unwrap();
318        assert_eq!(name, "name");
319        assert_eq!(transform, CaseTransform::PascalCase);
320    }
321
322    #[test]
323    fn test_parse_placeholder_syntax_snake_case() {
324        let engine = TemplateEngine::new();
325        let (name, transform) = engine.parse_placeholder_syntax("name_snake").unwrap();
326        assert_eq!(name, "name");
327        assert_eq!(transform, CaseTransform::SnakeCase);
328    }
329
330    #[test]
331    fn test_parse_placeholder_syntax_kebab_case() {
332        let engine = TemplateEngine::new();
333        let (name, transform) = engine.parse_placeholder_syntax("name-kebab").unwrap();
334        assert_eq!(name, "name");
335        assert_eq!(transform, CaseTransform::KebabCase);
336    }
337
338    #[test]
339    fn test_parse_placeholder_syntax_uppercase() {
340        let engine = TemplateEngine::new();
341        let (name, transform) = engine.parse_placeholder_syntax("NAME").unwrap();
342        assert_eq!(name, "name");
343        assert_eq!(transform, CaseTransform::UpperCase);
344    }
345
346    #[test]
347    fn test_parse_placeholder_syntax_camel_case() {
348        let engine = TemplateEngine::new();
349        let (name, transform) = engine.parse_placeholder_syntax("nameCamel").unwrap();
350        assert_eq!(name, "name");
351        assert_eq!(transform, CaseTransform::CamelCase);
352    }
353
354    #[test]
355    fn test_parse_placeholder_syntax_lowercase() {
356        let engine = TemplateEngine::new();
357        let (name, transform) = engine.parse_placeholder_syntax("name").unwrap();
358        assert_eq!(name, "name");
359        assert_eq!(transform, CaseTransform::LowerCase);
360    }
361
362    #[test]
363    fn test_render_with_context() {
364        let mut engine = TemplateEngine::new();
365        engine.add_value("name", "my_project");
366
367        let context = TemplateContext {
368            values: Default::default(),
369            options: RenderOptions::default(),
370        };
371
372        let result = engine.render("Hello {{name}}", &context);
373        assert!(result.is_ok());
374        assert_eq!(result.unwrap().content, "Hello my_project");
375    }
376
377    #[test]
378    fn test_render_result_includes_placeholders_used() {
379        let mut engine = TemplateEngine::new();
380        engine.add_value("name", "my_project");
381
382        let context = TemplateContext {
383            values: Default::default(),
384            options: RenderOptions::default(),
385        };
386
387        let result = engine.render("Hello {{name}}", &context).unwrap();
388        assert!(result.placeholders_used.contains(&"name".to_string()));
389    }
390}