ralph_workflow/prompts/
template_registry.rs1use std::fs;
15use std::path::PathBuf;
16
17#[derive(Debug, Clone, thiserror::Error)]
19pub enum TemplateError {
20 #[error("Template '{name}' not found")]
22 TemplateNotFound { name: String },
23
24 #[error("Failed to read template '{name}': {reason}")]
26 ReadError { name: String, reason: String },
27}
28
29#[derive(Debug, Clone)]
34pub struct TemplateRegistry {
35 user_templates_dir: Option<PathBuf>,
37}
38
39impl TemplateRegistry {
40 #[must_use]
47 pub const fn new(user_templates_dir: Option<PathBuf>) -> Self {
48 Self { user_templates_dir }
49 }
50
51 #[must_use]
60 pub fn default_user_templates_dir() -> Option<PathBuf> {
61 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
62 let xdg = xdg.trim();
63 if !xdg.is_empty() {
64 return Some(PathBuf::from(xdg).join("ralph").join("templates"));
65 }
66 }
67
68 dirs::home_dir().map(|d| d.join(".config").join("ralph").join("templates"))
69 }
70
71 #[must_use]
77 pub fn has_user_template(&self, name: &str) -> bool {
78 self.user_templates_dir
79 .as_ref()
80 .is_some_and(|user_dir| user_dir.join(format!("{name}.txt")).exists())
81 }
82
83 #[must_use]
90 pub fn template_source(&self, name: &str) -> &'static str {
91 if self.has_user_template(name) {
92 "user"
93 } else {
94 "embedded"
95 }
96 }
97
98 pub fn get_template(&self, name: &str) -> Result<String, TemplateError> {
114 use crate::prompts::template_catalog;
115
116 if let Some(user_dir) = &self.user_templates_dir {
118 let user_path = user_dir.join(format!("{name}.txt"));
119 if user_path.exists() {
120 return fs::read_to_string(&user_path).map_err(|e| TemplateError::ReadError {
121 name: name.to_string(),
122 reason: e.to_string(),
123 });
124 }
125 }
126
127 if let Some(content) = template_catalog::get_embedded_template(name) {
129 return Ok(content);
130 }
131
132 Err(TemplateError::TemplateNotFound {
134 name: name.to_string(),
135 })
136 }
137
138 #[must_use]
144 #[cfg(test)]
145 pub fn all_template_names() -> Vec<String> {
146 use crate::prompts::template_catalog;
147 template_catalog::list_all_templates()
148 .iter()
149 .map(|t| t.name.to_string())
150 .collect()
151 }
152
153 #[must_use]
159 #[cfg(test)]
160 pub fn template_exists(&self, name: &str) -> bool {
161 self.has_user_template(name) || self.get_template(name).is_ok()
162 }
163}
164
165impl Default for TemplateRegistry {
166 fn default() -> Self {
167 Self::new(Self::default_user_templates_dir())
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_registry_creation() {
177 let registry = TemplateRegistry::new(None);
178 assert!(registry.user_templates_dir.is_none());
179
180 let custom_dir = PathBuf::from("/custom/templates");
181 let registry = TemplateRegistry::new(Some(custom_dir.clone()));
182 assert_eq!(registry.user_templates_dir, Some(custom_dir));
183 }
184
185 #[test]
186 fn test_default_user_templates_dir() {
187 let dir = TemplateRegistry::default_user_templates_dir();
188 assert!(dir.is_some());
189 let path = dir.unwrap();
190 assert!(path.to_string_lossy().contains("templates"));
191 }
192
193 #[test]
194 fn test_has_user_template_no_dir() {
195 let registry = TemplateRegistry::new(None);
196 assert!(!registry.has_user_template("commit_message_xml"));
197 }
198
199 #[test]
200 fn test_template_source_no_dir() {
201 let registry = TemplateRegistry::new(None);
202 let source = registry.template_source("commit_message_xml");
203 assert_eq!(source, "embedded");
204 }
205
206 #[test]
207 fn test_template_source_not_found() {
208 let registry = TemplateRegistry::new(None);
209 let source = registry.template_source("nonexistent_template");
210 assert_eq!(source, "embedded");
211 }
212
213 #[test]
214 fn test_default_registry() {
215 let registry = TemplateRegistry::default();
216 if TemplateRegistry::default_user_templates_dir().is_some() {
218 assert!(registry.user_templates_dir.is_some());
219 }
220 }
221
222 #[test]
223 fn test_get_template_embedded() {
224 let registry = TemplateRegistry::new(None);
225 let result = registry.get_template("developer_iteration_xml");
226 assert!(result.is_ok());
227 let content = result.unwrap();
228 assert!(!content.is_empty());
229 assert!(content.contains("IMPLEMENTATION MODE") || content.contains("Developer"));
230 }
231
232 #[test]
233 fn test_get_template_not_found() {
234 let registry = TemplateRegistry::new(None);
235 let result = registry.get_template("nonexistent_template");
236 assert!(result.is_err());
237 assert!(matches!(
238 result.unwrap_err(),
239 TemplateError::TemplateNotFound { .. }
240 ));
241 }
242
243 #[test]
244 fn test_all_template_names() {
245 let names = TemplateRegistry::all_template_names();
246 assert!(!names.is_empty());
247 assert!(names.len() >= 10); assert!(names.contains(&"developer_iteration_xml".to_string()));
249 assert!(names.contains(&"commit_message_xml".to_string()));
250 }
251
252 #[test]
253 fn test_template_exists_embedded() {
254 let registry = TemplateRegistry::new(None);
255 assert!(registry.template_exists("developer_iteration_xml"));
256 assert!(registry.template_exists("commit_message_xml"));
257 }
258
259 #[test]
260 fn test_template_not_exists() {
261 let registry = TemplateRegistry::new(None);
262 assert!(!registry.template_exists("nonexistent_template"));
263 }
264
265 #[test]
266 fn test_get_commit_template() {
267 let registry = TemplateRegistry::new(None);
268 let result = registry.get_template("commit_message_xml");
269 assert!(result.is_ok());
270 let content = result.unwrap();
271 assert!(!content.is_empty());
272 }
273
274 #[test]
275 fn test_get_review_xml_template() {
276 let registry = TemplateRegistry::new(None);
277 let result = registry.get_template("review_xml");
279 assert!(result.is_ok());
280 let content = result.unwrap();
281 assert!(!content.is_empty());
282 assert!(content.contains("REVIEW MODE"));
283 }
284
285 #[test]
286 fn test_get_fix_mode_template() {
287 let registry = TemplateRegistry::new(None);
288 let result = registry.get_template("fix_mode_xml");
289 assert!(result.is_ok());
290 let content = result.unwrap();
291 assert!(!content.is_empty());
292 }
293
294 #[test]
295 fn test_all_templates_have_content() {
296 let registry = TemplateRegistry::new(None);
297 for name in TemplateRegistry::all_template_names() {
298 let result = registry.get_template(&name);
299 assert!(result.is_ok(), "Template '{name}' should load successfully");
300 let content = result.unwrap();
301 assert!(!content.is_empty(), "Template '{name}' should not be empty");
302 }
303 }
304}