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    /// Network-related errors
102    #[error("Network error: {0}")]
103    Network(String),
104
105    /// Invalid input provided
106    #[error("Invalid input: {0}")]
107    InvalidInput(String),
108
109    /// Feature not yet implemented
110    #[error("Not implemented: {0}")]
111    NotImplemented(String),
112
113    /// Required feature not enabled
114    #[error("Feature '{0}' not enabled. Enable it in Cargo.toml")]
115    FeatureNotEnabled(String),
116}
117
118#[cfg(feature = "export-toml")]
119impl From<toml::ser::Error> for Error {
120    fn from(err: toml::ser::Error) -> Self {
121        Self::Io(std::io::Error::new(
122            std::io::ErrorKind::Other,
123            err.to_string(),
124        ))
125    }
126}
127
128impl From<std::fmt::Error> for Error {
129    fn from(err: std::fmt::Error) -> Self {
130        Self::Io(std::io::Error::new(
131            std::io::ErrorKind::Other,
132            err.to_string(),
133        ))
134    }
135}
136
137impl Error {
138    /// Create a new parse error with helpful context
139    #[must_use]
140    pub fn parse(message: impl Into<String>, line: usize, col: usize) -> Self {
141        Self::Parse {
142            message: message.into(),
143            line,
144            col,
145            snippet: None,
146            help: None,
147        }
148    }
149
150    /// Create a parse error with source snippet and help text
151    #[must_use]
152    pub fn parse_with_context(
153        message: impl Into<String>,
154        line: usize,
155        col: usize,
156        snippet: impl Into<String>,
157        help: impl Into<String>,
158    ) -> Self {
159        Self::Parse {
160            message: message.into(),
161            line,
162            col,
163            snippet: Some(snippet.into()),
164            help: Some(help.into()),
165        }
166    }
167
168    /// Create an unexpected EOF error
169    #[must_use]
170    pub fn unexpected_eof(expected: impl Into<String>, line: usize) -> Self {
171        Self::UnexpectedEof {
172            expected: expected.into(),
173            line,
174        }
175    }
176
177    /// Create a syntax error with expected/found information
178    #[must_use]
179    pub fn syntax(
180        message: impl Into<String>,
181        line: usize,
182        col: usize,
183        expected: Option<String>,
184        found: Option<String>,
185    ) -> Self {
186        Self::Syntax {
187            message: message.into(),
188            line,
189            col,
190            expected,
191            found,
192        }
193    }
194
195    /// Create an invalid directive error with suggestion
196    #[must_use]
197    pub fn invalid_directive(
198        name: impl Into<String>,
199        reason: Option<String>,
200        suggestion: Option<String>,
201    ) -> Self {
202        Self::InvalidDirective {
203            name: name.into(),
204            reason,
205            suggestion,
206        }
207    }
208
209    /// Create a custom error
210    #[must_use]
211    pub fn custom(message: impl Into<String>) -> Self {
212        Self::Custom(message.into())
213    }
214
215    /// Get the error message for display
216    #[must_use]
217    pub fn message(&self) -> String {
218        match self {
219            Self::Parse { message, .. }
220            | Self::InvalidArgument { message, .. }
221            | Self::Syntax { message, .. } => message.clone(),
222            Self::InvalidDirective { name, .. } => name.clone(),
223            _ => self.to_string(),
224        }
225    }
226
227    /// Get detailed error information with formatted output
228    ///
229    /// This provides a beautiful, colorized error display with:
230    /// - Error location and message
231    /// - Source code snippet
232    /// - Visual pointer to the error
233    /// - Helpful suggestions
234    #[must_use]
235    pub fn detailed(&self) -> String {
236        match self {
237            Self::Parse {
238                message,
239                line,
240                col,
241                snippet,
242                help,
243            } => format_parse_error(*line, *col, message, snippet.as_deref(), help.as_deref()),
244            Self::Syntax {
245                message,
246                line,
247                col,
248                expected,
249                found,
250            } => format_syntax_error(*line, *col, message, expected.as_deref(), found.as_deref()),
251            Self::UnexpectedEof { expected, line } => {
252                format!("Unexpected end of file at line {line}\nExpected: {expected}")
253            }
254            Self::InvalidDirective {
255                name,
256                reason,
257                suggestion,
258            } => {
259                let mut output = format!("Invalid directive: {name}");
260                if let Some(r) = reason {
261                    let _ = write!(output, "\nReason: {r}");
262                }
263                if let Some(s) = suggestion {
264                    let _ = write!(output, "\nSuggestion: Try using '{s}' instead");
265                }
266                output
267            }
268            _ => self.to_string(),
269        }
270    }
271
272    /// Get a short, one-line description
273    #[must_use]
274    pub fn short(&self) -> String {
275        match self {
276            Self::Parse {
277                message, line, col, ..
278            }
279            | Self::Syntax {
280                message, line, col, ..
281            } => {
282                format!("line {line}:{col}: {message}")
283            }
284            _ => self.to_string(),
285        }
286    }
287}
288
289/// Format a parse error with beautiful output
290fn format_parse_error(
291    line: usize,
292    col: usize,
293    message: &str,
294    snippet: Option<&str>,
295    help: Option<&str>,
296) -> String {
297    let mut output = format!("Parse error at line {line}, column {col}: {message}");
298
299    if let Some(snippet) = snippet {
300        let _ = writeln!(output, "\n");
301        let _ = writeln!(output, "{snippet}");
302        // Add pointer to error location
303        let pointer = format!("{}^", " ".repeat(col.saturating_sub(1)));
304        let _ = writeln!(output, "{pointer}");
305    }
306
307    if let Some(help) = help {
308        let _ = writeln!(output, "\nHelp: {help}");
309    }
310
311    output
312}
313
314/// Format a syntax error with expected/found information
315fn format_syntax_error(
316    line: usize,
317    col: usize,
318    message: &str,
319    expected: Option<&str>,
320    found: Option<&str>,
321) -> String {
322    let mut output = format!("Syntax error at line {line}, column {col}: {message}");
323
324    if let Some(exp) = expected {
325        let _ = write!(output, "\nExpected: {exp}");
326    }
327
328    if let Some(fnd) = found {
329        let _ = write!(output, "\nFound: {fnd}");
330    }
331
332    output
333}
334
335#[cfg(feature = "serde")]
336impl From<serde_json::Error> for Error {
337    fn from(err: serde_json::Error) -> Self {
338        Self::Serialization(err.to_string())
339    }
340}
341
342#[cfg(feature = "serde")]
343impl From<serde_yaml::Error> for Error {
344    fn from(err: serde_yaml::Error) -> Self {
345        Self::Serialization(err.to_string())
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_parse_error() {
355        let err = Error::parse("unexpected token", 10, 5);
356        assert!(err.to_string().contains("line 10"));
357        assert!(err.to_string().contains("column 5"));
358        assert_eq!(err.short(), "line 10:5: unexpected token");
359    }
360
361    #[test]
362    fn test_parse_error_with_context() {
363        let err = Error::parse_with_context(
364            "unexpected semicolon",
365            2,
366            10,
367            "server { listen 80;; }",
368            "Remove the extra semicolon",
369        );
370        let detailed = err.detailed();
371        assert!(detailed.contains("line 2"));
372        assert!(detailed.contains("server { listen 80;; }"));
373        assert!(detailed.contains('^'));
374        assert!(detailed.contains("Help: Remove the extra semicolon"));
375    }
376
377    #[test]
378    fn test_syntax_error() {
379        let err = Error::syntax(
380            "invalid token",
381            5,
382            12,
383            Some("';' or '{'".to_string()),
384            Some("'@'".to_string()),
385        );
386        let detailed = err.detailed();
387        assert!(detailed.contains("Syntax error"));
388        assert!(detailed.contains("Expected: ';' or '{'"));
389        assert!(detailed.contains("Found: '@'"));
390    }
391
392    #[test]
393    fn test_unexpected_eof() {
394        let err = Error::unexpected_eof("closing brace '}'", 100);
395        assert!(err.to_string().contains("Unexpected end of input"));
396        let detailed = err.detailed();
397        assert!(detailed.contains("line 100"));
398        assert!(detailed.contains("Expected: closing brace '}'"));
399    }
400
401    #[test]
402    fn test_invalid_directive() {
403        let err = Error::invalid_directive(
404            "liste",
405            Some("Unknown directive".to_string()),
406            Some("listen".to_string()),
407        );
408        let detailed = err.detailed();
409        assert!(detailed.contains("Invalid directive: liste"));
410        assert!(detailed.contains("Reason: Unknown directive"));
411        assert!(detailed.contains("Try using 'listen' instead"));
412    }
413
414    #[test]
415    fn test_custom_error() {
416        let err = Error::custom("something went wrong");
417        assert_eq!(err.message(), "something went wrong");
418    }
419
420    #[test]
421    fn test_error_formatting() {
422        let err = Error::parse_with_context(
423            "missing semicolon",
424            3,
425            20,
426            "server { listen 80 }",
427            "Add a semicolon after '80'",
428        );
429
430        let detailed = err.detailed();
431        // Should contain line number
432        assert!(detailed.contains("line 3"));
433        // Should contain snippet
434        assert!(detailed.contains("server { listen 80 }"));
435        // Should contain pointer
436        assert!(detailed.contains('^'));
437        // Should contain help
438        assert!(detailed.contains("Help:"));
439    }
440}