Skip to main content

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