ricecoder_generation/templates/
validation.rs1use crate::models::{Boilerplate, BoilerplateFile, Template};
7use crate::templates::error::{BoilerplateError, TemplateError};
8use crate::templates::parser::{ParsedTemplate, TemplateElement, TemplateParser};
9use std::collections::HashSet;
10
11pub struct ValidationEngine;
13
14impl ValidationEngine {
15 pub fn validate_template_syntax(content: &str) -> Result<(), TemplateError> {
23 TemplateParser::parse(content)?;
24 Ok(())
25 }
26
27 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 pub fn validate_boilerplate_structure(
55 boilerplate: &Boilerplate,
56 ) -> Result<(), BoilerplateError> {
57 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 if boilerplate.files.is_empty() {
78 return Err(BoilerplateError::InvalidStructure(
79 "Boilerplate must have at least one file".to_string(),
80 ));
81 }
82
83 for file in &boilerplate.files {
85 Self::validate_boilerplate_file(file)?;
86 }
87
88 Ok(())
89 }
90
91 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 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 pub fn validate_placeholder_consistency(
126 parsed_template: &ParsedTemplate,
127 ) -> Result<(), TemplateError> {
128 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 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 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 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 pub fn validate_template_comprehensive(
227 template: &Template,
228 provided_placeholders: &HashSet<String>,
229 available_partials: &HashSet<String>,
230 ) -> Result<(), TemplateError> {
231 Self::validate_template_syntax(&template.content)?;
233
234 let parsed = TemplateParser::parse(&template.content)?;
236
237 Self::validate_placeholder_consistency(&parsed)?;
239
240 Self::validate_block_nesting(&parsed.elements)?;
242
243 Self::validate_partial_references(&parsed.elements, available_partials)?;
245
246 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}