rust_cel_parser/
error.rs

1use crate::parser::Rule; // Import Rule from the parser module
2use thiserror::Error;
3
4#[derive(Error, Debug)]
5pub enum CelParserError {
6    #[error("Pest parsing error: {0}")]
7    PestError(#[from] Box<pest::error::Error<Rule>>), // Boxed to avoid large enum variant
8
9    #[error("Invalid integer literal: '{0}' ({1})")]
10    InvalidIntegerLiteral(String, std::num::ParseIntError),
11
12    #[error("Invalid unsigned integer literal: '{0}' ({1})")]
13    InvalidUintLiteral(String, std::num::ParseIntError),
14
15    #[error("Invalid float literal: '{0}' ({1})")]
16    InvalidFloatLiteral(String, std::num::ParseFloatError),
17
18    #[error("Invalid escape sequence in string/bytes literal: '\\{0}'")]
19    InvalidEscapeSequence(String),
20
21    #[error("Incomplete escape sequence: '{0}'")]
22    IncompleteEscapeSequence(String),
23
24    #[error(
25        "Invalid Unicode escape sequence (must be U+0000 to U+10FFFF, excluding surrogates): '{0}'"
26    )]
27    InvalidUnicodeEscape(String),
28
29    #[error("Invalid byte sequence in string literal (not valid UTF-8)")]
30    InvalidUtf8String(#[from] std::string::FromUtf8Error),
31
32    #[error("Internal parser error: {0}")]
33    InternalError(String), // For unexpected states
34}
35
36// Helper to convert Pest errors, wrapping in Box
37impl From<pest::error::Error<Rule>> for CelParserError {
38    fn from(err: pest::error::Error<Rule>) -> Self {
39        CelParserError::PestError(Box::new(err))
40    }
41}
42
43impl CelParserError {
44    /// Returns the line and column number where the error occurred, if available.
45    ///
46    /// This is useful for providing detailed feedback to users about syntax errors.
47    /// The location is 1-indexed (line 1, column 1).
48    ///
49    /// # Example
50    /// ```
51    /// use rust_cel_parser::parse_cel_program;
52    ///
53    /// let result = parse_cel_program("1 + & 2"); // Invalid operator
54    /// if let Err(e) = result {
55    ///     if let Some((line, col)) = e.location() {
56    ///         println!("Error at line {}, column {}: {}", line, col, e);
57    ///         assert_eq!(line, 1);
58    ///         assert_eq!(col, 5);
59    ///     }
60    /// }
61    /// ```
62    pub fn location(&self) -> Option<(usize, usize)> {
63        match self {
64            CelParserError::PestError(e) => match e.line_col {
65                pest::error::LineColLocation::Pos(pos) => Some(pos),
66                pest::error::LineColLocation::Span(start, _) => Some(start),
67            },
68            _ => None, // Location information is only available for Pest-level errors.
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    // We need to import the parser function to generate an error
76    use crate::parser::parse_cel_program;
77
78    #[test]
79    fn test_error_location_invalid_operator() {
80        // This expression has a syntax error. The parser should fail
81        // when it sees the '&' which is not a valid operator.
82        // Input: "a && b & c"
83        // Col:   123456789
84        let invalid_expr = "a && b & c";
85        let result = parse_cel_program(invalid_expr);
86
87        // Ensure it failed
88        assert!(result.is_err());
89
90        // Get the error
91        let err = result.unwrap_err();
92
93        // Check the location using the new helper
94        let location = err.location();
95        assert!(
96            location.is_some(),
97            "Expected location information for a parse error"
98        );
99
100        let (line, col) = location.unwrap();
101        assert_eq!(line, 1, "Error should be on line 1");
102        // The error is *on* the invalid token here, because it's unexpected.
103        assert_eq!(col, 8, "Error should be at column 8 (the '&')");
104    }
105
106    #[test]
107    fn test_error_location_on_incomplete_expr() {
108        // The error is at the end of the input because the expression is incomplete.
109        // Pest reports this at the EOI (End of Input) position.
110        // Input: "1 + " (length 4)
111        // Col:   12345
112        let invalid_expr = "1 + ";
113        let result = parse_cel_program(invalid_expr);
114        assert!(result.is_err());
115        let err = result.unwrap_err();
116        let (line, col) = err.location().expect("Should have location info");
117
118        assert_eq!(line, 1);
119        // --- FIX ---
120        // The error is reported at the EOI, which is one column *after* the last character.
121        assert_eq!(
122            col, 5,
123            "Error should be at the end of the expression (col 5)"
124        );
125    }
126
127    #[test]
128    fn test_error_location_multiline() {
129        // The error is on a different line. Pest correctly tracks newlines.
130        // The `!` is on line 3. The last line is " : !" (length 4).
131        // The error is reported at the EOI on that line.
132        let invalid_expr = "a ||\n  b ? c \n : !";
133        let result = parse_cel_program(invalid_expr);
134        assert!(result.is_err());
135        let err = result.unwrap_err();
136        let (line, col) = err.location().expect("Should have location info");
137
138        assert_eq!(line, 3, "Error should be on line 3");
139        // --- FIX ---
140        // The error is reported at the EOI for the incomplete unary op '!', which is column 5 on line 3.
141        assert_eq!(col, 5, "Error should be at EOI on line 3 (col 5)");
142    }
143}