use crate::prelude::*;
use std::fs;
use tera::{Context as TeraContext, Tera};
use crate::state::VersionInfo;
use crate::workspace_state::WorkspaceState;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TemplateConfig {
pub name: String,
pub description: Option<String>,
pub source_path: String,
pub output_path: String,
pub enabled: bool,
}
pub struct TemplateManager {
project_root: PathBuf,
templates_dir: PathBuf,
tera_engine: Tera,
templates: HashMap<String, TemplateConfig>,
}
impl TemplateManager {
pub fn new(workspace_state: &WorkspaceState) -> Result<Self> {
let project_root = workspace_state.project_root.clone();
let templates_dir = workspace_state.workspace_dir().join("templates");
fs::create_dir_all(&templates_dir).context("Failed to create templates directory")?;
let mut tera_engine = Tera::default();
let templates = Self::load_template_configs(&templates_dir)?;
for template_config in templates.values() {
let template_path = templates_dir.join(&template_config.source_path);
if template_path.exists() {
let template_content = fs::read_to_string(&template_path).with_context(|| {
format!("Failed to read template: {}", template_path.display())
})?;
tera_engine
.add_raw_template(&template_config.name, &template_content)
.with_context(|| {
format!("Failed to register template: {}", template_config.name)
})?;
}
}
Ok(Self {
project_root,
templates_dir,
tera_engine,
templates,
})
}
pub fn add_template(
&mut self,
name: &str,
template_content: &str,
output_path: &str,
description: Option<String>,
) -> Result<()> {
let template_filename = format!("{}.tera", name);
let template_path = self.templates_dir.join(&template_filename);
fs::write(&template_path, template_content).with_context(|| {
format!("Failed to write template file: {}", template_path.display())
})?;
let template_config = TemplateConfig {
name: name.to_string(),
description,
source_path: template_filename,
output_path: output_path.to_string(),
enabled: true,
};
self.tera_engine
.add_raw_template(name, template_content)
.with_context(|| format!("Failed to register template: {}", name))?;
self.templates.insert(name.to_string(), template_config);
self.save_template_configs()?;
Ok(())
}
pub fn remove_template(&mut self, name: &str) -> Result<bool> {
if let Some(template_config) = self.templates.remove(name) {
let template_path = self.templates_dir.join(&template_config.source_path);
if template_path.exists() {
fs::remove_file(&template_path).with_context(|| {
format!(
"Failed to remove template file: {}",
template_path.display()
)
})?;
}
self.rebuild_tera_engine()?;
self.save_template_configs()?;
Ok(true)
} else {
Ok(false)
}
}
pub fn list_templates(&self) -> Vec<&TemplateConfig> {
self.templates.values().collect()
}
pub fn get_template(&self, name: &str) -> Option<&TemplateConfig> {
self.templates.get(name)
}
pub fn set_template_enabled(&mut self, name: &str, enabled: bool) -> Result<bool> {
if let Some(template_config) = self.templates.get_mut(name) {
template_config.enabled = enabled;
self.save_template_configs()?;
Ok(true)
} else {
Ok(false)
}
}
pub fn render_all_templates(
&self,
version_info: &VersionInfo,
project_name: Option<&str>,
) -> Result<Vec<String>> {
let mut rendered_files = Vec::new();
let context = self.create_template_context(version_info, project_name);
for template_config in self.templates.values() {
if template_config.enabled {
match self.render_template(template_config, &context) {
Ok(output_path) => {
rendered_files.push(output_path);
}
Err(e) => {
warn!(
"Failed to render template '{}': {}",
template_config.name, e
);
}
}
}
}
Ok(rendered_files)
}
pub fn render_template(
&self,
template_config: &TemplateConfig,
context: &TeraContext,
) -> Result<String> {
let rendered_content = self
.tera_engine
.render(&template_config.name, context)
.with_context(|| format!("Failed to render template: {}", template_config.name))?;
let output_path = if Path::new(&template_config.output_path).is_absolute() {
PathBuf::from(&template_config.output_path)
} else {
self.project_root.join(&template_config.output_path)
};
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent).with_context(|| {
format!("Failed to create output directory: {}", parent.display())
})?;
}
fs::write(&output_path, rendered_content).with_context(|| {
format!(
"Failed to write rendered template to: {}",
output_path.display()
)
})?;
Ok(output_path.display().to_string())
}
fn create_template_context(
&self,
version_info: &VersionInfo,
project_name: Option<&str>,
) -> TeraContext {
let mut context = TeraContext::new();
let mut project = HashMap::new();
project.insert("version".to_string(), version_info.full_version.clone());
project.insert(
"major_version".to_string(),
version_info.major_version.clone(),
);
project.insert(
"minor_version".to_string(),
version_info.minor_version.to_string(),
);
project.insert(
"patch_version".to_string(),
version_info.patch_version.to_string(),
);
if let Some(name) = project_name {
project.insert("name".to_string(), name.to_string());
}
context.insert("project", &project);
let now = chrono::Local::now();
let mut datetime = HashMap::new();
datetime.insert("iso".to_string(), now.to_rfc3339());
datetime.insert("date".to_string(), now.format("%Y-%m-%d").to_string());
datetime.insert("time".to_string(), now.format("%H:%M:%S").to_string());
datetime.insert("year".to_string(), now.format("%Y").to_string());
datetime.insert("month".to_string(), now.format("%m").to_string());
datetime.insert("day".to_string(), now.format("%d").to_string());
context.insert("datetime", &datetime);
context
}
fn load_template_configs(templates_dir: &Path) -> Result<HashMap<String, TemplateConfig>> {
let config_file = templates_dir.join("templates.json");
if !config_file.exists() {
return Ok(HashMap::new());
}
let content =
fs::read_to_string(&config_file).context("Failed to read template configurations")?;
let configs: HashMap<String, TemplateConfig> =
serde_json::from_str(&content).context("Failed to parse template configurations")?;
Ok(configs)
}
fn save_template_configs(&self) -> Result<()> {
let config_file = self.templates_dir.join("templates.json");
let content = serde_json::to_string_pretty(&self.templates)
.context("Failed to serialize template configurations")?;
fs::write(&config_file, content).context("Failed to write template configurations")?;
Ok(())
}
fn rebuild_tera_engine(&mut self) -> Result<()> {
self.tera_engine = Tera::default();
for template_config in self.templates.values() {
let template_path = self.templates_dir.join(&template_config.source_path);
if template_path.exists() {
let template_content = fs::read_to_string(&template_path).with_context(|| {
format!("Failed to read template: {}", template_path.display())
})?;
self.tera_engine
.add_raw_template(&template_config.name, &template_content)
.with_context(|| {
format!("Failed to register template: {}", template_config.name)
})?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_state(temp_dir: &Path) -> WorkspaceState {
WorkspaceState::initialize(temp_dir, None).unwrap()
}
#[test]
fn test_template_manager_creation() {
let temp_dir = TempDir::new().unwrap();
let state = create_test_state(temp_dir.path());
let manager = TemplateManager::new(&state).unwrap();
assert!(manager.templates_dir.exists());
assert_eq!(manager.list_templates().len(), 0);
}
#[test]
fn test_add_and_remove_template() {
let temp_dir = TempDir::new().unwrap();
let state = create_test_state(temp_dir.path());
let mut manager = TemplateManager::new(&state).unwrap();
let template_content = "# {{ project.name }} v{{ project.version }}\n\nRelease notes for version {{ project.version }}.";
let output_path = "RELEASE_NOTES.md";
manager
.add_template(
"release_notes",
template_content,
output_path,
Some("Release notes template".to_string()),
)
.unwrap();
assert_eq!(manager.list_templates().len(), 1);
let template = manager.get_template("release_notes").unwrap();
assert_eq!(template.name, "release_notes");
assert_eq!(template.output_path, output_path);
assert!(template.enabled);
let removed = manager.remove_template("release_notes").unwrap();
assert!(removed);
assert_eq!(manager.list_templates().len(), 0);
let removed = manager.remove_template("non_existent").unwrap();
assert!(!removed);
}
#[test]
fn test_enable_disable_template() {
let temp_dir = TempDir::new().unwrap();
let state = create_test_state(temp_dir.path());
let mut manager = TemplateManager::new(&state).unwrap();
let template_content = "Version: {{ project.version }}";
manager
.add_template("version", template_content, "VERSION.txt", None)
.unwrap();
let result = manager.set_template_enabled("version", false).unwrap();
assert!(result);
let template = manager.get_template("version").unwrap();
assert!(!template.enabled);
let result = manager.set_template_enabled("version", true).unwrap();
assert!(result);
let template = manager.get_template("version").unwrap();
assert!(template.enabled);
let result = manager.set_template_enabled("non_existent", true).unwrap();
assert!(!result);
}
#[test]
fn test_template_context_creation() {
let temp_dir = TempDir::new().unwrap();
let state = create_test_state(temp_dir.path());
let manager = TemplateManager::new(&state).unwrap();
let version_info = VersionInfo {
major_version: "v1.0".to_string(),
minor_version: 5,
patch_version: 100,
full_version: "1.0.5.100".to_string(),
};
let context = manager.create_template_context(&version_info, Some("test-project"));
let project = context.get("project").unwrap();
assert!(project.is_object());
let datetime = context.get("datetime").unwrap();
assert!(datetime.is_object());
}
#[test]
fn test_render_template() {
let temp_dir = TempDir::new().unwrap();
let state = create_test_state(temp_dir.path());
let mut manager = TemplateManager::new(&state).unwrap();
let template_content = "# {{ project.name | default(value='Unknown') }} v{{ project.version }}\n\nReleased on {{ datetime.date }}\n\n## Changes\n- Version bump to {{ project.version }}";
let output_path = temp_dir.path().join("CHANGELOG.md");
manager
.add_template(
"changelog",
template_content,
output_path.to_str().unwrap(),
None,
)
.unwrap();
let version_info = VersionInfo {
major_version: "v1.0".to_string(),
minor_version: 5,
patch_version: 100,
full_version: "1.0.5.100".to_string(),
};
let rendered_files = manager
.render_all_templates(&version_info, Some("test-project"))
.unwrap();
assert_eq!(rendered_files.len(), 1);
assert!(output_path.exists());
let rendered_content = fs::read_to_string(&output_path).unwrap();
assert!(rendered_content.contains("# test-project v1.0.5.100"));
assert!(rendered_content.contains("Version bump to 1.0.5.100"));
}
#[test]
fn test_template_persistence() {
let temp_dir = TempDir::new().unwrap();
let state = create_test_state(temp_dir.path());
{
let mut manager = TemplateManager::new(&state).unwrap();
manager
.add_template(
"test",
"{{ project.version }}",
"version.txt",
Some("Test template".to_string()),
)
.unwrap();
}
{
let manager = TemplateManager::new(&state).unwrap();
assert_eq!(manager.list_templates().len(), 1);
let template = manager.get_template("test").unwrap();
assert_eq!(template.description, Some("Test template".to_string()));
}
}
}