ferrous_forge/templates/
engine.rs

1//! Template engine for processing and generating projects
2
3use super::manifest::{TemplateFile, TemplateManifest};
4use crate::{Error, Result};
5use regex::Regex;
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10/// Template engine for processing templates
11pub struct TemplateEngine {
12    /// Variables for substitution
13    variables: HashMap<String, String>,
14
15    /// Template manifest
16    manifest: TemplateManifest,
17
18    /// Source directory for template files
19    source_dir: PathBuf,
20}
21
22/// Variable for template substitution
23pub use super::manifest::TemplateVariable;
24
25impl TemplateEngine {
26    /// Create a new template engine
27    pub fn new(manifest: TemplateManifest, source_dir: PathBuf) -> Self {
28        Self {
29            variables: HashMap::new(),
30            manifest,
31            source_dir,
32        }
33    }
34
35    /// Set a variable value
36    pub fn set_variable(&mut self, name: String, value: String) -> Result<()> {
37        // Validate against pattern if specified
38        if let Some(var_def) = self.manifest.variables.iter().find(|v| v.name == name) {
39            if let Some(pattern) = &var_def.pattern {
40                let regex = Regex::new(pattern)
41                    .map_err(|e| Error::validation(format!("Invalid regex pattern: {}", e)))?;
42                if !regex.is_match(&value) {
43                    return Err(Error::validation(format!(
44                        "Value '{}' does not match pattern for {}",
45                        value, name
46                    )));
47                }
48            }
49        }
50
51        self.variables.insert(name.clone(), value.clone());
52
53        // Auto-generate derived variables
54        if name == "project_name" {
55            // Convert hyphenated names to valid Rust identifiers
56            let project_ident = value.replace('-', "_");
57            self.variables
58                .insert("project_ident".to_string(), project_ident);
59        }
60
61        Ok(())
62    }
63
64    /// Set multiple variables at once
65    pub fn set_variables(&mut self, vars: HashMap<String, String>) -> Result<()> {
66        for (name, value) in vars {
67            self.set_variable(name, value)?;
68        }
69        Ok(())
70    }
71
72    /// Generate project from template
73    pub fn generate(&self, target_dir: &Path) -> Result<()> {
74        // Validate all required variables are set
75        self.validate_variables()?;
76
77        // Create target directory
78        if target_dir.exists() {
79            return Err(Error::validation(format!(
80                "Target directory already exists: {}",
81                target_dir.display()
82            )));
83        }
84        fs::create_dir_all(target_dir)?;
85
86        // Process each file
87        for file in &self.manifest.files {
88            self.process_file(file, target_dir)?;
89        }
90
91        // Run post-generation commands
92        self.run_post_generate(target_dir)?;
93
94        Ok(())
95    }
96
97    /// Validate all required variables are set
98    fn validate_variables(&self) -> Result<()> {
99        for var in &self.manifest.variables {
100            if var.required && !self.variables.contains_key(&var.name) && var.default.is_none() {
101                return Err(Error::validation(format!(
102                    "Required variable '{}' is not set",
103                    var.name
104                )));
105            }
106        }
107        Ok(())
108    }
109
110    /// Process a single template file
111    fn process_file(&self, file: &TemplateFile, target_dir: &Path) -> Result<()> {
112        let source_path = self.source_dir.join(&file.source);
113
114        // Substitute variables in destination path
115        let dest_str = self.substitute_variables(&file.destination.to_string_lossy())?;
116        let dest_path = target_dir.join(dest_str);
117
118        // Create parent directories
119        if let Some(parent) = dest_path.parent() {
120            fs::create_dir_all(parent)?;
121        }
122
123        if file.process {
124            // Read and process content
125            let content = fs::read_to_string(&source_path)?;
126            let processed = self.substitute_variables(&content)?;
127            fs::write(&dest_path, processed)?;
128        } else {
129            // Copy file as-is
130            fs::copy(&source_path, &dest_path)?;
131        }
132
133        // Set permissions if specified (Unix only)
134        #[cfg(unix)]
135        if let Some(perms) = file.permissions {
136            use std::os::unix::fs::PermissionsExt;
137            let permissions = fs::Permissions::from_mode(perms);
138            fs::set_permissions(&dest_path, permissions)?;
139        }
140
141        Ok(())
142    }
143
144    /// Substitute variables in text
145    fn substitute_variables(&self, text: &str) -> Result<String> {
146        let mut result = text.to_string();
147
148        // Build a complete variable map with defaults
149        let mut vars = self.variables.clone();
150        for var_def in &self.manifest.variables {
151            if !vars.contains_key(&var_def.name) {
152                if let Some(default) = &var_def.default {
153                    vars.insert(var_def.name.clone(), default.clone());
154                }
155            }
156        }
157
158        // Replace {{variable_name}} with values
159        for (name, value) in vars {
160            let pattern = format!("{{{{{}}}}}", name);
161            result = result.replace(&pattern, &value);
162        }
163
164        // Check for unsubstituted variables
165        let unsubstituted = Regex::new(r"\{\{[^}]+\}\}")
166            .map_err(|e| Error::validation(format!("Regex error: {}", e)))?;
167        if unsubstituted.is_match(&result) {
168            if let Some(m) = unsubstituted.find(&result) {
169                return Err(Error::validation(format!(
170                    "Unsubstituted variable found: {}",
171                    m.as_str()
172                )));
173            }
174        }
175
176        Ok(result)
177    }
178
179    /// Run post-generation commands
180    fn run_post_generate(&self, target_dir: &Path) -> Result<()> {
181        for command in &self.manifest.post_generate {
182            let processed = self.substitute_variables(command)?;
183
184            // Parse command
185            let parts: Vec<&str> = processed.split_whitespace().collect();
186            if parts.is_empty() {
187                continue;
188            }
189
190            // Execute command
191            let output = std::process::Command::new(parts[0])
192                .args(&parts[1..])
193                .current_dir(target_dir)
194                .output()
195                .map_err(|e| Error::process(format!("Failed to run command: {}", e)))?;
196
197            if !output.status.success() {
198                let stderr = String::from_utf8_lossy(&output.stderr);
199                return Err(Error::process(format!(
200                    "Command failed: {}\n{}",
201                    processed, stderr
202                )));
203            }
204        }
205
206        Ok(())
207    }
208
209    /// Get list of required variables
210    pub fn required_variables(&self) -> Vec<&TemplateVariable> {
211        self.manifest
212            .variables
213            .iter()
214            .filter(|v| v.required && v.default.is_none())
215            .collect()
216    }
217
218    /// Get list of optional variables
219    pub fn optional_variables(&self) -> Vec<&TemplateVariable> {
220        self.manifest
221            .variables
222            .iter()
223            .filter(|v| !v.required || v.default.is_some())
224            .collect()
225    }
226}