use super::{
storage::TemplateStorage,
types::{
Template, TemplateCategory, TemplateContext, TemplateError, TemplateQuery,
TemplateVariable, VariableType,
},
validation::AppliedTemplate,
};
use anyhow::Result;
use regex::Regex;
use std::collections::HashMap;
use tracing::{info, warn};
pub struct TemplateManager<T: TemplateStorage> {
storage: T,
regex_cache: HashMap<String, Regex>,
}
impl<T: TemplateStorage> TemplateManager<T> {
pub fn new(storage: T) -> Self {
Self {
storage,
regex_cache: HashMap::new(),
}
}
pub async fn save_template(&mut self, template: Template) -> Result<(), TemplateError> {
self.validate_template(&template)?;
self.storage.save_template(&template).await?;
info!("Saved template: {} ({})", template.name, template.id);
Ok(())
}
pub async fn load_template(&self, id: &str) -> Result<Template, TemplateError> {
self.storage.load_template(id).await
}
pub async fn delete_template(&mut self, id: &str) -> Result<(), TemplateError> {
self.storage.delete_template(id).await?;
info!("Deleted template: {}", id);
Ok(())
}
pub async fn list_templates(&self) -> Result<Vec<Template>, TemplateError> {
self.storage.list_templates().await
}
pub async fn search_templates(
&self,
query: TemplateQuery,
) -> Result<Vec<Template>, TemplateError> {
self.storage.search_templates(&query).await
}
pub async fn apply_template(
&mut self,
template_id: &str,
context: TemplateContext,
) -> Result<AppliedTemplate, TemplateError> {
let template = self.load_template(template_id).await?;
self.validate_context(&template, &context)?;
let description =
self.substitute_variables(&template.task_description, &template, &context)?;
let details = if let Some(ref details_template) = template.task_details {
Some(self.substitute_variables(details_template, &template, &context)?)
} else {
None
};
let target_files = template
.target_files
.iter()
.map(|file| self.substitute_variables(file, &template, &context))
.collect::<Result<Vec<_>, _>>()?;
self.storage.update_usage(&template.id, true).await?;
Ok(AppliedTemplate {
template: template.clone(),
task_description: description.clone(),
task_details: details.clone(),
applied_variables: context.variables.clone(),
description,
details,
priority: template.default_priority,
task_type: template.default_task_type,
estimated_duration: template.estimated_duration,
target_files,
})
}
pub async fn get_template_by_name(&self, name: &str) -> Result<Template, TemplateError> {
if let Ok(template) = self.load_template(name).await {
return Ok(template);
}
let query = TemplateQuery::new().with_search_term(name).with_limit(1);
let templates = self.search_templates(query).await?;
templates
.into_iter()
.next()
.ok_or_else(|| TemplateError::NotFound {
id: name.to_string(),
})
}
pub async fn get_templates_by_category(
&self,
category: TemplateCategory,
) -> Result<Vec<Template>, TemplateError> {
let query = TemplateQuery::new().with_category(category);
self.search_templates(query).await
}
pub async fn get_popular_templates(
&self,
limit: usize,
) -> Result<Vec<Template>, TemplateError> {
let query = TemplateQuery::new().sort_by_popularity().with_limit(limit);
self.search_templates(query).await
}
pub async fn get_recent_templates(&self, limit: usize) -> Result<Vec<Template>, TemplateError> {
let query = TemplateQuery::new().sort_by_date().with_limit(limit);
self.search_templates(query).await
}
fn validate_template(&self, template: &Template) -> Result<(), TemplateError> {
if !template.is_valid() {
return Err(TemplateError::ValidationFailed {
reason: "Template is missing required fields".to_string(),
});
}
self.validate_variable_references(&template.task_description, &template.variables)?;
if let Some(ref details) = template.task_details {
self.validate_variable_references(details, &template.variables)?;
}
for file in &template.target_files {
self.validate_variable_references(file, &template.variables)?;
}
for variable in &template.variables {
self.validate_variable_definition(variable)?;
}
Ok(())
}
fn validate_variable_references(
&self,
text: &str,
variables: &[TemplateVariable],
) -> Result<(), TemplateError> {
let var_regex = Regex::new(r"\{\{(\w+)\}\}")
.expect("Hardcoded variable regex pattern should always be valid");
let defined_vars: std::collections::HashSet<_> =
variables.iter().map(|v| &v.name).collect();
for capture in var_regex.captures_iter(text) {
let var_name = capture[1].to_string();
if !defined_vars.contains(&var_name) {
return Err(TemplateError::ValidationFailed {
reason: format!("Variable '{}' is referenced but not defined", var_name),
});
}
}
Ok(())
}
fn validate_variable_definition(
&self,
variable: &TemplateVariable,
) -> Result<(), TemplateError> {
if variable.name.is_empty()
|| !variable
.name
.chars()
.all(|c| c.is_alphanumeric() || c == '_')
{
return Err(TemplateError::ValidationFailed {
reason: format!("Invalid variable name: '{}'", variable.name),
});
}
if let VariableType::Choice(ref choices) = variable.variable_type
&& choices.is_empty()
{
return Err(TemplateError::ValidationFailed {
reason: format!("Choice variable '{}' has no options", variable.name),
});
}
if let Some(ref default) = variable.default_value {
self.validate_variable_value(&variable.name, default, &variable.variable_type)?;
}
Ok(())
}
fn validate_variable_value(
&self,
var_name: &str,
value: &str,
var_type: &VariableType,
) -> Result<(), TemplateError> {
match var_type {
VariableType::Text => {
Ok(())
}
VariableType::Boolean => {
if !["true", "false", "1", "0", "yes", "no"]
.contains(&value.to_lowercase().as_str())
{
Err(TemplateError::ValidationFailed {
reason: format!("Invalid boolean value for '{}': '{}'", var_name, value),
})
} else {
Ok(())
}
}
VariableType::Number => {
if value.parse::<f64>().is_err() {
Err(TemplateError::ValidationFailed {
reason: format!("Invalid number value for '{}': '{}'", var_name, value),
})
} else {
Ok(())
}
}
VariableType::FilePath => {
if value.contains('\0') {
Err(TemplateError::ValidationFailed {
reason: format!("Invalid file path for '{}': contains null byte", var_name),
})
} else {
Ok(())
}
}
VariableType::Url => {
if value.contains("://") {
Ok(())
} else {
Err(TemplateError::ValidationFailed {
reason: format!("Invalid URL for '{}': '{}'", var_name, value),
})
}
}
VariableType::List => {
Ok(())
}
VariableType::Choice(choices) => {
if !choices.contains(&value.to_string()) {
Err(TemplateError::ValidationFailed {
reason: format!(
"Invalid choice for '{}': '{}'. Must be one of: {}",
var_name,
value,
choices.join(", ")
),
})
} else {
Ok(())
}
}
}
}
fn validate_context(
&self,
template: &Template,
context: &TemplateContext,
) -> Result<(), TemplateError> {
for variable in &template.variables {
if variable.required {
let value = context.variables.get(&variable.name);
if value.is_none() && variable.default_value.is_none() {
return Err(TemplateError::ValidationFailed {
reason: format!("Required variable '{}' not provided", variable.name),
});
}
}
if let Some(value) = context.variables.get(&variable.name) {
self.validate_variable_value(&variable.name, value, &variable.variable_type)?;
}
}
Ok(())
}
fn substitute_variables(
&mut self,
text: &str,
template: &Template,
context: &TemplateContext,
) -> Result<String, TemplateError> {
let var_regex = if let Some(regex) = self.regex_cache.get("variable") {
regex.clone()
} else {
let regex = Regex::new(r"\{\{(\w+)\}\}")
.expect("Hardcoded variable regex pattern should always be valid");
self.regex_cache
.insert("variable".to_string(), regex.clone());
regex
};
let mut result = text.to_string();
for capture in var_regex.captures_iter(text) {
let var_name = capture[1].to_string();
let placeholder = &capture[0];
let variable = template
.variables
.iter()
.find(|v| v.name == var_name)
.ok_or_else(|| TemplateError::SubstitutionFailed {
reason: format!("Missing variable: {}", var_name),
})?;
let value = context
.variables
.get(&var_name)
.or(variable.default_value.as_ref())
.ok_or_else(|| TemplateError::SubstitutionFailed {
reason: format!("Missing variable: {}", var_name),
})?;
let transformed_value =
self.transform_variable_value(value, &variable.variable_type)?;
result = result.replace(placeholder, &transformed_value);
}
result = self.substitute_builtin_variables(&result, context)?;
Ok(result)
}
fn transform_variable_value(
&self,
value: &str,
var_type: &VariableType,
) -> Result<String, TemplateError> {
match var_type {
VariableType::Boolean => {
let normalized = match value.to_lowercase().as_str() {
"true" | "1" | "yes" => "true",
"false" | "0" | "no" => "false",
_ => value,
};
Ok(normalized.to_string())
}
VariableType::List => {
let items: Vec<&str> = value.split(',').map(|s| s.trim()).collect();
Ok(format!("[{}]", items.join(", ")))
}
_ => Ok(value.to_string()),
}
}
fn substitute_builtin_variables(
&self,
text: &str,
context: &TemplateContext,
) -> Result<String, TemplateError> {
let mut result = text.to_string();
if let Some(ref project_name) = context.project_name {
result = result.replace("{{project_name}}", project_name);
}
if let Some(ref agent_role) = context.agent_role {
result = result.replace("{{agent_role}}", agent_role);
}
let now = chrono::Utc::now();
result = result.replace("{{current_date}}", &now.format("%Y-%m-%d").to_string());
result = result.replace("{{current_time}}", &now.format("%H:%M:%S").to_string());
result = result.replace(
"{{current_datetime}}",
&now.format("%Y-%m-%d %H:%M:%S").to_string(),
);
Ok(result)
}
pub async fn get_template_stats(&self) -> Result<super::storage::TemplateStats, TemplateError> {
self.storage.get_stats().await
}
pub async fn update_template(&mut self, template: Template) -> Result<(), TemplateError> {
self.validate_template(&template)?;
if self.storage.exists(&template.id).await? {
self.storage.delete_template(&template.id).await?;
}
self.storage.save_template(&template).await?;
info!("Updated template: {} ({})", template.name, template.id);
Ok(())
}
pub async fn import_templates(
&mut self,
templates: Vec<Template>,
) -> Result<Vec<String>, TemplateError> {
let mut imported = Vec::new();
let mut errors = Vec::new();
for template in templates {
match self.save_template(template.clone()).await {
Ok(()) => {
imported.push(template.id.clone());
info!("Imported template: {}", template.id);
}
Err(e) => {
warn!("Failed to import template {}: {}", template.id, e);
errors.push(format!("{}: {}", template.id, e));
}
}
}
if !errors.is_empty() {
warn!("Failed to import {} templates: {:?}", errors.len(), errors);
}
Ok(imported)
}
pub async fn export_templates(&self) -> Result<Vec<Template>, TemplateError> {
self.list_templates().await
}
pub async fn clone_template(
&mut self,
source_id: &str,
new_id: &str,
new_name: Option<String>,
) -> Result<Template, TemplateError> {
let mut template = self.load_template(source_id).await?;
template.id = new_id.to_string();
if let Some(name) = new_name {
template.name = name;
} else {
template.name = format!("{} (Copy)", template.name);
}
template.usage_count = 0;
template.success_rate = None;
template.created_at = chrono::Utc::now();
template.updated_at = chrono::Utc::now();
self.save_template(template.clone()).await?;
Ok(template)
}
}