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}