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 {}