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}