1use std::path::PathBuf;
7
8use super::renderer::DependencyChainEntry;
9use crate::core::ResourceType;
10
11#[derive(Debug)]
13pub enum TemplateError {
14 VariableNotFound {
16 variable: String,
18 available_variables: Box<Vec<String>>,
20 suggestions: Box<Vec<String>>,
22 location: Box<ErrorLocation>,
24 },
25
26 CircularDependency {
28 chain: Box<Vec<DependencyChainEntry>>,
30 },
31
32 SyntaxError {
34 message: String,
36 location: Box<ErrorLocation>,
38 },
39
40 DependencyRenderFailed {
42 dependency: String,
44 source: Box<dyn std::error::Error + Send + Sync>,
46 location: Box<ErrorLocation>,
48 },
49
50 ContentFilterError {
52 depth: usize,
54 source: Box<dyn std::error::Error + Send + Sync>,
56 location: Box<ErrorLocation>,
58 },
59}
60
61#[derive(Debug, Clone)]
63pub struct ErrorLocation {
64 pub resource_name: String,
66 pub resource_type: ResourceType,
67 pub dependency_chain: Vec<DependencyChainEntry>,
69 pub file_path: Option<PathBuf>,
71 pub line_number: Option<usize>,
73}
74
75impl std::fmt::Display for TemplateError {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 TemplateError::VariableNotFound {
79 variable,
80 ..
81 } => {
82 write!(f, "Template variable not found: '{}'", variable)
83 }
84 TemplateError::SyntaxError {
85 message,
86 ..
87 } => {
88 write!(f, "Template syntax error: {}", message)
89 }
90 TemplateError::CircularDependency {
91 chain,
92 } => {
93 if let Some(first) = chain.first() {
94 write!(f, "Circular dependency detected: {}", first.name)
95 } else {
96 write!(f, "Circular dependency detected")
97 }
98 }
99 TemplateError::DependencyRenderFailed {
100 dependency,
101 source,
102 ..
103 } => {
104 write!(f, "Failed to render dependency '{}': {}", dependency, source)
105 }
106 TemplateError::ContentFilterError {
107 source,
108 ..
109 } => {
110 write!(f, "Content filter error: {}", source)
111 }
112 }
113 }
114}
115
116impl std::error::Error for TemplateError {
117 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
118 match self {
119 TemplateError::DependencyRenderFailed {
120 source,
121 ..
122 } => Some(source.as_ref()),
123 TemplateError::ContentFilterError {
124 source,
125 ..
126 } => Some(source.as_ref()),
127 _ => None,
128 }
129 }
130}
131
132impl TemplateError {
133 pub fn format_with_context(&self) -> String {
135 match self {
136 TemplateError::VariableNotFound {
137 variable,
138 available_variables,
139 suggestions,
140 location,
141 } => format_variable_not_found_error(
142 variable,
143 available_variables,
144 suggestions,
145 location,
146 ),
147 TemplateError::CircularDependency {
148 chain,
149 } => format_circular_dependency_error(chain),
150 TemplateError::SyntaxError {
151 message,
152 location,
153 } => format_syntax_error(message, location),
154 TemplateError::DependencyRenderFailed {
155 dependency,
156 source,
157 location,
158 } => format_dependency_render_error(dependency, source.as_ref(), location),
159 TemplateError::ContentFilterError {
160 depth,
161 source,
162 location,
163 } => format_content_filter_error(*depth, source.as_ref(), location),
164 }
165 }
166}
167
168fn format_variable_not_found_error(
170 variable: &str,
171 available_variables: &[String],
172 suggestions: &[String],
173 location: &ErrorLocation,
174) -> String {
175 let mut msg = String::new();
176
177 msg.push_str("ERROR: Template Variable Not Found\n\n");
179
180 msg.push_str(&format!("Variable: {}\n", variable));
182
183 if let Some(line) = location.line_number {
184 msg.push_str(&format!("Line: {}\n", line));
185 }
186
187 msg.push_str(&format!(
188 "Resource: {} ({})\n\n",
189 location.resource_name,
190 format_resource_type(&location.resource_type)
191 ));
192
193 if !location.dependency_chain.is_empty() {
195 msg.push_str("Dependency chain:\n");
196 for (i, entry) in location.dependency_chain.iter().enumerate() {
197 let indent = " ".repeat(i);
198 let arrow = if i > 0 {
199 "└─ "
200 } else {
201 ""
202 };
203 let warning = if i == location.dependency_chain.len() - 1 {
204 " ⚠️ Error occurred here"
205 } else {
206 ""
207 };
208
209 msg.push_str(&format!(
210 "{}{}{}: {}{}\n",
211 indent,
212 arrow,
213 format_resource_type(&entry.resource_type),
214 entry.name,
215 warning
216 ));
217 }
218 msg.push('\n');
219 }
220
221 if variable.starts_with("agpm.deps.") {
223 msg.push_str(&format_missing_dependency_suggestion(variable, location));
224 } else if !suggestions.is_empty() {
225 msg.push_str("Did you mean one of these?\n");
226 for suggestion in suggestions.iter() {
227 msg.push_str(&format!(" - {}\n", suggestion));
228 }
229 msg.push('\n');
230 }
231
232 if !available_variables.is_empty() {
234 msg.push_str("Available variables in this context:\n");
235
236 let mut grouped = std::collections::BTreeMap::new();
238 for var in available_variables.iter() {
239 let prefix = var.split('.').next().unwrap_or(var);
240 grouped.entry(prefix).or_insert_with(Vec::new).push(var.clone());
241 }
242
243 for (prefix, vars) in grouped.iter().take(5) {
244 if vars.len() <= 3 {
245 for var in vars {
246 msg.push_str(&format!(" {}\n", var));
247 }
248 } else {
249 msg.push_str(&format!(" {}.* ({} variables)\n", prefix, vars.len()));
250 }
251 }
252
253 if grouped.len() > 5 {
254 msg.push_str(&format!(" ... and {} more\n", grouped.len() - 5));
255 }
256 msg.push('\n');
257 }
258
259 msg
260}
261
262fn format_missing_dependency_suggestion(variable: &str, location: &ErrorLocation) -> String {
264 let parts: Vec<&str> = variable.split('.').collect();
266 if parts.len() < 4 || parts[0] != "agpm" || parts[1] != "deps" {
267 return String::new();
268 }
269
270 let dep_type = parts[2]; let dep_name = parts[3]; let suggested_filename = dep_name.replace('_', "-");
276
277 let mut msg = String::new();
278 msg.push_str(&format!(
279 "Suggestion: '{}' references '{}' but doesn't declare it as a dependency.\n\n",
280 location.resource_name, dep_name
281 ));
282
283 msg.push_str(&format!("Fix: Add this to {} frontmatter:\n\n", location.resource_name));
284 msg.push_str("---\n");
285 msg.push_str("agpm:\n");
286 msg.push_str(" templating: true\n");
287 msg.push_str("dependencies:\n");
288 msg.push_str(&format!(" {}:\n", dep_type));
289 msg.push_str(&format!(" - path: ./{}.md\n", suggested_filename));
290 msg.push_str(" install: false\n");
291 msg.push_str("---\n\n");
292
293 msg.push_str("Note: Adjust the path based on actual file location.\n\n");
294
295 msg
296}
297
298fn format_circular_dependency_error(chain: &[DependencyChainEntry]) -> String {
300 let mut msg = String::new();
301
302 msg.push_str("ERROR: Circular Dependency Detected\n\n");
303 msg.push_str("A resource is attempting to include itself through a chain of dependencies.\n\n");
304
305 msg.push_str("Circular chain:\n");
306 for entry in chain.iter() {
307 msg.push_str(&format!(
308 " {} ({})\n",
309 entry.name,
310 format_resource_type(&entry.resource_type)
311 ));
312 msg.push_str(" ↓\n");
313 }
314 msg.push_str(&format!(" {} (circular reference)\n\n", chain[0].name));
315
316 msg.push_str("Suggestion: Remove the dependency that creates the cycle.\n");
317 msg.push_str("Consider refactoring shared content into a separate resource.\n\n");
318
319 msg
320}
321
322fn format_syntax_error(message: &str, location: &ErrorLocation) -> String {
324 let mut msg = String::new();
325
326 msg.push_str("ERROR: Template syntax error\n\n");
327 msg.push_str(&format!("Error: {}\n", message));
328 msg.push_str(&format!(
329 "Resource: {} ({})\n",
330 location.resource_name,
331 format_resource_type(&location.resource_type)
332 ));
333
334 if let Some(line) = location.line_number {
335 msg.push_str(&format!("Line: {}\n", line));
336 }
337
338 if !location.dependency_chain.is_empty() {
339 msg.push_str("\nDependency chain:\n");
340 for entry in &location.dependency_chain {
341 msg.push_str(&format!(
342 " {} ({})\n",
343 entry.name,
344 format_resource_type(&entry.resource_type)
345 ));
346 }
347 }
348
349 msg.push_str("\nSuggestion: Check template syntax for unclosed tags or invalid expressions.\n");
350 msg.push_str("Common issues:\n");
351 msg.push_str(" - Unclosed {{ }} or {% %} delimiters\n");
352 msg.push_str(" - Invalid filter names\n");
353 msg.push_str(" - Missing quotes around string values\n\n");
354
355 msg
356}
357
358fn format_dependency_render_error(
360 dependency: &str,
361 source: &(dyn std::error::Error + Send + Sync),
362 location: &ErrorLocation,
363) -> String {
364 let mut msg = String::new();
365
366 msg.push_str("ERROR: Dependency Render Failed\n\n");
367 msg.push_str(&format!("Dependency: {}\n", dependency));
368 msg.push_str(&format!("Error: {}\n", source));
369
370 if let Some(line) = location.line_number {
371 msg.push_str(&format!("Line: {}\n", line));
372 }
373
374 msg.push_str(&format!(
375 "Resource: {} ({})\n",
376 location.resource_name,
377 format_resource_type(&location.resource_type)
378 ));
379
380 msg.push_str("\nSuggestion: Check the dependency file for template errors.\n");
381 msg.push_str("The dependency may contain invalid template syntax or missing variables.\n\n");
382
383 msg
384}
385
386fn format_content_filter_error(
388 depth: usize,
389 source: &(dyn std::error::Error + Send + Sync),
390 location: &ErrorLocation,
391) -> String {
392 let mut msg = String::new();
393
394 msg.push_str("ERROR: Content Filter Error\n\n");
395 msg.push_str(&format!("Depth: {}\n", depth));
396 msg.push_str(&format!("Error: {}\n", source));
397
398 if let Some(line) = location.line_number {
399 msg.push_str(&format!("Line: {}\n", line));
400 }
401
402 msg.push_str(&format!(
403 "Resource: {} ({})\n",
404 location.resource_name,
405 format_resource_type(&location.resource_type)
406 ));
407
408 msg.push_str("\nSuggestion: Check the file being included by the content filter.\n");
409 msg.push_str("The included file may contain template errors or circular dependencies.\n\n");
410
411 msg
412}
413
414pub fn format_resource_type(rt: &ResourceType) -> String {
443 match rt {
444 ResourceType::Agent => "agent",
445 ResourceType::Command => "command",
446 ResourceType::Snippet => "snippet",
447 ResourceType::Hook => "hook",
448 ResourceType::Script => "script",
449 ResourceType::McpServer => "mcp-server",
450 }
451 .to_string()
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::core::ResourceType;
458 use crate::templating::renderer::DependencyChainEntry;
459 use std::error::Error;
460
461 #[test]
462 fn test_template_error_variable_not_found() {
463 let error = TemplateError::VariableNotFound {
464 variable: "missing_var".to_string(),
465 available_variables: Box::new(vec![
466 "var1".to_string(),
467 "var2".to_string(),
468 "similar_var".to_string(),
469 ]),
470 suggestions: Box::new(vec![
471 "Did you mean 'similar_var'?".to_string(),
472 "Check variable spelling".to_string(),
473 ]),
474 location: Box::new(ErrorLocation {
475 resource_name: "test-agent".to_string(),
476 resource_type: ResourceType::Agent,
477 dependency_chain: vec![DependencyChainEntry {
478 name: "agent1".to_string(),
479 resource_type: ResourceType::Agent,
480 path: Some("agents/agent1.md".to_string()),
481 }],
482 file_path: None,
483 line_number: Some(10),
484 }),
485 };
486
487 let formatted = error.format_with_context();
488
489 assert!(formatted.contains("Template Variable Not Found"));
490 assert!(formatted.contains("missing_var"));
491 assert!(formatted.contains("test-agent"));
492 assert!(formatted.contains("Line: 10"));
493 assert!(formatted.contains("agent1"));
494 }
495
496 #[test]
497 fn test_template_error_circular_dependency() {
498 let error = TemplateError::CircularDependency {
499 chain: Box::new(vec![
500 DependencyChainEntry {
501 name: "agent-a".to_string(),
502 resource_type: ResourceType::Agent,
503 path: Some("agents/agent-a.md".to_string()),
504 },
505 DependencyChainEntry {
506 name: "agent-b".to_string(),
507 resource_type: ResourceType::Agent,
508 path: Some("agents/agent-b.md".to_string()),
509 },
510 DependencyChainEntry {
511 name: "agent-a".to_string(),
512 resource_type: ResourceType::Agent,
513 path: Some("agents/agent-a.md".to_string()),
514 },
515 ]),
516 };
517
518 let formatted = error.format_with_context();
519
520 assert!(formatted.contains("Circular Dependency"));
521 assert!(formatted.contains("agent-a"));
522 assert!(formatted.contains("agent-b"));
523 }
524
525 #[test]
526 fn test_template_error_syntax_error() {
527 let error = TemplateError::SyntaxError {
528 message: "Unexpected end of template".to_string(),
529 location: Box::new(ErrorLocation {
530 resource_name: "test-snippet".to_string(),
531 resource_type: ResourceType::Snippet,
532 dependency_chain: vec![],
533 file_path: None,
534 line_number: Some(25),
535 }),
536 };
537
538 let formatted = error.format_with_context();
539
540 assert!(formatted.contains("Template syntax error"));
541 assert!(formatted.contains("Unexpected end of template"));
542 assert!(formatted.contains("test-snippet"));
543 assert!(formatted.contains("Line: 25"));
544 }
545
546 #[test]
547 fn test_template_error_dependency_render_failed() {
548 let source_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
549 let error = TemplateError::DependencyRenderFailed {
550 dependency: "helper-agent".to_string(),
551 source: Box::new(source_error),
552 location: Box::new(ErrorLocation {
553 resource_name: "main-agent".to_string(),
554 resource_type: ResourceType::Agent,
555 dependency_chain: vec![DependencyChainEntry {
556 name: "helper-agent".to_string(),
557 resource_type: ResourceType::Agent,
558 path: Some("agents/helper-agent.md".to_string()),
559 }],
560 file_path: None,
561 line_number: None,
562 }),
563 };
564
565 let formatted = error.format_with_context();
566
567 assert!(formatted.contains("Dependency Render Failed"));
568 assert!(formatted.contains("helper-agent"));
569 assert!(formatted.contains("File not found"));
570 assert!(formatted.contains("main-agent"));
571 }
572
573 #[test]
574 fn test_template_error_content_filter_error() {
575 let source_error =
576 std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
577 let error = TemplateError::ContentFilterError {
578 depth: 5,
579 source: Box::new(source_error),
580 location: Box::new(ErrorLocation {
581 resource_name: "test-script".to_string(),
582 resource_type: ResourceType::Script,
583 dependency_chain: vec![],
584 file_path: None,
585 line_number: Some(15),
586 }),
587 };
588
589 let formatted = error.format_with_context();
590
591 assert!(formatted.contains("Content Filter Error"));
592 assert!(formatted.contains("Depth: 5"));
593 assert!(formatted.contains("Access denied"));
594 assert!(formatted.contains("test-script"));
595 assert!(formatted.contains("Line: 15"));
596 }
597
598 #[test]
599 fn test_error_location_with_line_number() {
600 let location = ErrorLocation {
601 resource_name: "test-resource".to_string(),
602 resource_type: ResourceType::McpServer,
603 dependency_chain: vec![DependencyChainEntry {
604 name: "dep1".to_string(),
605 resource_type: ResourceType::Agent,
606 path: Some("agents/dep1.md".to_string()),
607 }],
608 file_path: Some(std::path::PathBuf::from("agents/test.md")),
609 line_number: Some(42),
610 };
611
612 assert_eq!(location.resource_name, "test-resource");
613 assert_eq!(location.resource_type, ResourceType::McpServer);
614 assert_eq!(location.dependency_chain.len(), 1);
615 assert_eq!(location.file_path.as_ref().unwrap().to_str().unwrap(), "agents/test.md");
616 assert_eq!(location.line_number, Some(42));
617 }
618
619 #[test]
620 fn test_error_location_without_line_number() {
621 let location = ErrorLocation {
622 resource_name: "test-resource".to_string(),
623 resource_type: ResourceType::Command,
624 dependency_chain: vec![],
625 file_path: None,
626 line_number: None,
627 };
628
629 assert_eq!(location.resource_name, "test-resource");
630 assert_eq!(location.resource_type, ResourceType::Command);
631 assert!(location.dependency_chain.is_empty());
632 assert!(location.file_path.is_none());
633 assert!(location.line_number.is_none());
634 }
635
636 #[test]
637 fn test_format_resource_type() {
638 assert_eq!(format_resource_type(&ResourceType::Agent), "agent");
639 assert_eq!(format_resource_type(&ResourceType::Snippet), "snippet");
640 assert_eq!(format_resource_type(&ResourceType::Command), "command");
641 assert_eq!(format_resource_type(&ResourceType::McpServer), "mcp-server");
642 assert_eq!(format_resource_type(&ResourceType::Script), "script");
643 assert_eq!(format_resource_type(&ResourceType::Hook), "hook");
644 }
645
646 #[test]
647 fn test_template_error_source() {
648 let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied");
649 let error = TemplateError::DependencyRenderFailed {
650 dependency: "test-dep".to_string(),
651 source: Box::new(io_error),
652 location: Box::new(ErrorLocation {
653 resource_name: "test-resource".to_string(),
654 resource_type: ResourceType::Agent,
655 dependency_chain: vec![],
656 file_path: None,
657 line_number: None,
658 }),
659 };
660
661 let source = error.source();
663 assert!(source.is_some());
664 }
665}