use super::manifest::{TemplateFile, TemplateManifest};
use crate::{Error, Result};
use regex::Regex;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
pub struct TemplateEngine {
variables: HashMap<String, String>,
manifest: TemplateManifest,
source_dir: PathBuf,
}
pub use super::manifest::TemplateVariable;
impl TemplateEngine {
pub fn new(manifest: TemplateManifest, source_dir: PathBuf) -> Self {
Self {
variables: HashMap::new(),
manifest,
source_dir,
}
}
pub fn set_variable(&mut self, name: String, value: String) -> Result<()> {
if let Some(var_def) = self.manifest.variables.iter().find(|v| v.name == name)
&& let Some(pattern) = &var_def.pattern
{
let regex = Regex::new(pattern)
.map_err(|e| Error::validation(format!("Invalid regex pattern: {}", e)))?;
if !regex.is_match(&value) {
return Err(Error::validation(format!(
"Value '{}' does not match pattern for {}",
value, name
)));
}
}
self.variables.insert(name.clone(), value.clone());
if name == "project_name" {
let project_ident = value.replace('-', "_");
self.variables
.insert("project_ident".to_string(), project_ident);
}
Ok(())
}
pub fn set_variables(&mut self, vars: HashMap<String, String>) -> Result<()> {
for (name, value) in vars {
self.set_variable(name, value)?;
}
Ok(())
}
pub fn generate(&self, target_dir: &Path) -> Result<()> {
self.validate_variables()?;
if target_dir.exists() {
return Err(Error::validation(format!(
"Target directory already exists: {}",
target_dir.display()
)));
}
fs::create_dir_all(target_dir)?;
for file in &self.manifest.files {
self.process_file(file, target_dir)?;
}
self.run_post_generate(target_dir)?;
Ok(())
}
fn validate_variables(&self) -> Result<()> {
for var in &self.manifest.variables {
if var.required && !self.variables.contains_key(&var.name) && var.default.is_none() {
return Err(Error::validation(format!(
"Required variable '{}' is not set",
var.name
)));
}
}
Ok(())
}
fn process_file(&self, file: &TemplateFile, target_dir: &Path) -> Result<()> {
let source_path = self.source_dir.join(&file.source);
let dest_str = self.substitute_variables(&file.destination.to_string_lossy())?;
let dest_path = target_dir.join(dest_str);
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
}
if file.process {
let content = fs::read_to_string(&source_path)?;
let processed = self.substitute_variables(&content)?;
fs::write(&dest_path, processed)?;
} else {
fs::copy(&source_path, &dest_path)?;
}
#[cfg(unix)]
if let Some(perms) = file.permissions {
use std::os::unix::fs::PermissionsExt;
let permissions = fs::Permissions::from_mode(perms);
fs::set_permissions(&dest_path, permissions)?;
}
Ok(())
}
fn substitute_variables(&self, text: &str) -> Result<String> {
let mut result = text.to_string();
let mut vars = self.variables.clone();
for var_def in &self.manifest.variables {
if !vars.contains_key(&var_def.name)
&& let Some(default) = &var_def.default
{
vars.insert(var_def.name.clone(), default.clone());
}
}
for (name, value) in vars {
let pattern = format!("{{{{{}}}}}", name);
result = result.replace(&pattern, &value);
}
let unsubstituted = Regex::new(r"\{\{[^}]+\}\}")
.map_err(|e| Error::validation(format!("Regex error: {}", e)))?;
if unsubstituted.is_match(&result)
&& let Some(m) = unsubstituted.find(&result)
{
return Err(Error::validation(format!(
"Unsubstituted variable found: {}",
m.as_str()
)));
}
Ok(result)
}
fn run_post_generate(&self, target_dir: &Path) -> Result<()> {
for command in &self.manifest.post_generate {
let processed = self.substitute_variables(command)?;
let parts: Vec<&str> = processed.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let output = std::process::Command::new(parts[0])
.args(&parts[1..])
.current_dir(target_dir)
.output()
.map_err(|e| Error::process(format!("Failed to run command: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::process(format!(
"Command failed: {}\n{}",
processed, stderr
)));
}
}
Ok(())
}
pub fn required_variables(&self) -> Vec<&TemplateVariable> {
self.manifest
.variables
.iter()
.filter(|v| v.required && v.default.is_none())
.collect()
}
pub fn optional_variables(&self) -> Vec<&TemplateVariable> {
self.manifest
.variables
.iter()
.filter(|v| !v.required || v.default.is_some())
.collect()
}
}