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}