cooklang_reports/error.rs
1//! Error types for the cooklang-reports library.
2
3use thiserror::Error;
4
5/// Error type for this crate.
6#[derive(Error, Debug)]
7pub enum Error {
8 /// An error occurred when parsing the recipe.
9 #[error("error parsing recipe")]
10 RecipeParseError(#[from] cooklang::error::SourceReport),
11
12 /// An error occurred when generating a report from a template.
13 #[error("template error")]
14 TemplateError(#[from] minijinja::Error),
15}
16
17impl Error {
18 /// Format the error with full context including source chain and helpful hints
19 ///
20 /// This method provides comprehensive error formatting that includes:
21 /// - The main error message
22 /// - The complete chain of error causes
23 /// - Template-specific context for common errors
24 /// - Helpful suggestions for fixing the error
25 ///
26 /// # Returns
27 /// A formatted string suitable for display to end users with detailed error information.
28 ///
29 /// # Example
30 /// ```no_run
31 /// use cooklang_reports::render_template;
32 ///
33 /// let recipe = "@eggs{2}";
34 /// let template = "{% for item in ingredients %}{{ item.name }}{% endfor"; // Missing %}
35 ///
36 /// match render_template(recipe, template) {
37 /// Ok(result) => println!("{}", result),
38 /// Err(err) => {
39 /// // This will print detailed error information including:
40 /// // - The syntax error
41 /// // - Line and column information
42 /// // - Suggestions for fixing missing closing tags
43 /// eprintln!("{}", err.format_with_source());
44 /// }
45 /// }
46 /// ```
47 ///
48 /// # Output Format
49 /// The output includes:
50 /// - Primary error message with debug info (line numbers, source context)
51 /// - Caused by chain (if any)
52 /// - Additional details from minijinja
53 /// - Context-specific help for common template errors
54 #[must_use]
55 pub fn format_with_source(&self) -> String {
56 use std::fmt::Write;
57
58 let mut output = String::new();
59
60 // Add template-specific context if it's a template error
61 if let Error::TemplateError(minijinja_err) = self {
62 // First show the actual error message
63 let error_detail = minijinja_err.detail().unwrap_or_default();
64 if !error_detail.is_empty() {
65 let _ = writeln!(output, "Error: {error_detail}");
66 }
67
68 // Then show the debug info with source location
69 let _ = write!(output, "{}", minijinja_err.display_debug_info());
70
71 // Add helpful hints based on error type
72 match minijinja_err.kind() {
73 minijinja::ErrorKind::SyntaxError => {
74 output.push_str("\n\nHint: This is a syntax error. Check for:");
75 output.push_str("\n • Missing closing tags ({% endfor %}, {% endif %}, etc.)");
76 output.push_str("\n • Invalid Jinja2 syntax");
77 output.push_str("\n • Unclosed strings or brackets");
78 }
79 minijinja::ErrorKind::UndefinedError => {
80 output.push_str("\n\nHint: A variable or attribute is undefined. Check that:");
81 output
82 .push_str("\n • All variables used in the template exist in the context");
83 output.push_str("\n • Property names are spelled correctly");
84 output.push_str("\n • You're not trying to access properties on null values");
85 }
86 minijinja::ErrorKind::InvalidOperation => {
87 // Check if the error message contains specific keywords for better hints
88 let error_str = minijinja_err.to_string();
89 if error_str.contains("Failed to scale recipe") {
90 output.push_str("\n\nHint: Recipe scaling failed. Check that:");
91 output.push_str("\n • The referenced recipe has the required metadata (servings or yield)");
92 output.push_str(
93 "\n • The units in the reference match the recipe's metadata",
94 );
95 output.push_str("\n • The recipe file exists and is valid");
96 } else {
97 output.push_str("\n\nHint: Invalid operation. Check that:");
98 output.push_str("\n • You're using the correct types for operations");
99 output.push_str("\n • Functions are called with correct arguments");
100 output.push_str("\n • Filters are applied to compatible values");
101 }
102 }
103 minijinja::ErrorKind::NonKey => {
104 output.push_str("\n\nHint: Key not found. Check that:");
105 output.push_str("\n • The key exists in your datastore");
106 output.push_str("\n • The key path is spelled correctly");
107 output.push_str("\n • String transformations are producing the expected keys");
108 }
109 _ => {}
110 }
111 // Don't traverse the error chain for template errors since display_debug_info already shows it
112 return output;
113 }
114
115 // For non-template errors, use the standard display
116 let _ = write!(output, "Error: {self:#}");
117
118 // Traverse the error chain for non-template errors
119 let mut current_error: &dyn std::error::Error = self;
120 while let Some(source) = current_error.source() {
121 let _ = write!(output, "\n\nCaused by:\n {source:#}");
122 current_error = source;
123 }
124
125 output
126 }
127}