serde_saphyr/error.rs
1//! Defines error and its location
2use std::fmt;
3
4use serde::de::{self};
5use saphyr_parser::{ScanError, Span};
6use crate::budget::BudgetBreach;
7
8/// Row/column location within the source YAML document (1-indexed).
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct Location {
11 /// 1-indexed row number in the input stream.
12 pub (crate) row: u32,
13 /// 1-indexed column number in the input stream.
14 pub (crate) column: u32,
15}
16
17impl Location {
18 /// serde-yaml compatible line information
19 pub fn line(&self) -> u64 { self.row as u64 }
20
21 /// serde-yaml compatible column information
22 pub fn column(&self) -> u64 { self.column as u64}
23}
24
25impl Location {
26 /// Sentinel value meaning "location unknown".
27 ///
28 /// Used when a precise position is not yet available at error creation time.
29 pub const UNKNOWN: Self = Self { row: 0, column: 0 };
30
31 /// Create a new location record.
32 ///
33 /// Arguments:
34 /// - `row`: 1-indexed row.
35 /// - `column`: 1-indexed column.
36 ///
37 /// Returns:
38 /// - `Location` with the provided coordinates.
39 ///
40 /// Called by:
41 /// - Parser/scan adapters that convert upstream spans to `Location`.
42 pub(crate) const fn new(row: usize, column: usize) -> Self {
43 // 4 Gb is larger than any YAML document I can imagine, and also this is
44 // error reporting only.
45 Self { row: row as u32, column: column as u32 }
46 }
47}
48
49/// Convert a `saphyr_parser::Span` to a 1-indexed `Location`.
50///
51/// Called by:
52/// - The live events adapter for each raw parser event.
53pub(crate) fn location_from_span(span: &Span) -> Location {
54 let start = &span.start;
55 Location::new(start.line(), start.col() + 1)
56}
57
58/// Error type compatible with `serde::de::Error`.
59#[derive(Debug)]
60pub enum Error {
61 /// Free-form error with optional source location.
62 Message {
63 msg: String,
64 location: Location,
65 },
66 /// Unexpected end of input.
67 Eof {
68 location: Location,
69 },
70 /// Structural/type mismatch — something else than the expected token/value was seen.
71 Unexpected {
72 expected: &'static str,
73 location: Location,
74 },
75 /// Alias references a non-existent anchor id.
76 UnknownAnchor {
77 id: usize,
78 location: Location,
79 },
80 HookError {
81 msg: String,
82 location: Location,
83 }
84}
85
86impl Error {
87 /// Construct a `Message` error with no known location.
88 ///
89 /// Arguments:
90 /// - `s`: human-readable message.
91 ///
92 /// Returns:
93 /// - `Error::Message` pointing at [`Location::UNKNOWN`].
94 ///
95 /// Called by:
96 /// - Scalar parsers and helpers throughout this module.
97 pub(crate) fn msg<S: Into<String>>(s: S) -> Self {
98 Error::Message {
99 msg: s.into(),
100 location: Location::UNKNOWN
101 }
102 }
103
104 /// Convenience for an `Unexpected` error pre-filled with a human phrase.
105 ///
106 /// Arguments:
107 /// - `what`: short description like "sequence start".
108 ///
109 /// Returns:
110 /// - `Error::Unexpected` at unknown location.
111 ///
112 /// Called by:
113 /// - Deserializer methods that validate the next event kind.
114 pub(crate) fn unexpected(what: &'static str) -> Self {
115 Error::Unexpected {
116 expected: what,
117 location: Location::UNKNOWN
118 }
119 }
120
121 /// Construct an unexpected end-of-input error with unknown location.
122 ///
123 /// Used by:
124 /// - Lookahead and pull methods when `None` appears prematurely.
125 pub(crate) fn eof() -> Self {
126 Error::Eof {
127 location: Location::UNKNOWN
128 }
129 }
130
131 /// Construct an `UnknownAnchor` error for the given anchor id (unknown location).
132 ///
133 /// Called by:
134 /// - Alias replay logic in the live event source.
135 pub(crate) fn unknown_anchor(id: usize) -> Self {
136 Error::UnknownAnchor {
137 id,
138 location: Location::UNKNOWN
139 }
140 }
141
142 /// Attach/override a concrete location to this error and return it.
143 ///
144 /// Arguments:
145 /// - `set_location`: location to store in the error.
146 ///
147 /// Returns:
148 /// - The same `Error` with location updated.
149 ///
150 /// Called by:
151 /// - Most error paths once the event position becomes known.
152 pub(crate) fn with_location(mut self, set_location: Location) -> Self {
153 match &mut self {
154 Error::Message { location, .. }
155 | Error::Eof { location }
156 | Error::Unexpected { location, .. }
157 | Error::HookError { location, .. }
158 | Error::UnknownAnchor { location, .. } => {
159 *location = set_location;
160 }
161 }
162 self
163 }
164
165 /// If the error has a known location, return it.
166 ///
167 /// Returns:
168 /// - `Some(Location)` when coordinates are known; `None` otherwise.
169 ///
170 /// Used by:
171 /// - Callers that want to surface precise positions to users.
172 pub fn location(&self) -> Option<Location> {
173 match self {
174 Error::Message { location, .. }
175 | Error::Eof { location }
176 | Error::Unexpected { location, .. }
177 | Error::HookError { location, .. }
178 | Error::UnknownAnchor { location, .. } => {
179 if location != &Location::UNKNOWN {
180 Some(*location)
181 } else {
182 None
183 }
184 }
185 }
186 }
187
188 /// Map a `saphyr_parser::ScanError` into our error type with location.
189 ///
190 /// Called by:
191 /// - The live events adapter when the underlying parser fails.
192 pub(crate) fn from_scan_error(err: ScanError) -> Self {
193 let mark = err.marker();
194 let location = Location::new(mark.line(), mark.col() + 1);
195 Error::Message {
196 msg: err.info().to_owned(),
197 location,
198 }
199 }
200}
201
202impl fmt::Display for Error {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 match self {
205 Error::Message { msg, location } => fmt_with_location(f, msg, location),
206 Error::HookError { msg, location } => fmt_with_location(f, msg, location),
207 Error::Eof { location } => fmt_with_location(f, "unexpected end of input", location),
208 Error::Unexpected { expected, location } => {
209 fmt_with_location(f, &format!("unexpected event: expected {expected}"), location)
210 }
211 Error::UnknownAnchor { id, location } => {
212 fmt_with_location(f, &format!("alias references unknown anchor id {id}"), location)
213 }
214 }
215 }
216}
217impl std::error::Error for Error {}
218impl de::Error for Error {
219 fn custom<T: fmt::Display>(msg: T) -> Self {
220 Error::msg(msg.to_string())
221 }
222}
223
224/// Print a message optionally suffixed with "at line X, column Y".
225///
226/// Arguments:
227/// - `f`: destination formatter.
228/// - `msg`: main text.
229/// - `location`: position to attach if known.
230///
231/// Returns:
232/// - `fmt::Result` as required by `Display`.
233fn fmt_with_location(f: &mut fmt::Formatter<'_>, msg: &str, location: &Location) -> fmt::Result {
234 if location != &Location::UNKNOWN {
235 write!(
236 f,
237 "{msg} at line {}, column {}",
238 location.row, location.column
239 )
240 } else {
241 write!(f, "{msg}")
242 }
243}
244
245/// Convert a budget breach report into a user-facing error.
246///
247/// Arguments:
248/// - `breach`: which limit was exceeded (from the streaming budget checker).
249///
250/// Returns:
251/// - `Error::Message` with a formatted description.
252///
253/// Called by:
254/// - The live events layer when enforcing budgets during/after parsing.
255pub(crate) fn budget_error(breach: BudgetBreach) -> Error {
256 Error::msg(format!("YAML budget breached: {breach:?}"))
257}