Skip to main content

nginx_discovery/
error.rs

1//! Enhanced error types with helpful diagnostics
2//!
3//! This module provides detailed error messages with:
4//! - Source location tracking
5//! - Syntax highlighting of error locations
6//! - Helpful suggestions for fixes
7//! - Context-aware error messages
8
9use std::fmt::Write;
10
11/// Result type alias for nginx-discovery operations
12pub type Result<T> = std::result::Result<T, Error>;
13
14/// Main error type with enhanced diagnostics
15#[derive(Debug, thiserror::Error)]
16pub enum Error {
17    /// Error during parsing with detailed context
18    #[error("Parse error at line {line}, column {col}: {message}")]
19    Parse {
20        /// Error message
21        message: String,
22        /// Line number (1-indexed)
23        line: usize,
24        /// Column number (1-indexed)
25        col: usize,
26        /// Source snippet if available
27        snippet: Option<String>,
28        /// Helpful suggestion to fix the error
29        help: Option<String>,
30    },
31
32    /// Unexpected end of input while parsing
33    #[error("Unexpected end of input")]
34    UnexpectedEof {
35        /// What was expected
36        expected: String,
37        /// Location where EOF occurred
38        line: usize,
39    },
40
41    /// Invalid directive name or usage
42    #[error("Invalid directive: {name}")]
43    InvalidDirective {
44        /// Directive name
45        name: String,
46        /// Reason why it's invalid
47        reason: Option<String>,
48        /// Suggestion for valid alternative
49        suggestion: Option<String>,
50    },
51
52    /// Invalid argument for a directive
53    #[error("Invalid argument for directive '{directive}': {message}")]
54    InvalidArgument {
55        /// Directive name
56        directive: String,
57        /// Error message
58        message: String,
59        /// Expected format
60        expected: Option<String>,
61    },
62
63    /// Syntax error with context
64    #[error("Syntax error: {message}")]
65    Syntax {
66        /// Error message
67        message: String,
68        /// Location
69        line: usize,
70        /// Column
71        col: usize,
72        /// What was expected
73        expected: Option<String>,
74        /// What was found
75        found: Option<String>,
76    },
77
78    /// IO error
79    #[error("IO error: {0}")]
80    Io(#[from] std::io::Error),
81
82    /// System error (nginx not found, etc.)
83    #[cfg(feature = "system")]
84    #[error("System error: {0}")]
85    System(String),
86
87    /// Serialization error
88    #[cfg(feature = "serde")]
89    #[error("Serialization error: {0}")]
90    Serialization(String),
91
92    /// Include resolution error
93    #[cfg(feature = "includes")]
94    #[error("Include resolution error: {0}")]
95    Include(String),
96
97    /// Custom error
98    #[error("{0}")]
99    Custom(String),
100}
101
102impl Error {
103    /// Create a new parse error with helpful context
104    #[must_use]
105    pub fn parse(message: impl Into<String>, line: usize, col: usize) -> Self {
106        Self::Parse {
107            message: message.into(),
108            line,
109            col,
110            snippet: None,
111            help: None,
112        }
113    }
114
115    /// Create a parse error with source snippet and help text
116    #[must_use]
117    pub fn parse_with_context(
118        message: impl Into<String>,
119        line: usize,
120        col: usize,
121        snippet: impl Into<String>,
122        help: impl Into<String>,
123    ) -> Self {
124        Self::Parse {
125            message: message.into(),
126            line,
127            col,
128            snippet: Some(snippet.into()),
129            help: Some(help.into()),
130        }
131    }
132
133    /// Create an unexpected EOF error
134    #[must_use]
135    pub fn unexpected_eof(expected: impl Into<String>, line: usize) -> Self {
136        Self::UnexpectedEof {
137            expected: expected.into(),
138            line,
139        }
140    }
141
142    /// Create a syntax error with expected/found information
143    #[must_use]
144    pub fn syntax(
145        message: impl Into<String>,
146        line: usize,
147        col: usize,
148        expected: Option<String>,
149        found: Option<String>,
150    ) -> Self {
151        Self::Syntax {
152            message: message.into(),
153            line,
154            col,
155            expected,
156            found,
157        }
158    }
159
160    /// Create an invalid directive error with suggestion
161    #[must_use]
162    pub fn invalid_directive(
163        name: impl Into<String>,
164        reason: Option<String>,
165        suggestion: Option<String>,
166    ) -> Self {
167        Self::InvalidDirective {
168            name: name.into(),
169            reason,
170            suggestion,
171        }
172    }
173
174    /// Create a custom error
175    #[must_use]
176    pub fn custom(message: impl Into<String>) -> Self {
177        Self::Custom(message.into())
178    }
179
180    /// Get the error message for display
181    #[must_use]
182    pub fn message(&self) -> String {
183        match self {
184            Self::Parse { message, .. }
185            | Self::InvalidArgument { message, .. }
186            | Self::Syntax { message, .. } => message.clone(),
187            Self::InvalidDirective { name, .. } => name.clone(),
188            _ => self.to_string(),
189        }
190    }
191
192    /// Get detailed error information with formatted output
193    ///
194    /// This provides a beautiful, colorized error display with:
195    /// - Error location and message
196    /// - Source code snippet
197    /// - Visual pointer to the error
198    /// - Helpful suggestions
199    #[must_use]
200    pub fn detailed(&self) -> String {
201        match self {
202            Self::Parse {
203                message,
204                line,
205                col,
206                snippet,
207                help,
208            } => format_parse_error(*line, *col, message, snippet.as_deref(), help.as_deref()),
209            Self::Syntax {
210                message,
211                line,
212                col,
213                expected,
214                found,
215            } => format_syntax_error(*line, *col, message, expected.as_deref(), found.as_deref()),
216            Self::UnexpectedEof { expected, line } => {
217                format!("Unexpected end of file at line {line}\nExpected: {expected}")
218            }
219            Self::InvalidDirective {
220                name,
221                reason,
222                suggestion,
223            } => {
224                let mut output = format!("Invalid directive: {name}");
225                if let Some(r) = reason {
226                    let _ = write!(output, "\nReason: {r}");
227                }
228                if let Some(s) = suggestion {
229                    let _ = write!(output, "\nSuggestion: Try using '{s}' instead");
230                }
231                output
232            }
233            _ => self.to_string(),
234        }
235    }
236
237    /// Get a short, one-line description
238    #[must_use]
239    pub fn short(&self) -> String {
240        match self {
241            Self::Parse {
242                message, line, col, ..
243            }
244            | Self::Syntax {
245                message, line, col, ..
246            } => {
247                format!("line {line}:{col}: {message}")
248            }
249            _ => self.to_string(),
250        }
251    }
252}
253
254/// Format a parse error with beautiful output
255fn format_parse_error(
256    line: usize,
257    col: usize,
258    message: &str,
259    snippet: Option<&str>,
260    help: Option<&str>,
261) -> String {
262    let mut output = format!("Parse error at line {line}, column {col}: {message}");
263
264    if let Some(snippet) = snippet {
265        let _ = writeln!(output, "\n");
266        let _ = writeln!(output, "{snippet}");
267        // Add pointer to error location
268        let pointer = format!("{}^", " ".repeat(col.saturating_sub(1)));
269        let _ = writeln!(output, "{pointer}");
270    }
271
272    if let Some(help) = help {
273        let _ = writeln!(output, "\nHelp: {help}");
274    }
275
276    output
277}
278
279/// Format a syntax error with expected/found information
280fn format_syntax_error(
281    line: usize,
282    col: usize,
283    message: &str,
284    expected: Option<&str>,
285    found: Option<&str>,
286) -> String {
287    let mut output = format!("Syntax error at line {line}, column {col}: {message}");
288
289    if let Some(exp) = expected {
290        let _ = write!(output, "\nExpected: {exp}");
291    }
292
293    if let Some(fnd) = found {
294        let _ = write!(output, "\nFound: {fnd}");
295    }
296
297    output
298}
299
300#[cfg(feature = "serde")]
301impl From<serde_json::Error> for Error {
302    fn from(err: serde_json::Error) -> Self {
303        Self::Serialization(err.to_string())
304    }
305}
306
307#[cfg(feature = "serde")]
308impl From<serde_yaml::Error> for Error {
309    fn from(err: serde_yaml::Error) -> Self {
310        Self::Serialization(err.to_string())
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_parse_error() {
320        let err = Error::parse("unexpected token", 10, 5);
321        assert!(err.to_string().contains("line 10"));
322        assert!(err.to_string().contains("column 5"));
323        assert_eq!(err.short(), "line 10:5: unexpected token");
324    }
325
326    #[test]
327    fn test_parse_error_with_context() {
328        let err = Error::parse_with_context(
329            "unexpected semicolon",
330            2,
331            10,
332            "server { listen 80;; }",
333            "Remove the extra semicolon",
334        );
335        let detailed = err.detailed();
336        assert!(detailed.contains("line 2"));
337        assert!(detailed.contains("server { listen 80;; }"));
338        assert!(detailed.contains('^'));
339        assert!(detailed.contains("Help: Remove the extra semicolon"));
340    }
341
342    #[test]
343    fn test_syntax_error() {
344        let err = Error::syntax(
345            "invalid token",
346            5,
347            12,
348            Some("';' or '{'".to_string()),
349            Some("'@'".to_string()),
350        );
351        let detailed = err.detailed();
352        assert!(detailed.contains("Syntax error"));
353        assert!(detailed.contains("Expected: ';' or '{'"));
354        assert!(detailed.contains("Found: '@'"));
355    }
356
357    #[test]
358    fn test_unexpected_eof() {
359        let err = Error::unexpected_eof("closing brace '}'", 100);
360        assert!(err.to_string().contains("Unexpected end of input"));
361        let detailed = err.detailed();
362        assert!(detailed.contains("line 100"));
363        assert!(detailed.contains("Expected: closing brace '}'"));
364    }
365
366    #[test]
367    fn test_invalid_directive() {
368        let err = Error::invalid_directive(
369            "liste",
370            Some("Unknown directive".to_string()),
371            Some("listen".to_string()),
372        );
373        let detailed = err.detailed();
374        assert!(detailed.contains("Invalid directive: liste"));
375        assert!(detailed.contains("Reason: Unknown directive"));
376        assert!(detailed.contains("Try using 'listen' instead"));
377    }
378
379    #[test]
380    fn test_custom_error() {
381        let err = Error::custom("something went wrong");
382        assert_eq!(err.message(), "something went wrong");
383    }
384
385    #[test]
386    fn test_error_formatting() {
387        let err = Error::parse_with_context(
388            "missing semicolon",
389            3,
390            20,
391            "server { listen 80 }",
392            "Add a semicolon after '80'",
393        );
394
395        let detailed = err.detailed();
396        // Should contain line number
397        assert!(detailed.contains("line 3"));
398        // Should contain snippet
399        assert!(detailed.contains("server { listen 80 }"));
400        // Should contain pointer
401        assert!(detailed.contains('^'));
402        // Should contain help
403        assert!(detailed.contains("Help:"));
404    }
405}