Skip to main content

serde_saphyr/
spanned.rs

1//! Span-aware wrapper types.
2//!
3//! `Spanned<T>` lets you deserialize a value together with the source location
4//! (line/column) of the YAML node it came from.
5//!
6//! This is especially useful for config validation errors, where you want to
7//! point at the exact place in the YAML. Many configuration errors are not
8//! "invalid YAML", but rather "valid YAML with an invalid value". Using
9//! `Spanned` shows where the invalid value came from.
10//!
11//! ```rust
12//! # #[cfg(feature = "deserialize")]
13//! # {
14//! use serde::Deserialize;
15//!
16//! #[derive(Debug, Deserialize)]
17//! struct Cfg {
18//!     timeout: serde_saphyr::Spanned<u64>,
19//! }
20//!
21//! let cfg: Cfg = serde_saphyr::from_str("timeout: 5\n").unwrap();
22//! assert_eq!(cfg.timeout.value, 5);
23//! assert_eq!(cfg.timeout.referenced.line(), 1);
24//! assert_eq!(cfg.timeout.referenced.column(), 10);
25//! # }
26//! ```
27
28use serde_core::de::{self, Deserializer, IntoDeserializer};
29use serde_core::{Deserialize, Serialize};
30
31use crate::Location;
32
33pub(crate) const INTERNAL_SPANNED_MARKER: &str = "__serde_saphyr_private_spanned";
34
35/// A value paired with source locations describing where it came from. Spanned locations
36/// are specified in character positions and, when possible, in byte offsets as well. Byte offsets
37/// are available for string sources but not reader sources.
38///
39/// # Example
40///
41/// ```rust
42/// # #[cfg(feature = "deserialize")]
43/// # {
44/// use serde::Deserialize;
45///
46/// #[derive(Debug, Deserialize)]
47/// struct Cfg {
48///     timeout: serde_saphyr::Spanned<u64>,
49/// }
50///
51/// let cfg: Cfg = serde_saphyr::from_str("timeout: 5\n").unwrap();
52/// assert_eq!(cfg.timeout.value, 5);
53/// assert_eq!(cfg.timeout.referenced.line(), 1);
54/// assert_eq!(cfg.timeout.referenced.column(), 10);
55/// # }
56/// ```
57///
58/// # Location semantics for YAML aliases and merges
59///
60/// `Spanned<T>` exposes two locations:
61///
62/// - `referenced`: where the value is referenced/used in the YAML.
63///   - For aliases (`*a`): this is the location of the alias token.
64///   - For merge-derived values (`<<`): this is the location of the merge entry
65///     (typically the `<<: *a` site).
66/// - `defined`: where the value is defined in YAML.
67///   - For plain values: equals `referenced`.
68///   - For aliases: points to the anchored definition.
69///   - For merge-derived values: points to the originating scalar in the merged
70///     mapping.
71///
72/// # Limitation with `#[serde(flatten)]`, `#[serde(untagged)]`, and `#[serde(tag = "...")]`
73///
74/// When `Spanned<T>` is used inside a struct with `#[serde(flatten)]`, or inside
75/// variants of `#[serde(untagged)]` or `#[serde(tag = "...")]` enums, deserialization
76/// **succeeds** but **location information is lost**: both `referenced` and `defined`
77/// will be `Location::UNKNOWN` (line 0, column 0).
78///
79/// This is because serde buffers values through a generic `ContentDeserializer` in
80/// these cases, which discards the YAML deserializer context needed to capture spans.
81///
82/// ## Workaround for untagged/internally-tagged enums: Wrap the entire enum
83///
84/// Instead of putting `Spanned<T>` inside each variant, wrap the whole enum:
85///
86/// ```rust
87/// # #[cfg(feature = "deserialize")]
88/// # {
89/// use serde::Deserialize;
90/// use serde_saphyr::Spanned;
91///
92/// #[derive(Debug, Deserialize)]
93/// #[serde(untagged)]
94/// pub enum Payload {
95///     StringVariant { message: String },
96///     IntVariant { count: u32 },
97/// }
98///
99/// // Use Spanned<Payload> instead of Spanned<T> inside variants
100/// let yaml = "message: hello";
101/// let result: Spanned<Payload> = serde_saphyr::from_str(yaml).unwrap();
102/// assert_eq!(result.referenced.line(), 1);
103/// # }
104/// ```
105///
106/// ## Alternative: Use externally tagged enums (serde default)
107///
108/// Externally tagged enums (the default) work with `Spanned<T>` inside variants:
109///
110/// ```rust
111/// # #[cfg(feature = "deserialize")]
112/// # {
113/// use serde::Deserialize;
114/// use serde_saphyr::Spanned;
115///
116/// #[derive(Debug, Deserialize)]
117/// pub enum Payload {
118///     StringVariant { message: Spanned<String> },
119///     IntVariant { count: Spanned<u32> },
120/// }
121///
122/// let yaml = "StringVariant:\n  message: hello";
123/// let result: Payload = serde_saphyr::from_str(yaml).unwrap();
124/// match result {
125///     Payload::StringVariant { message } => {
126///         assert_eq!(&message.value, "hello");
127///         assert_eq!(message.referenced.line(), 2);
128///     }
129///     _ => panic!("Expected StringVariant"),
130/// }
131/// # }
132/// ```
133///
134/// ## Alternative: Use adjacently tagged enums
135///
136/// Adjacently tagged enums also work with `Spanned<T>` inside variants:
137///
138/// ```rust
139/// # #[cfg(feature = "deserialize")]
140/// # {
141/// use serde::Deserialize;
142/// use serde_saphyr::Spanned;
143///
144/// #[derive(Debug, Deserialize)]
145/// #[serde(tag = "type", content = "data")]
146/// pub enum Payload {
147///     StringVariant { message: Spanned<String> },
148///     IntVariant { count: Spanned<u32> },
149/// }
150///
151/// let yaml = "type: StringVariant\ndata:\n  message: hello";
152/// let result: Payload = serde_saphyr::from_str(yaml).unwrap();
153/// match result {
154///     Payload::StringVariant { message } => {
155///         assert_eq!(&message.value, "hello");
156///         assert_eq!(message.referenced.line(), 3);
157///     }
158///     _ => panic!("Expected StringVariant"),
159/// }
160/// # }
161/// ```
162#[derive(Clone, Debug, PartialEq, Eq)]
163pub struct Spanned<T> {
164    pub value: T,
165    pub referenced: Location,
166    pub defined: Location,
167}
168
169impl<T> Spanned<T> {
170    #[must_use]
171    pub const fn new(value: T, referenced: Location, defined: Location) -> Self {
172        Self {
173            value,
174            referenced,
175            defined,
176        }
177    }
178}
179
180impl<'de, T> Deserialize<'de> for Spanned<T>
181where
182    T: Deserialize<'de>,
183{
184    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185    where
186        D: Deserializer<'de>,
187    {
188        struct SpannedVisitor<T>(std::marker::PhantomData<T>);
189
190        impl<'de, T> de::Visitor<'de> for SpannedVisitor<T>
191        where
192            T: Deserialize<'de>,
193        {
194            type Value = Spanned<T>;
195
196            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
197                f.write_str("a span-aware newtype wrapper")
198            }
199
200            fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
201            where
202                D: Deserializer<'de>,
203            {
204                // Call deserialize_any so that:
205                // - Our YAML SpannedDeser calls deserialize_struct → visit_map with the
206                //   synthesized {value, referenced, defined} map → full location info.
207                // - serde's ContentDeserializer (used by #[serde(flatten)]) calls
208                //   visit_map with the buffered Content::Map → ReprOrPlainVisitor::visit_map.
209                // - serde's ContentDeserializer with a plain scalar calls visit_u64/visit_str/
210                //   etc. → ReprOrPlainVisitor plain-value fallbacks with Location::UNKNOWN.
211                deserializer.deserialize_any(ReprOrPlainVisitor::<T>(std::marker::PhantomData))
212            }
213        }
214
215        /// Visitor that handles both the normal YAML path (`visit_map` with synthesized
216        /// {value, referenced, defined} fields) and the flattened/content path where
217        /// serde's `ContentDeserializer` calls visit_* with a plain or map value.
218        struct ReprOrPlainVisitor<T>(std::marker::PhantomData<T>);
219
220        impl<'de, T> de::Visitor<'de> for ReprOrPlainVisitor<T>
221        where
222            T: Deserialize<'de>,
223        {
224            type Value = Spanned<T>;
225
226            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
227                f.write_str("a value or a span-aware map with value/referenced/defined fields")
228            }
229
230            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
231            where
232                A: de::MapAccess<'de>,
233            {
234                struct PrependMapAccess<A> {
235                    first_key: Option<String>,
236                    tail: A,
237                }
238
239                impl<'de, A> de::MapAccess<'de> for PrependMapAccess<A>
240                where
241                    A: de::MapAccess<'de>,
242                {
243                    type Error = A::Error;
244
245                    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, A::Error>
246                    where
247                        K: de::DeserializeSeed<'de>,
248                    {
249                        if let Some(first_key) = self.first_key.take() {
250                            seed.deserialize(first_key.into_deserializer()).map(Some)
251                        } else {
252                            self.tail.next_key_seed(seed)
253                        }
254                    }
255
256                    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, A::Error>
257                    where
258                        V: de::DeserializeSeed<'de>,
259                    {
260                        self.tail.next_value_seed(seed)
261                    }
262                }
263
264                enum Field {
265                    Value,
266                    Referenced,
267                    Defined,
268                    Ignore,
269                }
270
271                impl<'de> Deserialize<'de> for Field {
272                    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
273                    where
274                        D: Deserializer<'de>,
275                    {
276                        struct FieldVisitor;
277
278                        impl de::Visitor<'_> for FieldVisitor {
279                            type Value = Field;
280
281                            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
282                                f.write_str("a Spanned<T> field")
283                            }
284
285                            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
286                            where
287                                E: de::Error,
288                            {
289                                Ok(match value {
290                                    "value" => Field::Value,
291                                    "referenced" => Field::Referenced,
292                                    "defined" => Field::Defined,
293                                    _ => Field::Ignore,
294                                })
295                            }
296                        }
297
298                        deserializer.deserialize_identifier(FieldVisitor)
299                    }
300                }
301
302                let mut map = map;
303                // The YAML deserializer marks the synthetic Spanned<T> representation.
304                // Unmarked maps are user values buffered by serde, such as through flatten.
305                let Some(first_key) = map.next_key::<String>()? else {
306                    return T::deserialize(de::value::MapAccessDeserializer::new(map))
307                        .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN));
308                };
309
310                if first_key != INTERNAL_SPANNED_MARKER {
311                    return T::deserialize(de::value::MapAccessDeserializer::new(
312                        PrependMapAccess {
313                            first_key: Some(first_key),
314                            tail: map,
315                        },
316                    ))
317                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN));
318                }
319
320                let _ = map.next_value::<de::IgnoredAny>()?;
321                let mut value = None;
322                let mut referenced = None;
323                let mut defined = None;
324
325                while let Some(field) = map.next_key::<Field>()? {
326                    match field {
327                        Field::Value => {
328                            if value.is_some() {
329                                return Err(de::Error::duplicate_field("value"));
330                            }
331                            value = Some(map.next_value()?);
332                        }
333                        Field::Referenced => {
334                            if referenced.is_some() {
335                                return Err(de::Error::duplicate_field("referenced"));
336                            }
337                            referenced = Some(map.next_value()?);
338                        }
339                        Field::Defined => {
340                            if defined.is_some() {
341                                return Err(de::Error::duplicate_field("defined"));
342                            }
343                            defined = Some(map.next_value()?);
344                        }
345                        Field::Ignore => {
346                            let _ = map.next_value::<de::IgnoredAny>()?;
347                        }
348                    }
349                }
350
351                let value = value.ok_or_else(|| de::Error::missing_field("value"))?;
352                let referenced =
353                    referenced.ok_or_else(|| de::Error::missing_field("referenced"))?;
354                let defined = defined.ok_or_else(|| de::Error::missing_field("defined"))?;
355
356                Ok(Spanned::new(value, referenced, defined))
357            }
358
359            // Fallback handlers for plain values arriving via ContentDeserializer
360            // when Spanned<T> is inside a #[serde(flatten)] struct.
361            // Location information is unavailable in this path; Location::UNKNOWN is used.
362
363            fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
364                T::deserialize(v.into_deserializer())
365                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
366            }
367            fn visit_i8<E: de::Error>(self, v: i8) -> Result<Self::Value, E> {
368                T::deserialize(v.into_deserializer())
369                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
370            }
371            fn visit_i16<E: de::Error>(self, v: i16) -> Result<Self::Value, E> {
372                T::deserialize(v.into_deserializer())
373                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
374            }
375            fn visit_i32<E: de::Error>(self, v: i32) -> Result<Self::Value, E> {
376                T::deserialize(v.into_deserializer())
377                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
378            }
379            fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
380                T::deserialize(v.into_deserializer())
381                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
382            }
383            fn visit_u8<E: de::Error>(self, v: u8) -> Result<Self::Value, E> {
384                T::deserialize(v.into_deserializer())
385                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
386            }
387            fn visit_u16<E: de::Error>(self, v: u16) -> Result<Self::Value, E> {
388                T::deserialize(v.into_deserializer())
389                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
390            }
391            fn visit_u32<E: de::Error>(self, v: u32) -> Result<Self::Value, E> {
392                T::deserialize(v.into_deserializer())
393                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
394            }
395            fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
396                T::deserialize(v.into_deserializer())
397                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
398            }
399            fn visit_f32<E: de::Error>(self, v: f32) -> Result<Self::Value, E> {
400                T::deserialize(v.into_deserializer())
401                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
402            }
403            fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
404                T::deserialize(v.into_deserializer())
405                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
406            }
407            fn visit_char<E: de::Error>(self, v: char) -> Result<Self::Value, E> {
408                T::deserialize(v.into_deserializer())
409                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
410            }
411            fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
412                T::deserialize(v.into_deserializer())
413                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
414            }
415            fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
416                T::deserialize(v.into_deserializer())
417                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
418            }
419            fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
420                T::deserialize(de::value::BytesDeserializer::new(v))
421                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
422            }
423            fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
424                T::deserialize(de::value::BytesDeserializer::new(&v))
425                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
426            }
427            fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
428                T::deserialize(().into_deserializer())
429                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
430            }
431            fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
432                T::deserialize(().into_deserializer())
433                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
434            }
435            fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
436            where
437                A: de::SeqAccess<'de>,
438            {
439                T::deserialize(de::value::SeqAccessDeserializer::new(seq))
440                    .map(|val| Spanned::new(val, Location::UNKNOWN, Location::UNKNOWN))
441            }
442        }
443
444        deserializer
445            .deserialize_newtype_struct("__yaml_spanned", SpannedVisitor(std::marker::PhantomData))
446    }
447}
448
449impl<T> Serialize for Spanned<T>
450where
451    T: Serialize,
452{
453    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
454    where
455        S: serde_core::Serializer,
456    {
457        // `Spanned<T>` is a deserialization helper that records source locations.
458        // When serializing, we emit the wrapped value only.
459        self.value.serialize(serializer)
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn deserialize_spanned_accepts_named_fields_and_ignores_unknown_fields() {
469        let spanned: Spanned<u32> = serde_json::from_str(
470            r#"{
471                "__serde_saphyr_private_spanned": true,
472                "unknown": true,
473                "value": 42,
474                "referenced": { "line": 1, "column": 2 },
475                "defined": { "line": 3, "column": 4 }
476            }"#,
477        )
478        .unwrap();
479
480        assert_eq!(spanned.value, 42);
481        assert_eq!(spanned.referenced.line(), 1);
482        assert_eq!(spanned.referenced.column(), 2);
483        assert_eq!(spanned.defined.line(), 3);
484        assert_eq!(spanned.defined.column(), 4);
485    }
486
487    #[test]
488    fn deserialize_spanned_rejects_duplicate_fields() {
489        let err = serde_json::from_str::<Spanned<u32>>(
490            r#"{
491                "__serde_saphyr_private_spanned": true,
492                "value": 1,
493                "value": 2,
494                "referenced": { "line": 1, "column": 2 },
495                "defined": { "line": 3, "column": 4 }
496            }"#,
497        )
498        .unwrap_err();
499
500        assert!(err.to_string().contains("duplicate field `value`"));
501    }
502
503    #[test]
504    fn deserialize_spanned_rejects_missing_fields() {
505        let err = serde_json::from_str::<Spanned<u32>>(
506            r#"{
507                "__serde_saphyr_private_spanned": true,
508                "value": 1,
509                "referenced": { "line": 1, "column": 2 }
510            }"#,
511        )
512        .unwrap_err();
513
514        assert!(err.to_string().contains("missing field `defined`"));
515    }
516}