Skip to main content

jjpwrgem_parse/
error.rs

1use core::{ops::Deref, range::Range};
2
3use displaydoc::Display;
4
5use crate::tokens::{
6    CharWithContext, ErrorToken, JsonCharOption, Token, TokenOption, TokenWithContext,
7    lexical::{JsonChar, trim_end_whitespace},
8};
9
10pub type Result<T> = std::result::Result<T, Error>;
11
12#[derive(Debug, PartialEq, Eq, Display, Clone)]
13pub enum ErrorKind {
14    // array/object
15    /// expected key, found {1}
16    ExpectedKey(TokenWithContext, TokenOption),
17    /// expected colon after key, found {1}
18    ExpectedColon(TokenWithContext, TokenOption),
19    /// expected json value, found {1}
20    ExpectedValue(Option<TokenWithContext>, TokenOption),
21    /// expected entry or closed delimiter `{expected}`, found {found}
22    ExpectedEntryOrClosedDelimiter {
23        open_ctx: TokenWithContext,
24        expected: JsonChar,
25        found: TokenOption,
26    },
27    /// expected comma or closed curly brace, found {found}
28    ExpectedCommaOrClosedCurlyBrace {
29        range: Range<usize>,
30        open_ctx: TokenWithContext,
31        found: TokenOption,
32    },
33    /// expected open brace `{expected}`, found {found}
34    ExpectedOpenBrace {
35        expected: JsonChar,
36        context: Option<TokenWithContext>,
37        found: TokenOption,
38    },
39
40    // mantissa
41    /// expected digit following minus sign, found {1}
42    ExpectedDigitFollowingMinus(Range<usize>, JsonCharOption),
43    /// expected '-' or digit to start number, found {0}
44    ExpectedMinusOrDigit(JsonCharOption),
45    /// unexpected leading zero
46    UnexpectedLeadingZero {
47        initial: Range<usize>,
48        extra: Range<usize>,
49    },
50    /// expected fraction digit following dot, found {maybe_c}
51    ExpectedDigitAfterDot {
52        number_range: Range<usize>,
53        dot_range: Range<usize>,
54        maybe_c: JsonCharOption,
55    },
56
57    // exponent
58    /// expected +/- or digit after exponent indicator, found {maybe_c}
59    ExpectedPlusOrMinusOrDigitAfterE {
60        e_range: Range<usize>,
61        maybe_c: JsonCharOption,
62    },
63    /// expected digit after exponent indicator, found {maybe_c}
64    ExpectedDigitAfterE {
65        exponent_range: Range<usize>,
66        maybe_c: JsonCharOption,
67    },
68
69    // string
70    /// unexpected unescaped control character `{0}` in string literal
71    UnexpectedControlCharacterInString(JsonChar),
72    /// expected closing quote
73    ExpectedQuote {
74        open_range: Range<usize>,
75        string_range: Range<usize>,
76    },
77    /// expected hex digit {digit_idx} of 4 in escape, found {maybe_c}
78    ExpectedHexDigit {
79        quote_range: Range<usize>,
80        slash_range: Range<usize>,
81        u_range: Range<usize>,
82        maybe_c: JsonCharOption,
83        digit_idx: usize,
84    },
85    /** expected escapable sequence, found {maybe_c}.
86    valid escapes are `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t` or `\uXXXX` (4 hex digits) */
87    ExpectedEscape {
88        maybe_c: JsonCharOption,
89        slash_range: Range<usize>,
90        string_range: Range<usize>,
91        quote_range: Range<usize>,
92    },
93
94    // misc
95    /// {_0}
96    InvalidEncoding(bytes2chars::ErrorKind),
97    /// unexpected character `{0}`. expected start of a json value
98    UnexpectedCharacter(JsonChar),
99    /// unexpected token {0} after json finished
100    TokenAfterEnd(ErrorToken),
101    /// json is nested too deeply (max depth: {0})
102    NestingTooDeep(usize),
103}
104
105impl ErrorKind {
106    pub(crate) fn expected_entry_or_closed_delimiter(
107        open_ctx: TokenWithContext,
108        found: TokenOption,
109    ) -> Option<Self> {
110        closing_delimiter_for_open(open_ctx.token).map(|expected| {
111            Self::ExpectedEntryOrClosedDelimiter {
112                open_ctx,
113                expected,
114                found,
115            }
116        })
117    }
118}
119
120fn closing_delimiter_for_open(token: Token) -> Option<JsonChar> {
121    match token {
122        Token::OpenCurlyBrace => Some('}'.into()),
123        Token::OpenSquareBracket => Some(']'.into()),
124        _ => None,
125    }
126}
127
128#[derive(Debug, PartialEq, Eq, Display, Clone)]
129// box inner error for performance--a Rust enum is as large as the largest
130// variant so happy path case becomes 100s of bytes otherwise
131/// {0}
132pub struct Error(pub(crate) Box<ErrorInner>);
133
134impl std::error::Error for Error {}
135
136#[derive(Debug, PartialEq, Eq, Display, Clone)]
137/// {kind}
138pub struct ErrorInner {
139    pub(crate) kind: ErrorKind,
140    pub(crate) range: Range<usize>,
141    pub(crate) source_text: String,
142    pub(crate) source_name: String,
143}
144
145impl From<ErrorInner> for Error {
146    fn from(value: ErrorInner) -> Self {
147        Error(Box::new(value))
148    }
149}
150
151impl Deref for Error {
152    type Target = ErrorInner;
153
154    fn deref(&self) -> &Self::Target {
155        &self.0
156    }
157}
158
159impl Error {
160    pub fn message(&self) -> String {
161        self.to_string()
162    }
163
164    pub fn source_text(&self) -> &str {
165        &self.source_text
166    }
167
168    /// byte range
169    pub fn range(&self) -> &Range<usize> {
170        &self.0.range
171    }
172
173    pub(crate) fn new(kind: ErrorKind, range: Range<usize>, text: &str) -> Self {
174        ErrorInner {
175            kind,
176            range,
177            source_text: text.into(),
178            source_name: "stdin".into(),
179        }
180        .into()
181    }
182
183    pub(crate) fn from_unterminated(kind: ErrorKind, text: &str) -> Self {
184        let trimmed = trim_end_whitespace(text);
185        let last_char_start = trimmed.char_indices().next_back().map_or(0, |(i, _)| i);
186        Self::new(kind, last_char_start..trimmed.len(), text)
187    }
188
189    /// # Panics
190    /// if bytes are valid at the location reported by [std::str::Utf8Error]
191    pub fn from_utf8_error_slice(e: std::str::Utf8Error, bytes: &[u8]) -> Error {
192        use bytes2chars::Utf8CharIndices;
193        const LOSSY_BYTE_LENGTH: usize = '\u{FFFD}'.len_utf8();
194        let b2c_err =
195            Utf8CharIndices::new(bytes[e.valid_up_to()..].iter().copied(), e.valid_up_to())
196                .next()
197                .and_then(|r| r.err())
198                .expect("a Utf8Error was returned so this must be an error");
199
200        ErrorInner {
201            kind: ErrorKind::InvalidEncoding(b2c_err.kind),
202            range: e.valid_up_to()..e.valid_up_to() + LOSSY_BYTE_LENGTH,
203            source_text: String::from_utf8_lossy(bytes).into_owned(),
204            source_name: "stdin".into(),
205        }
206        .into()
207    }
208
209    pub(crate) fn from_maybe_token_with_context<F>(
210        f: F,
211        maybe_token: Option<TokenWithContext>,
212        text: &str,
213    ) -> Self
214    where
215        F: Fn(TokenOption) -> ErrorKind,
216    {
217        if let Some(twc) = maybe_token {
218            Error::new(
219                f(TokenOption(Some(ErrorToken::new(
220                    twc.token, twc.range, text,
221                )))),
222                twc.range,
223                text,
224            )
225        } else {
226            Error::from_unterminated(f(None.into()), text)
227        }
228    }
229
230    pub(crate) fn from_maybe_json_char_with_context<F>(
231        f: F,
232        maybe_c: Option<CharWithContext>,
233        text: &str,
234    ) -> Self
235    where
236        F: Fn(JsonCharOption) -> ErrorKind,
237    {
238        if let Some(CharWithContext(r, c)) = maybe_c {
239            Error::new(f(Some(c).into()), r, text)
240        } else {
241            Error::from_unterminated(f(None.into()), text)
242        }
243    }
244}