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<()> {
37 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 if name == "project_name" {
55 let project_ident = value.replace('-', "_");
57 self.variables
58 .insert("project_ident".to_string(), project_ident);
59 }
60
61 Ok(())
62 }
63
64 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 pub fn generate(&self, target_dir: &Path) -> Result<()> {
74 self.validate_variables()?;
76
77 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 for file in &self.manifest.files {
88 self.process_file(file, target_dir)?;
89 }
90
91 self.run_post_generate(target_dir)?;
93
94 Ok(())
95 }
96
97 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 fn process_file(&self, file: &TemplateFile, target_dir: &Path) -> Result<()> {
112 let source_path = self.source_dir.join(&file.source);
113
114 let dest_str = self.substitute_variables(&file.destination.to_string_lossy())?;
116 let dest_path = target_dir.join(dest_str);
117
118 if let Some(parent) = dest_path.parent() {
120 fs::create_dir_all(parent)?;
121 }
122
123 if file.process {
124 let content = fs::read_to_string(&source_path)?;
126 let processed = self.substitute_variables(&content)?;
127 fs::write(&dest_path, processed)?;
128 } else {
129 fs::copy(&source_path, &dest_path)?;
131 }
132
133 #[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 fn substitute_variables(&self, text: &str) -> Result<String> {
146 let mut result = text.to_string();
147
148 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 for (name, value) in vars {
160 let pattern = format!("{{{{{}}}}}", name);
161 result = result.replace(&pattern, &value);
162 }
163
164 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 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 let parts: Vec<&str> = processed.split_whitespace().collect();
186 if parts.is_empty() {
187 continue;
188 }
189
190 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 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 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}