Skip to main content

serde_saphyr/
location.rs

1//! Source location utilities.
2
3pub use crate::span::Span;
4#[cfg(feature = "deserialize")]
5use granit_parser::Span as ParserSpan;
6use serde_core::de::{self, IgnoredAny, MapAccess, Visitor};
7
8/// Row/column location within the source YAML document (1-indexed, character-based).
9///
10/// This type is used for both:
11/// - deserialization error reporting ([`crate::Error`])
12/// - span-aware values ([`crate::Spanned`])
13///
14/// # Example
15///
16/// ```
17/// # #[cfg(feature = "deserialize")]
18/// # {
19/// use serde::Deserialize;
20///
21/// #[derive(Deserialize, Debug)]
22/// struct Doc {
23///     val: String,
24/// }
25///
26/// // 1. Parse invalid YAML (type mismatch: expected string, found sequence)
27/// // Due emoji character and byte offsets are different.
28/// let yaml = "val🔑: [1, 2]";
29/// let err: Result<Doc, _> = serde_saphyr::from_str(yaml);
30///
31/// // 2. Obtain the error and its location
32/// if let Err(e) = err {
33///     if let Some(loc) = e.location() {
34///         // 3. Print row, column, and byte offsets
35///         // Output: Error at line 1, col 7. Byte offset: 9
36///         println!("Error at line {}, col {}", loc.line(), loc.column());
37///         if let Some(byte_off) = loc.span().byte_offset() {
38///             println!("Byte offset: {}", byte_off);
39///         }
40///     }
41/// }
42/// # }
43/// ```
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub struct Location {
46    /// 1-indexed row number in the input stream.
47    pub(crate) line: u32,
48    /// 1-indexed column number in the input stream.
49    pub(crate) column: u32,
50    /// Character-based span within the document
51    /// Byte offsets are available for string source but not from the reader.
52    pub(crate) span: Span,
53    /// Numeric id of the source where this location is from.
54    pub(crate) source_id: u32,
55}
56
57// As we do not use serde here (serde core only), we need manual implementation.
58impl<'de> serde_core::Deserialize<'de> for Location {
59    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60    where
61        D: serde_core::Deserializer<'de>,
62    {
63        enum Field {
64            Line,
65            Column,
66            Span,
67            SourceId,
68            Ignore,
69        }
70
71        impl<'de> serde_core::Deserialize<'de> for Field {
72            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
73            where
74                D: serde_core::Deserializer<'de>,
75            {
76                struct FieldVisitor;
77
78                impl Visitor<'_> for FieldVisitor {
79                    type Value = Field;
80
81                    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
82                        f.write_str("a Location field")
83                    }
84
85                    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
86                    where
87                        E: de::Error,
88                    {
89                        Ok(match value {
90                            "line" => Field::Line,
91                            "column" => Field::Column,
92                            "span" => Field::Span,
93                            "source_id" => Field::SourceId,
94                            _ => Field::Ignore,
95                        })
96                    }
97                }
98
99                deserializer.deserialize_identifier(FieldVisitor)
100            }
101        }
102
103        struct LocationVisitor;
104
105        impl<'de> Visitor<'de> for LocationVisitor {
106            type Value = Location;
107
108            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
109                f.write_str("a source location")
110            }
111
112            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
113            where
114                A: MapAccess<'de>,
115            {
116                let mut line = None;
117                let mut column = None;
118                let mut span = None;
119                let mut source_id = None;
120
121                while let Some(field) = map.next_key::<Field>()? {
122                    match field {
123                        Field::Line => {
124                            if line.is_some() {
125                                return Err(de::Error::duplicate_field("line"));
126                            }
127                            line = Some(map.next_value()?);
128                        }
129                        Field::Column => {
130                            if column.is_some() {
131                                return Err(de::Error::duplicate_field("column"));
132                            }
133                            column = Some(map.next_value()?);
134                        }
135                        Field::Span => {
136                            if span.is_some() {
137                                return Err(de::Error::duplicate_field("span"));
138                            }
139                            span = Some(map.next_value()?);
140                        }
141                        Field::SourceId => {
142                            if source_id.is_some() {
143                                return Err(de::Error::duplicate_field("source_id"));
144                            }
145                            source_id = Some(map.next_value()?);
146                        }
147                        Field::Ignore => {
148                            let _ = map.next_value::<IgnoredAny>()?;
149                        }
150                    }
151                }
152
153                let line = line.ok_or_else(|| de::Error::missing_field("line"))?;
154                let column = column.ok_or_else(|| de::Error::missing_field("column"))?;
155                let span = span.unwrap_or_default();
156                let source_id = source_id.unwrap_or_default();
157
158                Ok(Location {
159                    line,
160                    column,
161                    span,
162                    source_id,
163                })
164            }
165        }
166
167        const FIELDS: &[&str] = &["line", "column", "span", "source_id"];
168        deserializer.deserialize_struct("Location", FIELDS, LocationVisitor)
169    }
170}
171
172impl Location {
173    /// serde_yaml-compatible line information.
174    #[inline]
175    #[must_use]
176    pub fn line(&self) -> u64 {
177        u64::from(self.line)
178    }
179
180    /// serde_yaml-compatible column information.
181    #[inline]
182    #[must_use]
183    pub fn column(&self) -> u64 {
184        u64::from(self.column)
185    }
186
187    /// Character-based span within the source document.
188    /// For string source, it also can provide byte offsets.
189    #[inline]
190    #[must_use]
191    pub fn span(&self) -> Span {
192        self.span
193    }
194
195    /// Numeric id of the source.
196    #[inline]
197    #[must_use]
198    pub fn source_id(&self) -> u32 {
199        self.source_id
200    }
201}
202
203impl Location {
204    /// Sentinel value meaning "location unknown".
205    ///
206    /// Used when a precise position is not yet available at error creation time.
207    pub const UNKNOWN: Self = Self {
208        line: 0,
209        column: 0,
210        span: Span::UNKNOWN,
211        source_id: 0,
212    };
213
214    /// Create a new location record.
215    ///
216    /// Arguments:
217    /// - `line`: 1-indexed line.
218    /// - `column`: 1-indexed column.
219    #[cfg(feature = "deserialize")]
220    pub(crate) const fn new(line: usize, column: usize) -> Self {
221        Self {
222            line: if line > u32::MAX as usize {
223                u32::MAX
224            } else {
225                line as u32
226            },
227            column: if column > u32::MAX as usize {
228                u32::MAX
229            } else {
230                column as u32
231            },
232            span: Span::UNKNOWN,
233            source_id: 0,
234        }
235    }
236
237    #[cfg(feature = "deserialize")]
238    pub(crate) const fn with_span(mut self, span: Span) -> Self {
239        self.span = span;
240        self
241    }
242
243    #[cfg(feature = "deserialize")]
244    pub(crate) const fn with_source_id(mut self, source_id: u32) -> Self {
245        self.source_id = source_id;
246        self
247    }
248}
249
250/// Convert a `granit_parser::Span` to a 1-indexed [`Location`].
251///
252/// Called by:
253/// - The live events adapter for each raw parser event.
254///
255/// The resulting [`Location::span`] carries character offsets/lengths (not bytes),
256/// matching what the parser reports.
257#[cfg(feature = "deserialize")]
258pub(crate) fn location_from_span(span: &ParserSpan) -> Location {
259    let start = &span.start;
260    let end = &span.end;
261
262    let mut out = Span::new(start.index() as u64, span.len() as u64);
263
264    if let (Some(start_byte), Some(end_byte)) = (start.byte_offset(), end.byte_offset()) {
265        let byte_len = end_byte.saturating_sub(start_byte);
266
267        #[cfg(not(feature = "huge_documents"))]
268        {
269            // If byte offsets exceed 4 GiB on non-huge builds, mark byte info as unavailable.
270            if start_byte <= (u32::MAX as usize) && byte_len <= (u32::MAX as usize) {
271                out = out.with_byte_info(start_byte as u64, byte_len as u64);
272            }
273        }
274
275        #[cfg(feature = "huge_documents")]
276        {
277            out = out.with_byte_info(start_byte as u64, byte_len as u64);
278        }
279    }
280
281    Location::new(start.line(), start.col() + 1).with_span(out)
282}
283
284/// Pair of locations for values that may come indirectly from YAML anchors.
285///
286/// - `reference_location`: where the value is *used* (alias/merge site).
287/// - `defined_location`: where the value is *defined* (anchor definition site).
288#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
289pub struct Locations {
290    pub reference_location: Location,
291    pub defined_location: Location,
292}
293
294impl Locations {
295    #[cfg_attr(not(any(feature = "garde", feature = "validator")), allow(dead_code))]
296    pub(crate) const UNKNOWN: Locations = Locations {
297        reference_location: Location::UNKNOWN,
298        defined_location: Location::UNKNOWN,
299    };
300
301    #[inline]
302    #[cfg(feature = "deserialize")]
303    pub(crate) fn same(location: &Location) -> Option<Locations> {
304        if location == &Location::UNKNOWN {
305            None
306        } else {
307            Some(Locations {
308                reference_location: *location,
309                defined_location: *location,
310            })
311        }
312    }
313
314    #[inline]
315    #[must_use]
316    pub fn primary_location(self) -> Option<Location> {
317        if self.reference_location != Location::UNKNOWN {
318            Some(self.reference_location)
319        } else if self.defined_location != Location::UNKNOWN {
320            Some(self.defined_location)
321        } else {
322            None
323        }
324    }
325}
326
327#[cfg(all(test, feature = "deserialize"))]
328mod tests {
329    use super::*;
330    use granit_parser::{Event, Parser};
331
332    #[test]
333    fn test_location_from_span() {
334        let input = "foo";
335        let mut parser = Parser::new_from_str(input);
336
337        // StreamStart
338        parser.next().unwrap().unwrap();
339        // DocumentStart
340        parser.next().unwrap().unwrap();
341
342        // Scalar "foo"
343        let (event, parser_span) = parser.next().unwrap().unwrap();
344        assert!(matches!(event, Event::Scalar(..)));
345
346        let loc = location_from_span(&parser_span);
347
348        // "foo" starts at line 1, col 1
349        assert_eq!(loc.line(), 1);
350        assert_eq!(loc.column(), 1);
351
352        let span = loc.span();
353        assert_eq!(span.offset(), 0);
354        assert_eq!(span.len(), 3);
355        assert!(!span.is_empty());
356
357        // "foo" is 3 bytes
358        assert_eq!(span.byte_offset(), Some(0));
359        assert_eq!(span.byte_len(), Some(3));
360    }
361
362    #[test]
363    fn test_location_from_span_offset() {
364        let input = "  bar";
365        let mut parser = Parser::new_from_str(input);
366
367        parser.next().unwrap().unwrap();
368        parser.next().unwrap().unwrap();
369
370        let (event, parser_span) = parser.next().unwrap().unwrap();
371        assert!(matches!(event, Event::Scalar(..)));
372
373        let loc = location_from_span(&parser_span);
374
375        // "  bar" -> "bar" starts at line 1, col 3
376        // Wait, does granit-parser count columns 0-indexed or 1-indexed?
377        // location_from_span does `start.col() + 1`. So if parser is 0-indexed, loc is 1-indexed.
378        // col 0: ' ', col 1: ' ', col 2: 'b'.
379        // So start.col() should be 2. location.column() should be 3.
380        assert_eq!(loc.line(), 1);
381        assert_eq!(loc.column(), 3);
382
383        let span = loc.span();
384        assert_eq!(span.offset(), 2);
385        assert_eq!(span.len(), 3);
386    }
387
388    #[test]
389    fn test_span_methods() {
390        let span = Span::new(10, 5).with_byte_info(20, 5);
391
392        assert_eq!(span.offset(), 10);
393        assert_eq!(span.len(), 5);
394        assert!(!span.is_empty());
395        assert_eq!(span.byte_offset(), Some(20));
396        assert_eq!(span.byte_len(), Some(5));
397
398        let empty_span = Span::new(10, 0).with_byte_info(20, 0);
399        assert!(empty_span.is_empty());
400    }
401
402    #[test]
403    fn test_location_methods() {
404        let loc = Location::new(5, 10);
405        assert_eq!(loc.line(), 5);
406        assert_eq!(loc.column(), 10);
407        assert_eq!(loc.span(), Span::UNKNOWN);
408
409        let span = Span::new(100, 20).with_byte_info(100, 20);
410        let loc_with_span = loc.with_span(span);
411
412        assert_eq!(loc_with_span.line(), 5);
413        assert_eq!(loc_with_span.column(), 10);
414        assert_eq!(loc_with_span.span(), span);
415    }
416
417    #[test]
418    fn location_new_saturates_line_and_column() {
419        let loc = Location::new(usize::MAX, usize::MAX);
420
421        assert_eq!(loc.line(), u64::from(u32::MAX));
422        assert_eq!(loc.column(), u64::from(u32::MAX));
423    }
424
425    #[test]
426    fn deserialize_location_accepts_optional_and_unknown_fields() {
427        let loc: Location = serde_json::from_str(
428            r#"{
429                "ignored": true,
430                "line": 5,
431                "column": 10,
432                "span": { "offset": 20, "len": 3, "byte_info": [40, 3] },
433                "source_id": 7
434            }"#,
435        )
436        .unwrap();
437
438        assert_eq!(loc.line(), 5);
439        assert_eq!(loc.column(), 10);
440        assert_eq!(loc.span().offset(), 20);
441        assert_eq!(loc.span().len(), 3);
442        assert_eq!(loc.span().byte_offset(), Some(40));
443        assert_eq!(loc.span().byte_len(), Some(3));
444        assert_eq!(loc.source_id(), 7);
445    }
446
447    #[test]
448    fn deserialize_location_defaults_span_and_source_id() {
449        let loc: Location = serde_json::from_str(r#"{ "line": 1, "column": 2 }"#).unwrap();
450
451        assert_eq!(loc.line(), 1);
452        assert_eq!(loc.column(), 2);
453        assert_eq!(loc.span(), Span::default());
454        assert_eq!(loc.source_id(), 0);
455    }
456
457    #[test]
458    fn deserialize_location_rejects_duplicate_required_fields() {
459        let err = serde_json::from_str::<Location>(r#"{ "line": 1, "line": 2, "column": 3 }"#)
460            .unwrap_err();
461
462        assert!(err.to_string().contains("duplicate field `line`"));
463    }
464
465    #[test]
466    fn deserialize_location_rejects_duplicate_defaulted_fields() {
467        let duplicate_span = serde_json::from_str::<Location>(
468            r#"{
469                "line": 1,
470                "column": 2,
471                "span": { "offset": 1, "len": 1, "byte_info": [0, 0] },
472                "span": { "offset": 2, "len": 2, "byte_info": [0, 0] }
473            }"#,
474        )
475        .unwrap_err();
476        assert!(
477            duplicate_span
478                .to_string()
479                .contains("duplicate field `span`")
480        );
481
482        let duplicate_source_id = serde_json::from_str::<Location>(
483            r#"{ "line": 1, "column": 2, "source_id": 1, "source_id": 2 }"#,
484        )
485        .unwrap_err();
486        assert!(
487            duplicate_source_id
488                .to_string()
489                .contains("duplicate field `source_id`")
490        );
491    }
492
493    #[test]
494    fn deserialize_location_rejects_missing_required_fields() {
495        let err = serde_json::from_str::<Location>(r#"{ "line": 1 }"#).unwrap_err();
496
497        assert!(err.to_string().contains("missing field `column`"));
498    }
499
500    #[test]
501    fn test_locations_methods() {
502        let l1 = Location::new(1, 1);
503        let locations = Locations::same(&l1).unwrap();
504        assert_eq!(locations.reference_location, l1);
505        assert_eq!(locations.defined_location, l1);
506
507        assert_eq!(locations.primary_location(), Some(l1));
508
509        let l2 = Location::new(2, 2);
510        let locations_diff = Locations {
511            reference_location: l1,
512            defined_location: l2,
513        };
514        assert_eq!(locations_diff.primary_location(), Some(l1));
515
516        let locations_unknown = Locations {
517            reference_location: Location::UNKNOWN,
518            defined_location: l2,
519        };
520        assert_eq!(locations_unknown.primary_location(), Some(l2));
521
522        assert!(Locations::same(&Location::UNKNOWN).is_none());
523    }
524}