ricecoder_generation/templates/
boilerplate.rs

1//! Boilerplate management for project scaffolding
2//!
3//! Handles loading, parsing, validating, and applying boilerplates to create new projects.
4//! Supports variable customization, file conflict resolution, and custom boilerplate creation.
5
6use crate::models::{Boilerplate, BoilerplateFile, BoilerplateSource, ConflictResolution};
7use crate::templates::discovery::BoilerplateDiscovery;
8use crate::templates::error::BoilerplateError;
9use crate::templates::resolver::{CaseTransform, PlaceholderResolver};
10use std::collections::HashMap;
11use std::fs;
12use std::path::Path;
13
14/// Manages boilerplate operations: loading, validation, and application
15///
16/// Implements requirements:
17/// - Requirement 3.1: Load and parse boilerplate definitions
18/// - Requirement 3.2: Validate boilerplate before application
19/// - Requirement 3.3: Prompt for variable customization
20/// - Requirement 3.4: Handle file conflicts (skip/overwrite/merge)
21/// - Requirement 3.5: Support custom boilerplate creation
22/// - Requirement 3.6: Validate boilerplate structure
23pub struct BoilerplateManager;
24
25impl BoilerplateManager {
26    /// Create a new boilerplate manager
27    pub fn new() -> Self {
28        Self
29    }
30
31    /// Load a boilerplate from a directory
32    ///
33    /// Parses the boilerplate metadata and validates the structure.
34    ///
35    /// # Arguments
36    /// * `boilerplate_path` - Path to the boilerplate directory
37    ///
38    /// # Returns
39    /// Loaded boilerplate or error
40    ///
41    /// # Requirements
42    /// - Requirement 3.1: Load and parse boilerplate definitions
43    /// - Requirement 3.2: Validate boilerplate before application
44    pub fn load(&self, boilerplate_path: &Path) -> Result<Boilerplate, BoilerplateError> {
45        // Validate boilerplate structure
46        BoilerplateDiscovery::validate_boilerplate(boilerplate_path)?;
47
48        // Parse metadata
49        let boilerplate = BoilerplateDiscovery::parse_metadata(boilerplate_path)?;
50
51        Ok(boilerplate)
52    }
53
54    /// Load a boilerplate by name from discovery
55    ///
56    /// Searches for the boilerplate in project and global scopes.
57    ///
58    /// # Arguments
59    /// * `project_root` - Root directory of the project
60    /// * `boilerplate_name` - Name of the boilerplate to load
61    ///
62    /// # Returns
63    /// Loaded boilerplate or error
64    pub fn load_by_name(
65        &self,
66        project_root: &Path,
67        boilerplate_name: &str,
68    ) -> Result<Boilerplate, BoilerplateError> {
69        let discovery_result = BoilerplateDiscovery::discover(project_root)?;
70
71        // Find the boilerplate by name (project scope takes precedence)
72        let metadata = discovery_result
73            .boilerplates
74            .iter()
75            .find(|bp| bp.name.to_lowercase() == boilerplate_name.to_lowercase())
76            .ok_or_else(|| BoilerplateError::NotFound(boilerplate_name.to_string()))?;
77
78        // Get the path from the source
79        let path = match &metadata.source {
80            BoilerplateSource::Global(p) | BoilerplateSource::Project(p) => p,
81        };
82
83        self.load(path)
84    }
85
86    /// Validate a boilerplate structure
87    ///
88    /// Checks that the boilerplate has required fields and valid structure.
89    ///
90    /// # Arguments
91    /// * `boilerplate` - Boilerplate to validate
92    ///
93    /// # Returns
94    /// Ok if valid, error otherwise
95    ///
96    /// # Requirements
97    /// - Requirement 3.2: Validate boilerplate before application
98    pub fn validate(&self, boilerplate: &Boilerplate) -> Result<(), BoilerplateError> {
99        // Check required fields
100        if boilerplate.id.is_empty() {
101            return Err(BoilerplateError::ValidationFailed(
102                "Boilerplate ID cannot be empty".to_string(),
103            ));
104        }
105
106        if boilerplate.name.is_empty() {
107            return Err(BoilerplateError::ValidationFailed(
108                "Boilerplate name cannot be empty".to_string(),
109            ));
110        }
111
112        if boilerplate.language.is_empty() {
113            return Err(BoilerplateError::ValidationFailed(
114                "Boilerplate language cannot be empty".to_string(),
115            ));
116        }
117
118        // Check that files have valid paths
119        for file in &boilerplate.files {
120            if file.path.is_empty() {
121                return Err(BoilerplateError::ValidationFailed(
122                    "Boilerplate file path cannot be empty".to_string(),
123                ));
124            }
125
126            if file.template.is_empty() {
127                return Err(BoilerplateError::ValidationFailed(
128                    "Boilerplate file template cannot be empty".to_string(),
129                ));
130            }
131        }
132
133        Ok(())
134    }
135
136    /// Extract placeholders from a boilerplate
137    ///
138    /// Scans all template files in the boilerplate and extracts required placeholders.
139    ///
140    /// # Arguments
141    /// * `boilerplate` - Boilerplate to scan
142    ///
143    /// # Returns
144    /// Map of placeholder names to descriptions
145    pub fn extract_placeholders(
146        &self,
147        boilerplate: &Boilerplate,
148    ) -> Result<HashMap<String, String>, BoilerplateError> {
149        let mut placeholders = HashMap::new();
150
151        // Compile regex once outside the loop
152        let re = regex::Regex::new(r"\{\{([a-zA-Z_][a-zA-Z0-9_-]*)\}\}").unwrap();
153
154        for file in &boilerplate.files {
155            // Extract placeholders from template content using regex
156            for cap in re.captures_iter(&file.template) {
157                if let Some(name) = cap.get(1) {
158                    let placeholder_name = name.as_str().to_string();
159                    placeholders.insert(placeholder_name, format!("Placeholder in {}", file.path));
160                }
161            }
162        }
163
164        Ok(placeholders)
165    }
166
167    /// Apply a boilerplate to create a new project
168    ///
169    /// Creates the project structure from the boilerplate, rendering templates with provided values.
170    ///
171    /// # Arguments
172    /// * `boilerplate` - Boilerplate to apply
173    /// * `target_dir` - Directory where to create the project
174    /// * `variables` - Variable values for template substitution
175    /// * `conflict_resolution` - How to handle file conflicts
176    ///
177    /// # Returns
178    /// Result of scaffolding operation
179    ///
180    /// # Requirements
181    /// - Requirement 3.1: Create project structure from boilerplate
182    /// - Requirement 3.3: Render template files with context
183    /// - Requirement 3.4: Handle file conflicts (skip/overwrite/merge)
184    pub fn apply(
185        &self,
186        boilerplate: &Boilerplate,
187        target_dir: &Path,
188        variables: &HashMap<String, String>,
189        conflict_resolution: ConflictResolution,
190    ) -> Result<ScaffoldingResult, BoilerplateError> {
191        // Validate boilerplate
192        self.validate(boilerplate)?;
193
194        // Create target directory if it doesn't exist
195        fs::create_dir_all(target_dir).map_err(BoilerplateError::IoError)?;
196
197        let mut created_files = Vec::new();
198        let mut skipped_files = Vec::new();
199        let mut conflicts = Vec::new();
200
201        // Create files from boilerplate
202        for file in &boilerplate.files {
203            // Evaluate condition if present
204            if let Some(condition) = &file.condition {
205                if !self.evaluate_condition(condition, variables) {
206                    continue;
207                }
208            }
209
210            // Render template with variables
211            let mut resolver = PlaceholderResolver::new();
212            resolver.add_values(variables.clone());
213            let rendered_content = self.render_template(&file.template, &resolver)?;
214
215            // Determine target file path
216            let file_path = target_dir.join(&file.path);
217
218            // Create parent directories
219            if let Some(parent) = file_path.parent() {
220                fs::create_dir_all(parent).map_err(BoilerplateError::IoError)?;
221            }
222
223            // Handle file conflicts
224            if file_path.exists() {
225                match conflict_resolution {
226                    ConflictResolution::Skip => {
227                        skipped_files.push(file.path.clone());
228                        continue;
229                    }
230                    ConflictResolution::Overwrite => {
231                        // Continue to write the file
232                    }
233                    ConflictResolution::Merge => {
234                        // For now, treat merge as overwrite
235                        // In a full implementation, this would merge file contents
236                        conflicts.push(FileConflict {
237                            path: file.path.clone(),
238                            reason: "File already exists".to_string(),
239                            resolution: "Merged (overwritten)".to_string(),
240                        });
241                    }
242                }
243            }
244
245            // Write file
246            fs::write(&file_path, &rendered_content).map_err(BoilerplateError::IoError)?;
247
248            created_files.push(file.path.clone());
249        }
250
251        Ok(ScaffoldingResult {
252            created_files,
253            skipped_files,
254            conflicts,
255        })
256    }
257
258    /// Create a custom boilerplate from a template
259    ///
260    /// Generates a new boilerplate definition from a template directory.
261    ///
262    /// # Arguments
263    /// * `source_dir` - Directory containing the template files
264    /// * `boilerplate_id` - ID for the new boilerplate
265    /// * `boilerplate_name` - Name for the new boilerplate
266    /// * `language` - Programming language
267    ///
268    /// # Returns
269    /// Created boilerplate or error
270    ///
271    /// # Requirements
272    /// - Requirement 3.5: Support custom boilerplate creation
273    pub fn create_custom(
274        &self,
275        source_dir: &Path,
276        boilerplate_id: &str,
277        boilerplate_name: &str,
278        language: &str,
279    ) -> Result<Boilerplate, BoilerplateError> {
280        if !source_dir.exists() {
281            return Err(BoilerplateError::InvalidStructure(format!(
282                "Source directory not found: {}",
283                source_dir.display()
284            )));
285        }
286
287        let mut files = Vec::new();
288
289        // Scan source directory for template files
290        self.scan_directory(source_dir, source_dir, &mut files)?;
291
292        Ok(Boilerplate {
293            id: boilerplate_id.to_string(),
294            name: boilerplate_name.to_string(),
295            description: format!("Custom boilerplate for {}", language),
296            language: language.to_string(),
297            files,
298            dependencies: Vec::new(),
299            scripts: Vec::new(),
300        })
301    }
302
303    /// Save a boilerplate to disk
304    ///
305    /// Writes the boilerplate metadata and files to a directory.
306    ///
307    /// # Arguments
308    /// * `boilerplate` - Boilerplate to save
309    /// * `target_dir` - Directory where to save the boilerplate
310    ///
311    /// # Returns
312    /// Ok if successful, error otherwise
313    pub fn save(
314        &self,
315        boilerplate: &Boilerplate,
316        target_dir: &Path,
317    ) -> Result<(), BoilerplateError> {
318        // Validate boilerplate
319        self.validate(boilerplate)?;
320
321        // Create target directory
322        fs::create_dir_all(target_dir).map_err(BoilerplateError::IoError)?;
323
324        // Write metadata as YAML
325        let metadata_path = target_dir.join("boilerplate.yaml");
326        let yaml_content = serde_yaml::to_string(boilerplate)
327            .map_err(|e| BoilerplateError::InvalidStructure(format!("YAML error: {}", e)))?;
328
329        fs::write(&metadata_path, yaml_content).map_err(BoilerplateError::IoError)?;
330
331        Ok(())
332    }
333
334    /// Evaluate a condition string with variables
335    ///
336    /// Simple condition evaluation for file inclusion.
337    /// Supports basic syntax like "variable_name" or "!variable_name"
338    ///
339    /// # Arguments
340    /// * `condition` - Condition string to evaluate
341    /// * `variables` - Available variables
342    ///
343    /// # Returns
344    /// True if condition is met, false otherwise
345    fn evaluate_condition(&self, condition: &str, variables: &HashMap<String, String>) -> bool {
346        let condition = condition.trim();
347
348        // Handle negation
349        if let Some(var_name) = condition.strip_prefix('!') {
350            return !variables.contains_key(var_name.trim());
351        }
352
353        // Check if variable exists and is truthy
354        variables
355            .get(condition)
356            .map(|v| !v.is_empty() && v != "false" && v != "0")
357            .unwrap_or(false)
358    }
359
360    /// Render a template string with placeholder resolution
361    ///
362    /// # Arguments
363    /// * `template` - Template string with placeholders
364    /// * `resolver` - Placeholder resolver with values
365    ///
366    /// # Returns
367    /// Rendered content or error
368    fn render_template(
369        &self,
370        template: &str,
371        resolver: &PlaceholderResolver,
372    ) -> Result<String, BoilerplateError> {
373        let mut result = template.to_string();
374
375        // Find and replace all placeholders
376        let re = regex::Regex::new(r"\{\{([a-zA-Z_][a-zA-Z0-9_-]*(?:_snake|-kebab|Camel)?)\}\}")
377            .map_err(|e| BoilerplateError::InvalidStructure(format!("Regex error: {}", e)))?;
378
379        for cap in re.captures_iter(template) {
380            if let Some(placeholder_match) = cap.get(1) {
381                let placeholder_content = placeholder_match.as_str();
382
383                // Parse placeholder syntax to extract name and case transform
384                let (name, case_transform) = self.parse_placeholder_syntax(placeholder_content)?;
385
386                // Resolve the placeholder
387                let resolved = resolver
388                    .resolve(&name, case_transform)
389                    .map_err(|e| BoilerplateError::InvalidStructure(e.to_string()))?;
390
391                // Replace in result
392                let placeholder_str = format!("{{{{{}}}}}", placeholder_content);
393                result = result.replace(&placeholder_str, &resolved);
394            }
395        }
396
397        Ok(result)
398    }
399
400    /// Parse placeholder syntax to extract name and case transform
401    ///
402    /// # Arguments
403    /// * `content` - Placeholder content (e.g., "Name", "name_snake", "name-kebab")
404    ///
405    /// # Returns
406    /// Tuple of (name, case_transform) or error
407    fn parse_placeholder_syntax(
408        &self,
409        content: &str,
410    ) -> Result<(String, CaseTransform), BoilerplateError> {
411        let content = content.trim();
412
413        // Determine case transform based on suffix
414        if content.ends_with("_snake") {
415            let name = content.trim_end_matches("_snake").to_string();
416            Ok((name, CaseTransform::SnakeCase))
417        } else if content.ends_with("-kebab") {
418            let name = content.trim_end_matches("-kebab").to_string();
419            Ok((name, CaseTransform::KebabCase))
420        } else if content.ends_with("Camel") {
421            let name = content.trim_end_matches("Camel").to_string();
422            Ok((name, CaseTransform::CamelCase))
423        } else if content.chars().all(|c| c.is_uppercase() || c == '_') && content.len() > 1 {
424            // All uppercase = UPPERCASE transform
425            Ok((content.to_string(), CaseTransform::UpperCase))
426        } else if content.chars().next().is_some_and(|c| c.is_uppercase()) {
427            // Starts with uppercase = PascalCase
428            Ok((content.to_string(), CaseTransform::PascalCase))
429        } else {
430            // Default to lowercase
431            Ok((content.to_string(), CaseTransform::LowerCase))
432        }
433    }
434
435    /// Recursively scan a directory for template files
436    ///
437    /// # Arguments
438    /// * `current_dir` - Current directory being scanned
439    /// * `base_dir` - Base directory for relative path calculation
440    /// * `files` - Accumulator for found files
441    ///
442    /// # Returns
443    /// Ok if successful, error otherwise
444    #[allow(clippy::only_used_in_recursion)]
445    fn scan_directory(
446        &self,
447        current_dir: &Path,
448        base_dir: &Path,
449        files: &mut Vec<BoilerplateFile>,
450    ) -> Result<(), BoilerplateError> {
451        for entry in fs::read_dir(current_dir).map_err(BoilerplateError::IoError)? {
452            let entry = entry.map_err(BoilerplateError::IoError)?;
453            let path = entry.path();
454
455            if path.is_dir() {
456                // Recursively scan subdirectories
457                self.scan_directory(&path, base_dir, files)?;
458            } else if path.is_file() {
459                // Read file content
460                let content = fs::read_to_string(&path).map_err(BoilerplateError::IoError)?;
461
462                // Calculate relative path
463                let relative_path = path
464                    .strip_prefix(base_dir)
465                    .map_err(|_| {
466                        BoilerplateError::InvalidStructure(
467                            "Failed to calculate relative path".to_string(),
468                        )
469                    })?
470                    .to_string_lossy()
471                    .to_string();
472
473                files.push(BoilerplateFile {
474                    path: relative_path,
475                    template: content,
476                    condition: None,
477                });
478            }
479        }
480
481        Ok(())
482    }
483}
484
485impl Default for BoilerplateManager {
486    fn default() -> Self {
487        Self::new()
488    }
489}
490
491/// Result of scaffolding operation
492#[derive(Debug, Clone)]
493pub struct ScaffoldingResult {
494    /// Files that were created
495    pub created_files: Vec<String>,
496    /// Files that were skipped due to conflicts
497    pub skipped_files: Vec<String>,
498    /// File conflicts that occurred
499    pub conflicts: Vec<FileConflict>,
500}
501
502/// Information about a file conflict
503#[derive(Debug, Clone)]
504pub struct FileConflict {
505    /// Path to the conflicting file
506    pub path: String,
507    /// Reason for the conflict
508    pub reason: String,
509    /// How the conflict was resolved
510    pub resolution: String,
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    use tempfile::TempDir;
517
518    #[test]
519    fn test_create_boilerplate_manager() {
520        let _manager = BoilerplateManager::new();
521        // Manager is created successfully
522    }
523
524    #[test]
525    fn test_validate_boilerplate_success() {
526        let manager = BoilerplateManager::new();
527        let boilerplate = Boilerplate {
528            id: "test-bp".to_string(),
529            name: "Test Boilerplate".to_string(),
530            description: "A test boilerplate".to_string(),
531            language: "rust".to_string(),
532            files: vec![BoilerplateFile {
533                path: "src/main.rs".to_string(),
534                template: "fn main() {}".to_string(),
535                condition: None,
536            }],
537            dependencies: vec![],
538            scripts: vec![],
539        };
540
541        assert!(manager.validate(&boilerplate).is_ok());
542    }
543
544    #[test]
545    fn test_validate_boilerplate_missing_id() {
546        let manager = BoilerplateManager::new();
547        let boilerplate = Boilerplate {
548            id: "".to_string(),
549            name: "Test Boilerplate".to_string(),
550            description: "A test boilerplate".to_string(),
551            language: "rust".to_string(),
552            files: vec![],
553            dependencies: vec![],
554            scripts: vec![],
555        };
556
557        assert!(manager.validate(&boilerplate).is_err());
558    }
559
560    #[test]
561    fn test_validate_boilerplate_missing_name() {
562        let manager = BoilerplateManager::new();
563        let boilerplate = Boilerplate {
564            id: "test-bp".to_string(),
565            name: "".to_string(),
566            description: "A test boilerplate".to_string(),
567            language: "rust".to_string(),
568            files: vec![],
569            dependencies: vec![],
570            scripts: vec![],
571        };
572
573        assert!(manager.validate(&boilerplate).is_err());
574    }
575
576    #[test]
577    fn test_validate_boilerplate_missing_language() {
578        let manager = BoilerplateManager::new();
579        let boilerplate = Boilerplate {
580            id: "test-bp".to_string(),
581            name: "Test Boilerplate".to_string(),
582            description: "A test boilerplate".to_string(),
583            language: "".to_string(),
584            files: vec![],
585            dependencies: vec![],
586            scripts: vec![],
587        };
588
589        assert!(manager.validate(&boilerplate).is_err());
590    }
591
592    #[test]
593    fn test_extract_placeholders() {
594        let manager = BoilerplateManager::new();
595        let boilerplate = Boilerplate {
596            id: "test-bp".to_string(),
597            name: "Test Boilerplate".to_string(),
598            description: "A test boilerplate".to_string(),
599            language: "rust".to_string(),
600            files: vec![
601                BoilerplateFile {
602                    path: "src/main.rs".to_string(),
603                    template: "pub struct {{Name}} {}".to_string(),
604                    condition: None,
605                },
606                BoilerplateFile {
607                    path: "Cargo.toml".to_string(),
608                    template: "[package]\nname = \"{{name_snake}}\"".to_string(),
609                    condition: None,
610                },
611            ],
612            dependencies: vec![],
613            scripts: vec![],
614        };
615
616        let placeholders = manager.extract_placeholders(&boilerplate).unwrap();
617        assert!(placeholders.contains_key("Name"));
618        assert!(placeholders.contains_key("name_snake"));
619    }
620
621    #[test]
622    fn test_apply_boilerplate_skip_conflicts() {
623        let temp_dir = TempDir::new().unwrap();
624        let manager = BoilerplateManager::new();
625
626        // Create existing file
627        let existing_file = temp_dir.path().join("src").join("main.rs");
628        fs::create_dir_all(existing_file.parent().unwrap()).unwrap();
629        fs::write(&existing_file, "// existing").unwrap();
630
631        let boilerplate = Boilerplate {
632            id: "test-bp".to_string(),
633            name: "Test Boilerplate".to_string(),
634            description: "A test boilerplate".to_string(),
635            language: "rust".to_string(),
636            files: vec![BoilerplateFile {
637                path: "src/main.rs".to_string(),
638                template: "fn main() {}".to_string(),
639                condition: None,
640            }],
641            dependencies: vec![],
642            scripts: vec![],
643        };
644
645        let variables = HashMap::new();
646        let result = manager
647            .apply(
648                &boilerplate,
649                temp_dir.path(),
650                &variables,
651                ConflictResolution::Skip,
652            )
653            .unwrap();
654
655        assert_eq!(result.skipped_files.len(), 1);
656        assert_eq!(result.created_files.len(), 0);
657
658        // Verify existing file was not overwritten
659        let content = fs::read_to_string(&existing_file).unwrap();
660        assert_eq!(content, "// existing");
661    }
662
663    #[test]
664    fn test_apply_boilerplate_overwrite_conflicts() {
665        let temp_dir = TempDir::new().unwrap();
666        let manager = BoilerplateManager::new();
667
668        // Create existing file
669        let existing_file = temp_dir.path().join("src").join("main.rs");
670        fs::create_dir_all(existing_file.parent().unwrap()).unwrap();
671        fs::write(&existing_file, "// existing").unwrap();
672
673        let boilerplate = Boilerplate {
674            id: "test-bp".to_string(),
675            name: "Test Boilerplate".to_string(),
676            description: "A test boilerplate".to_string(),
677            language: "rust".to_string(),
678            files: vec![BoilerplateFile {
679                path: "src/main.rs".to_string(),
680                template: "fn main() {}".to_string(),
681                condition: None,
682            }],
683            dependencies: vec![],
684            scripts: vec![],
685        };
686
687        let variables = HashMap::new();
688        let result = manager
689            .apply(
690                &boilerplate,
691                temp_dir.path(),
692                &variables,
693                ConflictResolution::Overwrite,
694            )
695            .unwrap();
696
697        assert_eq!(result.created_files.len(), 1);
698        assert_eq!(result.skipped_files.len(), 0);
699
700        // Verify file was overwritten
701        let content = fs::read_to_string(&existing_file).unwrap();
702        assert_eq!(content, "fn main() {}");
703    }
704
705    #[test]
706    fn test_apply_boilerplate_with_variables() {
707        let temp_dir = TempDir::new().unwrap();
708        let manager = BoilerplateManager::new();
709
710        let boilerplate = Boilerplate {
711            id: "test-bp".to_string(),
712            name: "Test Boilerplate".to_string(),
713            description: "A test boilerplate".to_string(),
714            language: "rust".to_string(),
715            files: vec![BoilerplateFile {
716                path: "src/main.rs".to_string(),
717                template: "pub struct {{Name}} {}".to_string(),
718                condition: None,
719            }],
720            dependencies: vec![],
721            scripts: vec![],
722        };
723
724        let mut variables = HashMap::new();
725        variables.insert("Name".to_string(), "MyStruct".to_string());
726
727        let result = manager
728            .apply(
729                &boilerplate,
730                temp_dir.path(),
731                &variables,
732                ConflictResolution::Skip,
733            )
734            .unwrap();
735
736        assert_eq!(result.created_files.len(), 1);
737
738        let content = fs::read_to_string(temp_dir.path().join("src/main.rs")).unwrap();
739        assert!(content.contains("MyStruct"));
740    }
741
742    #[test]
743    fn test_apply_boilerplate_with_condition() {
744        let temp_dir = TempDir::new().unwrap();
745        let manager = BoilerplateManager::new();
746
747        let boilerplate = Boilerplate {
748            id: "test-bp".to_string(),
749            name: "Test Boilerplate".to_string(),
750            description: "A test boilerplate".to_string(),
751            language: "rust".to_string(),
752            files: vec![
753                BoilerplateFile {
754                    path: "src/main.rs".to_string(),
755                    template: "fn main() {}".to_string(),
756                    condition: Some("include_main".to_string()),
757                },
758                BoilerplateFile {
759                    path: "src/lib.rs".to_string(),
760                    template: "pub fn lib() {}".to_string(),
761                    condition: Some("include_lib".to_string()),
762                },
763            ],
764            dependencies: vec![],
765            scripts: vec![],
766        };
767
768        let mut variables = HashMap::new();
769        variables.insert("include_main".to_string(), "true".to_string());
770
771        let result = manager
772            .apply(
773                &boilerplate,
774                temp_dir.path(),
775                &variables,
776                ConflictResolution::Skip,
777            )
778            .unwrap();
779
780        assert_eq!(result.created_files.len(), 1);
781        assert!(temp_dir.path().join("src/main.rs").exists());
782        assert!(!temp_dir.path().join("src/lib.rs").exists());
783    }
784
785    #[test]
786    fn test_create_custom_boilerplate() {
787        let temp_dir = TempDir::new().unwrap();
788        let manager = BoilerplateManager::new();
789
790        // Create source files
791        let src_dir = temp_dir.path().join("src");
792        fs::create_dir_all(&src_dir).unwrap();
793        fs::write(src_dir.join("main.rs"), "fn main() {}").unwrap();
794        fs::write(src_dir.join("lib.rs"), "pub fn lib() {}").unwrap();
795
796        let boilerplate = manager
797            .create_custom(temp_dir.path(), "custom-bp", "Custom Boilerplate", "rust")
798            .unwrap();
799
800        assert_eq!(boilerplate.id, "custom-bp");
801        assert_eq!(boilerplate.name, "Custom Boilerplate");
802        assert_eq!(boilerplate.language, "rust");
803        assert_eq!(boilerplate.files.len(), 2);
804    }
805
806    #[test]
807    fn test_save_boilerplate() {
808        let temp_dir = TempDir::new().unwrap();
809        let manager = BoilerplateManager::new();
810
811        let boilerplate = Boilerplate {
812            id: "test-bp".to_string(),
813            name: "Test Boilerplate".to_string(),
814            description: "A test boilerplate".to_string(),
815            language: "rust".to_string(),
816            files: vec![BoilerplateFile {
817                path: "src/main.rs".to_string(),
818                template: "fn main() {}".to_string(),
819                condition: None,
820            }],
821            dependencies: vec![],
822            scripts: vec![],
823        };
824
825        let save_dir = temp_dir.path().join("saved-bp");
826        manager.save(&boilerplate, &save_dir).unwrap();
827
828        assert!(save_dir.join("boilerplate.yaml").exists());
829    }
830
831    #[test]
832    fn test_evaluate_condition_true() {
833        let manager = BoilerplateManager::new();
834        let mut variables = HashMap::new();
835        variables.insert("include_feature".to_string(), "true".to_string());
836
837        assert!(manager.evaluate_condition("include_feature", &variables));
838    }
839
840    #[test]
841    fn test_evaluate_condition_false() {
842        let manager = BoilerplateManager::new();
843        let mut variables = HashMap::new();
844        variables.insert("include_feature".to_string(), "false".to_string());
845
846        assert!(!manager.evaluate_condition("include_feature", &variables));
847    }
848
849    #[test]
850    fn test_evaluate_condition_negation() {
851        let manager = BoilerplateManager::new();
852        let variables = HashMap::new();
853
854        assert!(manager.evaluate_condition("!include_feature", &variables));
855    }
856}