use crate::packs::types::{Pack, PackTemplate};
use ggen_utils::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use tera::{Context, Tera};
use tracing::{debug, info, warn};
pub struct TemplateGenerator {
#[allow(dead_code)]
tera: Tera,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerationReport {
pub files_created: Vec<PathBuf>,
pub total_size: u64,
pub variables_used: HashMap<String, String>,
pub duration: Duration,
pub hooks_executed: Vec<String>,
pub success: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariableDefinition {
pub name: String,
pub var_type: VariableType,
pub description: String,
pub default: Option<String>,
pub required: bool,
pub pattern: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VariableType {
String,
Integer,
Boolean,
Path,
Email,
Url,
}
impl TemplateGenerator {
pub fn new() -> Result<Self> {
let tera = Tera::default();
Ok(Self { tera })
}
pub fn list_templates(&self, pack: &Pack) -> Result<Vec<PackTemplate>> {
Ok(pack.templates.clone())
}
pub fn generate_from_template(
&mut self, template: &PackTemplate, variables: HashMap<String, String>, target_dir: &Path,
) -> Result<GenerationReport> {
let start = Instant::now();
info!(
"Generating from template '{}' to {}",
template.name,
target_dir.display()
);
self.validate_variables(template, &variables)?;
std::fs::create_dir_all(target_dir)?;
let context = self.build_context(&variables)?;
let files_created = self.generate_files(template, &context, target_dir)?;
let total_size = files_created
.iter()
.filter_map(|path| std::fs::metadata(path).ok())
.map(|meta| meta.len())
.sum();
let hooks_executed = self.execute_post_hooks(template, target_dir, &variables)?;
let duration = start.elapsed();
info!(
"Generated {} files ({} bytes) in {:?}",
files_created.len(),
total_size,
duration
);
Ok(GenerationReport {
files_created,
total_size,
variables_used: variables,
duration,
hooks_executed,
success: true,
})
}
pub fn validate_variables(
&self, template: &PackTemplate, vars: &HashMap<String, String>,
) -> Result<()> {
let var_defs = self.extract_variable_definitions(template);
for var_def in &var_defs {
if var_def.required && !vars.contains_key(&var_def.name) {
return Err(Error::new(&format!(
"Required variable '{}' not provided for template '{}'",
var_def.name, template.name
)));
}
if let Some(value) = vars.get(&var_def.name) {
self.validate_variable_value(&var_def, value)?;
}
}
Ok(())
}
fn extract_variable_definitions(&self, template: &PackTemplate) -> Vec<VariableDefinition> {
template
.variables
.iter()
.map(|name| VariableDefinition {
name: name.clone(),
var_type: VariableType::String,
description: format!("Variable: {}", name),
default: None,
required: true,
pattern: None,
})
.collect()
}
fn validate_variable_value(&self, var_def: &VariableDefinition, value: &str) -> Result<()> {
match var_def.var_type {
VariableType::Integer => {
value.parse::<i64>().map_err(|_| {
Error::new(&format!(
"Variable '{}' must be an integer, got '{}'",
var_def.name, value
))
})?;
}
VariableType::Boolean => {
if value != "true" && value != "false" {
return Err(Error::new(&format!(
"Variable '{}' must be 'true' or 'false', got '{}'",
var_def.name, value
)));
}
}
VariableType::Email => {
if !value.contains('@') {
return Err(Error::new(&format!(
"Variable '{}' must be a valid email, got '{}'",
var_def.name, value
)));
}
}
VariableType::Url => {
if !value.starts_with("http://") && !value.starts_with("https://") {
return Err(Error::new(&format!(
"Variable '{}' must be a valid URL, got '{}'",
var_def.name, value
)));
}
}
VariableType::Path => {
if value.is_empty() {
return Err(Error::new(&format!(
"Variable '{}' cannot be empty",
var_def.name
)));
}
}
VariableType::String => {
}
}
if let Some(pattern) = &var_def.pattern {
let re = regex::Regex::new(pattern)
.map_err(|e| Error::new(&format!("Invalid regex pattern '{}': {}", pattern, e)))?;
if !re.is_match(value) {
return Err(Error::new(&format!(
"Variable '{}' value '{}' does not match pattern '{}'",
var_def.name, value, pattern
)));
}
}
Ok(())
}
fn build_context(&self, variables: &HashMap<String, String>) -> Result<Context> {
let mut context = Context::new();
for (key, value) in variables {
context.insert(key, value);
}
context.insert("timestamp", &chrono::Utc::now().to_rfc3339());
context.insert("uuid", &uuid::Uuid::new_v4().to_string());
Ok(context)
}
fn generate_files(
&mut self, template: &PackTemplate, _context: &Context, target_dir: &Path,
) -> Result<Vec<PathBuf>> {
let mut files_created = Vec::new();
debug!("Generating from template at path: {}", template.path);
let output_file = target_dir.join(format!("{}.generated", template.name));
let content = format!(
"# Generated from template: {}\n# Description: {}\n\n",
template.name, template.description
);
ggen_utils::fmea_track!(
"file_io_write_fail",
&format!("write_{}", output_file.display()),
{
std::fs::write(&output_file, &content)
.map_err(|e| ggen_utils::error::Error::io_error(&e.to_string()))
}
)?;
files_created.push(output_file);
info!("Created {} files", files_created.len());
Ok(files_created)
}
fn execute_post_hooks(
&self, template: &PackTemplate, target_dir: &Path, variables: &HashMap<String, String>,
) -> Result<Vec<String>> {
let mut hooks_executed = Vec::new();
let hooks = self.determine_hooks(template, variables);
for hook in &hooks {
match hook.as_str() {
"npm_install" => {
if self.should_run_npm_install(target_dir) {
info!("Running npm install in {}", target_dir.display());
hooks_executed.push("npm_install".to_string());
}
}
"cargo_check" => {
if target_dir.join("Cargo.toml").exists() {
info!("Running cargo check in {}", target_dir.display());
hooks_executed.push("cargo_check".to_string());
}
}
"git_init" => {
if !target_dir.join(".git").exists() {
info!("Initializing git repository in {}", target_dir.display());
hooks_executed.push("git_init".to_string());
}
}
_ => {
warn!("Unknown hook: {}", hook);
}
}
}
Ok(hooks_executed)
}
fn determine_hooks(
&self, template: &PackTemplate, _variables: &HashMap<String, String>,
) -> Vec<String> {
let mut hooks = Vec::new();
if template.name.contains("node") || template.name.contains("javascript") {
hooks.push("npm_install".to_string());
}
if template.name.contains("rust") || template.name.contains("cargo") {
hooks.push("cargo_check".to_string());
}
hooks.push("git_init".to_string());
hooks
}
fn should_run_npm_install(&self, target_dir: &Path) -> bool {
target_dir.join("package.json").exists()
}
pub fn prompt_variables(&self, template: &PackTemplate) -> Result<HashMap<String, String>> {
let var_defs = self.extract_variable_definitions(template);
let mut variables = HashMap::new();
info!(
"Template '{}' requires {} variables",
template.name,
var_defs.len()
);
for var_def in &var_defs {
let _prompt = if let Some(default) = &var_def.default {
format!("{} [{}]: ", var_def.description, default)
} else {
format!("{}: ", var_def.description)
};
let value = var_def
.default
.clone()
.unwrap_or_else(|| format!("value_for_{}", var_def.name));
variables.insert(var_def.name.clone(), value);
}
Ok(variables)
}
}
impl Default for TemplateGenerator {
fn default() -> Self {
Self {
tera: tera::Tera::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_template() -> PackTemplate {
PackTemplate {
name: "test-template".to_string(),
path: "templates/test".to_string(),
description: "A test template".to_string(),
variables: vec!["project_name".to_string(), "author".to_string()],
}
}
#[test]
fn test_template_generator_creation() {
let generator = TemplateGenerator::new();
assert!(generator.is_ok());
}
#[test]
fn test_validate_variables_success() {
let generator = TemplateGenerator::new().unwrap();
let template = create_test_template();
let mut variables = HashMap::new();
variables.insert("project_name".to_string(), "my-project".to_string());
variables.insert("author".to_string(), "Test Author".to_string());
let result = generator.validate_variables(&template, &variables);
assert!(result.is_ok());
}
#[test]
fn test_validate_variables_missing_required() {
let generator = TemplateGenerator::new().unwrap();
let template = create_test_template();
let mut variables = HashMap::new();
variables.insert("project_name".to_string(), "my-project".to_string());
let result = generator.validate_variables(&template, &variables);
assert!(result.is_err());
}
#[test]
fn test_validate_variable_value_integer() {
let generator = TemplateGenerator::new().unwrap();
let var_def = VariableDefinition {
name: "port".to_string(),
var_type: VariableType::Integer,
description: "Port number".to_string(),
default: None,
required: true,
pattern: None,
};
assert!(generator.validate_variable_value(&var_def, "8080").is_ok());
assert!(generator
.validate_variable_value(&var_def, "not-a-number")
.is_err());
}
#[test]
fn test_validate_variable_value_boolean() {
let generator = TemplateGenerator::new().unwrap();
let var_def = VariableDefinition {
name: "enabled".to_string(),
var_type: VariableType::Boolean,
description: "Enabled flag".to_string(),
default: None,
required: true,
pattern: None,
};
assert!(generator.validate_variable_value(&var_def, "true").is_ok());
assert!(generator.validate_variable_value(&var_def, "false").is_ok());
assert!(generator.validate_variable_value(&var_def, "yes").is_err());
}
#[test]
fn test_validate_variable_value_email() {
let generator = TemplateGenerator::new().unwrap();
let var_def = VariableDefinition {
name: "email".to_string(),
var_type: VariableType::Email,
description: "Email address".to_string(),
default: None,
required: true,
pattern: None,
};
assert!(generator
.validate_variable_value(&var_def, "test@example.com")
.is_ok());
assert!(generator
.validate_variable_value(&var_def, "invalid-email")
.is_err());
}
#[test]
fn test_generate_from_template() {
let mut generator = TemplateGenerator::new().unwrap();
let template = create_test_template();
let mut variables = HashMap::new();
variables.insert("project_name".to_string(), "test-project".to_string());
variables.insert("author".to_string(), "Test Author".to_string());
let temp_dir = tempfile::tempdir().unwrap();
let result = generator.generate_from_template(&template, variables, temp_dir.path());
assert!(result.is_ok());
let report = result.unwrap();
assert!(report.success);
assert!(!report.files_created.is_empty());
}
#[test]
fn test_determine_hooks() {
let generator = TemplateGenerator::new().unwrap();
let node_template = PackTemplate {
name: "node-app".to_string(),
path: "templates/node".to_string(),
description: "Node.js app".to_string(),
variables: vec![],
};
let hooks = generator.determine_hooks(&node_template, &HashMap::new());
assert!(hooks.contains(&"npm_install".to_string()));
let rust_template = PackTemplate {
name: "rust-lib".to_string(),
path: "templates/rust".to_string(),
description: "Rust library".to_string(),
variables: vec![],
};
let hooks = generator.determine_hooks(&rust_template, &HashMap::new());
assert!(hooks.contains(&"cargo_check".to_string()));
}
}