ricecoder_generation/templates/
validation.rs

1//! Template validation engine
2//!
3//! Validates template syntax, checks placeholder references,
4//! and validates boilerplate structure.
5
6use crate::models::{Boilerplate, BoilerplateFile, Template};
7use crate::templates::error::{BoilerplateError, TemplateError};
8use crate::templates::parser::{ParsedTemplate, TemplateElement, TemplateParser};
9use std::collections::HashSet;
10
11/// Template validation engine
12pub struct ValidationEngine;
13
14impl ValidationEngine {
15    /// Validate template syntax
16    ///
17    /// # Arguments
18    /// * `content` - Template content to validate
19    ///
20    /// # Returns
21    /// Ok if valid, Err with line number if invalid
22    pub fn validate_template_syntax(content: &str) -> Result<(), TemplateError> {
23        TemplateParser::parse(content)?;
24        Ok(())
25    }
26
27    /// Validate that all required placeholders are provided
28    ///
29    /// # Arguments
30    /// * `template` - Template to validate
31    /// * `provided_placeholders` - Set of provided placeholder names
32    ///
33    /// # Returns
34    /// Ok if all required placeholders provided, Err otherwise
35    pub fn validate_placeholder_references(
36        template: &Template,
37        provided_placeholders: &HashSet<String>,
38    ) -> Result<(), TemplateError> {
39        for placeholder in &template.placeholders {
40            if placeholder.required && !provided_placeholders.contains(&placeholder.name) {
41                return Err(TemplateError::MissingPlaceholder(placeholder.name.clone()));
42            }
43        }
44        Ok(())
45    }
46
47    /// Validate boilerplate structure
48    ///
49    /// # Arguments
50    /// * `boilerplate` - Boilerplate to validate
51    ///
52    /// # Returns
53    /// Ok if valid, Err if structure is invalid
54    pub fn validate_boilerplate_structure(
55        boilerplate: &Boilerplate,
56    ) -> Result<(), BoilerplateError> {
57        // Check required fields
58        if boilerplate.id.is_empty() {
59            return Err(BoilerplateError::InvalidStructure(
60                "Boilerplate ID cannot be empty".to_string(),
61            ));
62        }
63
64        if boilerplate.name.is_empty() {
65            return Err(BoilerplateError::InvalidStructure(
66                "Boilerplate name cannot be empty".to_string(),
67            ));
68        }
69
70        if boilerplate.language.is_empty() {
71            return Err(BoilerplateError::InvalidStructure(
72                "Boilerplate language cannot be empty".to_string(),
73            ));
74        }
75
76        // Check files
77        if boilerplate.files.is_empty() {
78            return Err(BoilerplateError::InvalidStructure(
79                "Boilerplate must have at least one file".to_string(),
80            ));
81        }
82
83        // Validate each file
84        for file in &boilerplate.files {
85            Self::validate_boilerplate_file(file)?;
86        }
87
88        Ok(())
89    }
90
91    /// Validate a single boilerplate file
92    fn validate_boilerplate_file(file: &BoilerplateFile) -> Result<(), BoilerplateError> {
93        if file.path.is_empty() {
94            return Err(BoilerplateError::InvalidStructure(
95                "Boilerplate file path cannot be empty".to_string(),
96            ));
97        }
98
99        if file.template.is_empty() {
100            return Err(BoilerplateError::InvalidStructure(
101                "Boilerplate file template cannot be empty".to_string(),
102            ));
103        }
104
105        // Validate template syntax if it looks like a template
106        if file.template.contains("{{") {
107            ValidationEngine::validate_template_syntax(&file.template).map_err(|e| {
108                BoilerplateError::InvalidStructure(format!(
109                    "Invalid template in file {}: {}",
110                    file.path, e
111                ))
112            })?;
113        }
114
115        Ok(())
116    }
117
118    /// Validate placeholder consistency in template
119    ///
120    /// # Arguments
121    /// * `parsed_template` - Parsed template to validate
122    ///
123    /// # Returns
124    /// Ok if consistent, Err if issues found
125    pub fn validate_placeholder_consistency(
126        parsed_template: &ParsedTemplate,
127    ) -> Result<(), TemplateError> {
128        // Check for duplicate placeholder definitions
129        let mut seen = HashSet::new();
130        for placeholder in &parsed_template.placeholders {
131            if !seen.insert(&placeholder.name) {
132                return Err(TemplateError::ValidationFailed(format!(
133                    "Duplicate placeholder definition: {}",
134                    placeholder.name
135                )));
136            }
137        }
138
139        Ok(())
140    }
141
142    /// Validate template block nesting
143    ///
144    /// # Arguments
145    /// * `elements` - Template elements to validate
146    ///
147    /// # Returns
148    /// Ok if nesting is valid, Err if issues found
149    pub fn validate_block_nesting(elements: &[TemplateElement]) -> Result<(), TemplateError> {
150        Self::validate_nesting_recursive(elements, 0)
151    }
152
153    fn validate_nesting_recursive(
154        elements: &[TemplateElement],
155        depth: usize,
156    ) -> Result<(), TemplateError> {
157        // Prevent excessive nesting (max 10 levels)
158        if depth > 10 {
159            return Err(TemplateError::ValidationFailed(
160                "Template nesting too deep (max 10 levels)".to_string(),
161            ));
162        }
163
164        for element in elements {
165            match element {
166                TemplateElement::Conditional { content, .. }
167                | TemplateElement::Loop { content, .. } => {
168                    Self::validate_nesting_recursive(content, depth + 1)?;
169                }
170                _ => {}
171            }
172        }
173
174        Ok(())
175    }
176
177    /// Validate that all referenced partials exist
178    ///
179    /// # Arguments
180    /// * `elements` - Template elements
181    /// * `available_partials` - Set of available partial names
182    ///
183    /// # Returns
184    /// Ok if all partials exist, Err if missing
185    pub fn validate_partial_references(
186        elements: &[TemplateElement],
187        available_partials: &HashSet<String>,
188    ) -> Result<(), TemplateError> {
189        Self::validate_partials_recursive(elements, available_partials)
190    }
191
192    fn validate_partials_recursive(
193        elements: &[TemplateElement],
194        available_partials: &HashSet<String>,
195    ) -> Result<(), TemplateError> {
196        for element in elements {
197            match element {
198                TemplateElement::Include(partial_name) => {
199                    if !available_partials.contains(partial_name) {
200                        return Err(TemplateError::ValidationFailed(format!(
201                            "Referenced partial not found: {}",
202                            partial_name
203                        )));
204                    }
205                }
206                TemplateElement::Conditional { content, .. }
207                | TemplateElement::Loop { content, .. } => {
208                    Self::validate_partials_recursive(content, available_partials)?;
209                }
210                _ => {}
211            }
212        }
213
214        Ok(())
215    }
216
217    /// Comprehensive template validation
218    ///
219    /// # Arguments
220    /// * `template` - Template to validate
221    /// * `provided_placeholders` - Set of provided placeholder names
222    /// * `available_partials` - Set of available partial names
223    ///
224    /// # Returns
225    /// Ok if all validations pass, Err if any fail
226    pub fn validate_template_comprehensive(
227        template: &Template,
228        provided_placeholders: &HashSet<String>,
229        available_partials: &HashSet<String>,
230    ) -> Result<(), TemplateError> {
231        // Validate syntax
232        Self::validate_template_syntax(&template.content)?;
233
234        // Parse template
235        let parsed = TemplateParser::parse(&template.content)?;
236
237        // Validate placeholder consistency
238        Self::validate_placeholder_consistency(&parsed)?;
239
240        // Validate block nesting
241        Self::validate_block_nesting(&parsed.elements)?;
242
243        // Validate partial references
244        Self::validate_partial_references(&parsed.elements, available_partials)?;
245
246        // Validate placeholder references
247        Self::validate_placeholder_references(template, provided_placeholders)?;
248
249        Ok(())
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::models::Placeholder;
257
258    #[test]
259    fn test_validate_valid_template_syntax() {
260        let content = "Hello {{name}}";
261        assert!(ValidationEngine::validate_template_syntax(content).is_ok());
262    }
263
264    #[test]
265    fn test_validate_invalid_template_syntax() {
266        let content = "Hello {{name";
267        assert!(ValidationEngine::validate_template_syntax(content).is_err());
268    }
269
270    #[test]
271    fn test_validate_placeholder_references_all_provided() {
272        let template = Template {
273            id: "test".to_string(),
274            name: "test".to_string(),
275            language: "rust".to_string(),
276            content: "{{name}}".to_string(),
277            placeholders: vec![Placeholder {
278                name: "name".to_string(),
279                description: "Name".to_string(),
280                default: None,
281                required: true,
282            }],
283            metadata: Default::default(),
284        };
285
286        let mut provided = HashSet::new();
287        provided.insert("name".to_string());
288
289        assert!(ValidationEngine::validate_placeholder_references(&template, &provided).is_ok());
290    }
291
292    #[test]
293    fn test_validate_placeholder_references_missing() {
294        let template = Template {
295            id: "test".to_string(),
296            name: "test".to_string(),
297            language: "rust".to_string(),
298            content: "{{name}}".to_string(),
299            placeholders: vec![Placeholder {
300                name: "name".to_string(),
301                description: "Name".to_string(),
302                default: None,
303                required: true,
304            }],
305            metadata: Default::default(),
306        };
307
308        let provided = HashSet::new();
309        assert!(ValidationEngine::validate_placeholder_references(&template, &provided).is_err());
310    }
311
312    #[test]
313    fn test_validate_boilerplate_structure_valid() {
314        let boilerplate = Boilerplate {
315            id: "test".to_string(),
316            name: "Test".to_string(),
317            description: "Test boilerplate".to_string(),
318            language: "rust".to_string(),
319            files: vec![BoilerplateFile {
320                path: "src/main.rs".to_string(),
321                template: "fn main() {}".to_string(),
322                condition: None,
323            }],
324            dependencies: vec![],
325            scripts: vec![],
326        };
327
328        assert!(ValidationEngine::validate_boilerplate_structure(&boilerplate).is_ok());
329    }
330
331    #[test]
332    fn test_validate_boilerplate_structure_empty_id() {
333        let boilerplate = Boilerplate {
334            id: "".to_string(),
335            name: "Test".to_string(),
336            description: "Test boilerplate".to_string(),
337            language: "rust".to_string(),
338            files: vec![BoilerplateFile {
339                path: "src/main.rs".to_string(),
340                template: "fn main() {}".to_string(),
341                condition: None,
342            }],
343            dependencies: vec![],
344            scripts: vec![],
345        };
346
347        assert!(ValidationEngine::validate_boilerplate_structure(&boilerplate).is_err());
348    }
349
350    #[test]
351    fn test_validate_boilerplate_structure_no_files() {
352        let boilerplate = Boilerplate {
353            id: "test".to_string(),
354            name: "Test".to_string(),
355            description: "Test boilerplate".to_string(),
356            language: "rust".to_string(),
357            files: vec![],
358            dependencies: vec![],
359            scripts: vec![],
360        };
361
362        assert!(ValidationEngine::validate_boilerplate_structure(&boilerplate).is_err());
363    }
364
365    #[test]
366    fn test_validate_block_nesting_valid() {
367        let elements = vec![TemplateElement::Conditional {
368            condition: "test".to_string(),
369            content: vec![TemplateElement::Text("content".to_string())],
370        }];
371
372        assert!(ValidationEngine::validate_block_nesting(&elements).is_ok());
373    }
374
375    #[test]
376    fn test_validate_partial_references_valid() {
377        let elements = vec![TemplateElement::Include("header".to_string())];
378        let mut available = HashSet::new();
379        available.insert("header".to_string());
380
381        assert!(ValidationEngine::validate_partial_references(&elements, &available).is_ok());
382    }
383
384    #[test]
385    fn test_validate_partial_references_missing() {
386        let elements = vec![TemplateElement::Include("missing".to_string())];
387        let available = HashSet::new();
388
389        assert!(ValidationEngine::validate_partial_references(&elements, &available).is_err());
390    }
391}