Skip to main content

qubit_json/decode/
json_syntax_error.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Structured location-aware JSON lexical errors.
9
10use std::error::Error;
11use std::fmt;
12
13use super::JsonDecodeContext;
14use super::JsonSyntaxErrorReason;
15use crate::lexical::JsonLexicalFailure;
16
17/// A JSON syntax error with a zero-based byte offset and one-based source
18/// coordinates. Its column counts UTF-8 characters; CRLF is one line break,
19/// while LF and standalone CR each also start a new line.
20///
21/// # Examples
22///
23/// ```
24/// use qubit_json::decode::{JsonSyntaxError, JsonSyntaxErrorReason};
25///
26/// let error = JsonSyntaxError::new(
27///     0,
28///     1,
29///     1,
30///     JsonSyntaxErrorReason::UnexpectedEnd,
31/// );
32/// assert_eq!(error.line(), 1);
33/// assert_eq!(error.reason(), JsonSyntaxErrorReason::UnexpectedEnd);
34/// ```
35#[must_use]
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub struct JsonSyntaxError {
38    /// Zero-based byte offset at which the error was observed.
39    offset: usize,
40    /// One-based source line containing the error.
41    line: usize,
42    /// One-based UTF-8 character column containing the error.
43    column: usize,
44    /// Stable classification of the lexical failure.
45    reason: JsonSyntaxErrorReason,
46    /// Grammar location where the lexical failure occurred, when known.
47    context: Option<JsonDecodeContext>,
48}
49
50impl JsonSyntaxError {
51    /// Creates a syntax error from a source position and stable reason.
52    ///
53    /// # Parameters
54    ///
55    /// * `offset` - Zero-based byte offset at which the error was observed.
56    /// * `line` - One-based source line containing the error.
57    /// * `column` - One-based UTF-8 character column containing the error.
58    /// * `reason` - Stable classification of the lexical failure.
59    ///
60    /// # Returns
61    ///
62    /// A syntax error containing the supplied location and classification.
63    #[inline]
64    pub const fn new(
65        offset: usize,
66        line: usize,
67        column: usize,
68        reason: JsonSyntaxErrorReason,
69    ) -> Self {
70        Self {
71            offset,
72            line,
73            column,
74            reason,
75            context: None,
76        }
77    }
78
79    /// Converts a crate-private lexical failure into the text-domain syntax
80    /// error.
81    ///
82    /// The byte offset, line, and column are preserved, while the lexical
83    /// reason is mapped to its syntax-error classification.
84    ///
85    /// # Parameters
86    ///
87    /// * `failure` - The lexical failure whose location and reason are
88    ///   converted.
89    ///
90    /// # Returns
91    ///
92    /// A syntax error with the lexical failure's source coordinates and mapped
93    /// reason.
94    pub(crate) fn from_lexical(failure: JsonLexicalFailure) -> Self {
95        let mut error = Self::new(
96            failure.offset,
97            failure.line,
98            failure.column,
99            failure.reason.into(),
100        );
101        error.context = failure.context;
102        error
103    }
104
105    /// Returns the grammar location where the error occurred, when known.
106    #[must_use]
107    #[inline(always)]
108    pub const fn context(&self) -> Option<JsonDecodeContext> {
109        self.context
110    }
111
112    /// Returns the zero-based byte offset.
113    ///
114    /// # Returns
115    ///
116    /// The zero-based byte offset at which the error was observed.
117    #[must_use]
118    #[inline(always)]
119    pub const fn offset(&self) -> usize {
120        self.offset
121    }
122
123    /// Returns the one-based source line.
124    ///
125    /// # Returns
126    ///
127    /// The one-based source line containing the error.
128    #[must_use]
129    #[inline(always)]
130    pub const fn line(&self) -> usize {
131        self.line
132    }
133
134    /// Returns the one-based UTF-8 character column.
135    ///
136    /// # Returns
137    ///
138    /// The one-based UTF-8 character column containing the error.
139    #[must_use]
140    #[inline(always)]
141    pub const fn column(&self) -> usize {
142        self.column
143    }
144
145    /// Returns the stable syntax classification.
146    ///
147    /// # Returns
148    ///
149    /// The stable lexical failure classification.
150    #[inline(always)]
151    pub const fn reason(&self) -> JsonSyntaxErrorReason {
152        self.reason
153    }
154
155    /// Returns this error with the supplied grammar context.
156    ///
157    /// # Parameters
158    ///
159    /// * `context` - Grammar location associated with this syntax error.
160    #[must_use = "the updated syntax error must be used"]
161    #[inline(always)]
162    pub const fn with_context(mut self, context: JsonDecodeContext) -> Self {
163        self.context = Some(context);
164        self
165    }
166}
167
168impl fmt::Display for JsonSyntaxError {
169    /// Formats the reason and complete source location for human-readable
170    /// diagnostics.
171    ///
172    /// # Parameters
173    ///
174    /// * `formatter` - Destination that receives the human-readable diagnostic.
175    ///
176    /// # Errors
177    ///
178    /// Returns the formatter's error if writing the diagnostic to its output
179    /// fails.
180    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
181        write!(
182            formatter,
183            "{} at line {} column {} (byte offset {})",
184            self.reason, self.line, self.column, self.offset,
185        )
186    }
187}
188
189impl Error for JsonSyntaxError {}