ferrous_forge/templates/
engine.rs1use super::manifest::{TemplateFile, TemplateManifest};
4use crate::{Error, Result};
5use regex::Regex;
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10pub struct TemplateEngine {
12 variables: HashMap<String, String>,
14
15 manifest: TemplateManifest,
17
18 source_dir: PathBuf,
20}
21
22pub use super::manifest::TemplateVariable;
24
25impl TemplateEngine {
26 pub fn new(manifest: TemplateManifest, source_dir: PathBuf) -> Self {
28 Self {
29 variables: HashMap::new(),
30 manifest,
31 source_dir,
32 }
33 }
34
35 pub fn set_variable(&mut self, name: String, value: String) -> Result<()> {
41 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 if name == "project_name" {
59 let project_ident = value.replace('-', "_");
61 self.variables
62 .insert("project_ident".to_string(), project_ident);
63 }
64
65 Ok(())
66 }
67
68 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 pub fn generate(&self, target_dir: &Path) -> Result<()> {
87 self.validate_variables()?;
89
90 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 for file in &self.manifest.files {
101 self.process_file(file, target_dir)?;
102 }
103
104 self.run_post_generate(target_dir)?;
106
107 Ok(())
108 }
109
110 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 fn process_file(&self, file: &TemplateFile, target_dir: &Path) -> Result<()> {
125 let source_path = self.source_dir.join(&file.source);
126
127 let dest_str = self.substitute_variables(&file.destination.to_string_lossy())?;
129 let dest_path = target_dir.join(dest_str);
130
131 if let Some(parent) = dest_path.parent() {
133 fs::create_dir_all(parent)?;
134 }
135
136 if file.process {
137 let content = fs::read_to_string(&source_path)?;
139 let processed = self.substitute_variables(&content)?;
140 fs::write(&dest_path, processed)?;
141 } else {
142 fs::copy(&source_path, &dest_path)?;
144 }
145
146 #[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 fn substitute_variables(&self, text: &str) -> Result<String> {
159 let mut result = text.to_string();
160
161 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 for (name, value) in vars {
173 let pattern = format!("{{{{{}}}}}", name);
174 result = result.replace(&pattern, &value);
175 }
176
177 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 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 let parts: Vec<&str> = processed.split_whitespace().collect();
199 if parts.is_empty() {
200 continue;
201 }
202
203 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 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 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}