use std::path::PathBuf;
#[derive(Debug, Clone, thiserror::Error)]
pub enum TemplateError {
#[error("Template '{name}' not found")]
TemplateNotFound { name: String },
#[error("Failed to read template '{name}': {reason}")]
ReadError { name: String, reason: String },
#[error("Missing required variable: {0}")]
MissingVariable(String),
#[error("Circular reference in templates: {0}")]
CircularReference(String),
#[error("Partial template not found: {0}")]
PartialNotFound(String),
}
#[derive(Debug, Clone)]
pub struct TemplateRegistry {
user_templates_dir: Option<PathBuf>,
}
impl TemplateRegistry {
#[must_use]
pub const fn new(user_templates_dir: Option<PathBuf>) -> Self {
Self { user_templates_dir }
}
#[must_use]
pub fn default_user_templates_dir() -> Option<PathBuf> {
if let Some(xdg) = get_xdg_config_home() {
let xdg_str = xdg.to_string_lossy().trim().to_string();
if !xdg_str.is_empty() {
return Some(PathBuf::from(xdg_str).join("ralph").join("templates"));
}
}
dirs::home_dir().map(|d| d.join(".config").join("ralph").join("templates"))
}
#[must_use]
pub fn has_user_template(&self, name: &str) -> bool {
self.user_templates_dir
.as_ref()
.is_some_and(|user_dir| template_exists(&user_dir.join(format!("{name}.txt"))))
}
#[must_use]
pub fn template_source(&self, name: &str) -> &'static str {
if self.has_user_template(name) {
"user"
} else {
"embedded"
}
}
pub fn get_template(&self, name: &str) -> Result<String, TemplateError> {
use crate::prompts::template_catalog;
if let Some(user_dir) = &self.user_templates_dir {
let user_path = user_dir.join(format!("{name}.txt"));
if template_exists(&user_path) {
return load_template(&user_path).map_err(|e| TemplateError::ReadError {
name: name.to_string(),
reason: e.to_string(),
});
}
}
if let Some(content) = template_catalog::get_embedded_template(name) {
return Ok(content);
}
Err(TemplateError::TemplateNotFound {
name: name.to_string(),
})
}
#[must_use]
#[cfg(test)]
pub fn all_template_names() -> Vec<String> {
use crate::prompts::template_catalog;
template_catalog::list_all_templates()
.iter()
.map(|t| t.name.to_string())
.collect()
}
#[must_use]
#[cfg(test)]
pub fn template_exists(&self, name: &str) -> bool {
self.has_user_template(name) || self.get_template(name).is_ok()
}
}
include!("template_registry/io.rs");
impl Default for TemplateRegistry {
fn default() -> Self {
Self::new(Self::default_user_templates_dir())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_registry_creation() {
let registry = TemplateRegistry::new(None);
assert!(registry.user_templates_dir.is_none());
let custom_dir = PathBuf::from("/custom/templates");
let registry = TemplateRegistry::new(Some(custom_dir.clone()));
assert_eq!(registry.user_templates_dir, Some(custom_dir));
}
#[test]
fn test_default_user_templates_dir() {
let dir = TemplateRegistry::default_user_templates_dir();
assert!(dir.is_some());
let path = dir.unwrap();
assert!(path.to_string_lossy().contains("templates"));
}
#[test]
fn test_has_user_template_no_dir() {
let registry = TemplateRegistry::new(None);
assert!(!registry.has_user_template("commit_message_xml"));
}
#[test]
fn test_template_source_no_dir() {
let registry = TemplateRegistry::new(None);
let source = registry.template_source("commit_message_xml");
assert_eq!(source, "embedded");
}
#[test]
fn test_template_source_not_found() {
let registry = TemplateRegistry::new(None);
let source = registry.template_source("nonexistent_template");
assert_eq!(source, "embedded");
}
#[test]
fn test_default_registry() {
let registry = TemplateRegistry::default();
if TemplateRegistry::default_user_templates_dir().is_some() {
assert!(registry.user_templates_dir.is_some());
}
}
#[test]
fn test_get_template_embedded() {
let registry = TemplateRegistry::new(None);
let result = registry.get_template("developer_iteration_xml");
assert!(result.is_ok());
let content = result.unwrap();
assert!(!content.is_empty());
assert!(content.contains("IMPLEMENTATION MODE") || content.contains("Developer"));
}
#[test]
fn test_get_template_not_found() {
let registry = TemplateRegistry::new(None);
let result = registry.get_template("nonexistent_template");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
TemplateError::TemplateNotFound { .. }
));
}
#[test]
fn test_all_template_names() {
let names = TemplateRegistry::all_template_names();
assert!(!names.is_empty());
assert!(names.len() >= 10); assert!(names.contains(&"developer_iteration_xml".to_string()));
assert!(names.contains(&"commit_message_xml".to_string()));
}
#[test]
fn test_template_exists_embedded() {
let registry = TemplateRegistry::new(None);
assert!(registry.template_exists("developer_iteration_xml"));
assert!(registry.template_exists("commit_message_xml"));
}
#[test]
fn test_template_not_exists() {
let registry = TemplateRegistry::new(None);
assert!(!registry.template_exists("nonexistent_template"));
}
#[test]
fn test_get_commit_template() {
let registry = TemplateRegistry::new(None);
let result = registry.get_template("commit_message_xml");
assert!(result.is_ok());
let content = result.unwrap();
assert!(!content.is_empty());
}
#[test]
fn test_get_review_xml_template() {
let registry = TemplateRegistry::new(None);
let result = registry.get_template("review_xml");
assert!(result.is_ok());
let content = result.unwrap();
assert!(!content.is_empty());
assert!(content.contains("REVIEW MODE"));
}
#[test]
fn test_get_fix_mode_template() {
let registry = TemplateRegistry::new(None);
let result = registry.get_template("fix_mode_xml");
assert!(result.is_ok());
let content = result.unwrap();
assert!(!content.is_empty());
}
#[test]
fn test_all_templates_have_content() {
let registry = TemplateRegistry::new(None);
TemplateRegistry::all_template_names()
.into_iter()
.for_each(|name| {
let result = registry.get_template(&name);
assert!(result.is_ok(), "Template '{name}' should load successfully");
let content = result.unwrap();
assert!(!content.is_empty(), "Template '{name}' should not be empty");
});
}
#[test]
fn load_template_failure_yields_typed_error() {
let path = Path::new("/nonexistent-template-file");
let result = load_template(path);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, LoadTemplateError::Io { .. }));
assert!(err.to_string().contains("No such file"));
}
}