use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::Path; use walkdir::WalkDir; #[derive(Debug, Deserialize, Serialize)] pub struct TemplateConfig { pub template: TemplateInfo, #[serde(default)] pub renames: Vec, pub variables: HashMap, pub process: ProcessConfig, } #[derive(Debug, Deserialize, Serialize)] pub struct TemplateInfo { pub name: String, pub description: String, } #[derive(Debug, Deserialize, Serialize)] pub struct RenameRule { pub from: String, pub to: String, } #[derive(Debug, Deserialize, Serialize)] pub struct VariableConfig { pub required: bool, pub description: String, } #[derive(Debug, Deserialize, Serialize)] pub struct ProcessConfig { pub file_extensions: Vec, } pub struct TemplateEngine { config: TemplateConfig, variables: HashMap, } impl TemplateEngine { pub fn load(template_dir: &Path) -> crate::error::Result { let config_path = template_dir.join("template.toml"); let content = fs::read_to_string(&config_path)?; let config: TemplateConfig = toml::from_str(&content)?; Ok(Self { config, variables: HashMap::new(), }) } pub fn set_variable(&mut self, key: String, value: String) { self.variables.insert(key, value); } pub fn validate_variables(&self) -> crate::error::Result<()> { for (key, var_config) in &self.config.variables { if var_config.required && !self.variables.contains_key(key) { return Err(crate::error::CliError::InvalidInput(format!( "missing required template variable: {} ({})", key, var_config.description ))); } } Ok(()) } pub fn process_directory(&self, dir: &Path) -> crate::error::Result<()> { self.validate_variables()?; self.replace_in_files(dir)?; self.apply_renames(dir)?; self.verify_no_placeholders(dir)?; Ok(()) } fn replace_in_files(&self, dir: &Path) -> crate::error::Result<()> { let placeholder_regex = placeholder_regex(); for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) { let path = entry.path(); if !path.is_file() || !self.should_process(path) { continue; } let content = fs::read_to_string(path)?; let mut updated = content.clone(); for cap in placeholder_regex.captures_iter(&content) { let placeholder = &cap[0]; let var_name = placeholder_name(&cap); if let Some(value) = self.variables.get(var_name) { updated = updated.replace(placeholder, value); } } if updated != content { fs::write(path, updated)?; println!(" - processed template file: {}", path.display()); } } Ok(()) } fn apply_renames(&self, dir: &Path) -> crate::error::Result<()> { for rule in &self.config.renames { let from_path = dir.join(&rule.from); if !from_path.exists() { continue; } let to_path_str = self.replace_placeholders(&rule.to); let to_path = dir.join(&to_path_str); if let Some(parent) = to_path.parent() { fs::create_dir_all(parent)?; } fs::rename(&from_path, &to_path)?; println!(" - renamed: {} -> {}", rule.from, to_path_str); } Ok(()) } fn replace_placeholders(&self, text: &str) -> String { let placeholder_regex = placeholder_regex(); let mut result = text.to_string(); for cap in placeholder_regex.captures_iter(text) { let placeholder = &cap[0]; let var_name = placeholder_name(&cap); if let Some(value) = self.variables.get(var_name) { result = result.replace(placeholder, value); } } result } fn verify_no_placeholders(&self, dir: &Path) -> crate::error::Result<()> { let placeholder_regex = placeholder_regex(); let mut found_placeholders = Vec::new(); for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) { let path = entry.path(); if !path.is_file() || !self.should_process(path) { continue; } let content = fs::read_to_string(path)?; for cap in placeholder_regex.captures_iter(&content) { let var_name = placeholder_name(&cap); if !self.config.variables.contains_key(var_name) { continue; } found_placeholders.push(format!("{}:{}", path.display(), var_name)); } } if !found_placeholders.is_empty() { return Err(crate::error::CliError::InvalidData(format!( "unresolved template placeholders: {}", found_placeholders.join(", ") ))); } Ok(()) } fn should_process(&self, path: &Path) -> bool { path.extension() .and_then(|s| s.to_str()) .map(|ext| { self.config .process .file_extensions .iter() .any(|configured| configured == ext) }) .unwrap_or(false) } } fn placeholder_regex() -> Regex { Regex::new(r"\{\{(\w+)\}\}|__(\w+)__").unwrap() } fn placeholder_name<'a>(cap: &'a regex::Captures<'a>) -> &'a str { cap.get(1) .or_else(|| cap.get(2)) .expect("placeholder capture is missing") .as_str() }