Files
emod-cli/src/template.rs

206 lines
5.8 KiB
Rust
Raw Normal View History

2026-04-26 20:48:26 +08:00
use regex::Regex;
2025-11-29 23:38:03 +08:00
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<RenameRule>,
pub variables: HashMap<String, VariableConfig>,
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<String>,
}
pub struct TemplateEngine {
config: TemplateConfig,
variables: HashMap<String, String>,
}
impl TemplateEngine {
pub fn load(template_dir: &Path) -> crate::error::Result<Self> {
let config_path = template_dir.join("template.toml");
let content = fs::read_to_string(&config_path)?;
let config: TemplateConfig = toml::from_str(&content)?;
2026-04-26 20:48:26 +08:00
2025-11-29 23:38:03 +08:00
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!(
2026-04-26 20:48:26 +08:00
"missing required template variable: {} ({})",
2025-11-29 23:38:03 +08:00
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<()> {
2026-04-26 20:48:26 +08:00
let placeholder_regex = placeholder_regex();
2025-11-29 23:38:03 +08:00
for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
2026-04-26 20:48:26 +08:00
if !path.is_file() || !self.should_process(path) {
2025-11-29 23:38:03 +08:00
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];
2026-04-26 20:48:26 +08:00
let var_name = placeholder_name(&cap);
2025-11-29 23:38:03 +08:00
if let Some(value) = self.variables.get(var_name) {
updated = updated.replace(placeholder, value);
}
}
if updated != content {
fs::write(path, updated)?;
2026-04-26 20:48:26 +08:00
println!(" - processed template file: {}", path.display());
2025-11-29 23:38:03 +08:00
}
}
Ok(())
}
fn apply_renames(&self, dir: &Path) -> crate::error::Result<()> {
2026-04-26 20:48:26 +08:00
for rule in &self.config.renames {
2025-11-29 23:38:03 +08:00
let from_path = dir.join(&rule.from);
2026-04-26 20:48:26 +08:00
2025-11-29 23:38:03 +08:00
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)?;
2026-04-26 20:48:26 +08:00
println!(" - renamed: {} -> {}", rule.from, to_path_str);
2025-11-29 23:38:03 +08:00
}
Ok(())
}
fn replace_placeholders(&self, text: &str) -> String {
2026-04-26 20:48:26 +08:00
let placeholder_regex = placeholder_regex();
2025-11-29 23:38:03 +08:00
let mut result = text.to_string();
for cap in placeholder_regex.captures_iter(text) {
let placeholder = &cap[0];
2026-04-26 20:48:26 +08:00
let var_name = placeholder_name(&cap);
2025-11-29 23:38:03 +08:00
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<()> {
2026-04-26 20:48:26 +08:00
let placeholder_regex = placeholder_regex();
2025-11-29 23:38:03 +08:00
let mut found_placeholders = Vec::new();
for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
2026-04-26 20:48:26 +08:00
if !path.is_file() || !self.should_process(path) {
2025-11-29 23:38:03 +08:00
continue;
}
let content = fs::read_to_string(path)?;
2026-04-26 20:48:26 +08:00
2025-11-29 23:38:03 +08:00
for cap in placeholder_regex.captures_iter(&content) {
2026-04-26 20:48:26 +08:00
let var_name = placeholder_name(&cap);
if !self.config.variables.contains_key(var_name) {
continue;
}
2025-11-29 23:38:03 +08:00
found_placeholders.push(format!("{}:{}", path.display(), var_name));
}
}
if !found_placeholders.is_empty() {
2026-04-26 20:48:26 +08:00
return Err(crate::error::CliError::InvalidData(format!(
"unresolved template placeholders: {}",
found_placeholders.join(", ")
)));
2025-11-29 23:38:03 +08:00
}
Ok(())
}
2026-04-26 20:48:26 +08:00
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()
}