pub struct Spanned<T> {
pub value: T,
pub referenced: Location,
pub defined: Location,
}Expand description
A value paired with source locations describing where it came from. Spanned locations are specified in character positions and, when possible, in byte offsets as well. Byte offsets are available for string sources but not reader sources.
§Example
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Cfg {
timeout: serde_saphyr::Spanned<u64>,
}
let cfg: Cfg = serde_saphyr::from_str("timeout: 5\n").unwrap();
assert_eq!(cfg.timeout.value, 5);
assert_eq!(cfg.timeout.referenced.line(), 1);
assert_eq!(cfg.timeout.referenced.column(), 10);§Location semantics for YAML aliases and merges
Spanned<T> exposes two locations:
referenced: where the value is referenced/used in the YAML.- For aliases (
*a): this is the location of the alias token. - For merge-derived values (
<<): this is the location of the merge entry (typically the<<: *asite).
- For aliases (
defined: where the value is defined in YAML.- For plain values: equals
referenced. - For aliases: points to the anchored definition.
- For merge-derived values: points to the originating scalar in the merged mapping.
- For plain values: equals
§Limitation with #[serde(flatten)], #[serde(untagged)], and #[serde(tag = "...")]
When Spanned<T> is used inside a struct with #[serde(flatten)], or inside
variants of #[serde(untagged)] or #[serde(tag = "...")] enums, deserialization
succeeds but location information is lost: both referenced and defined
will be Location::UNKNOWN (line 0, column 0).
This is because serde buffers values through a generic ContentDeserializer in
these cases, which discards the YAML deserializer context needed to capture spans.
§Workaround for untagged/internally-tagged enums: Wrap the entire enum
Instead of putting Spanned<T> inside each variant, wrap the whole enum:
use serde::Deserialize;
use serde_saphyr::Spanned;
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Payload {
StringVariant { message: String },
IntVariant { count: u32 },
}
// Use Spanned<Payload> instead of Spanned<T> inside variants
let yaml = "message: hello";
let result: Spanned<Payload> = serde_saphyr::from_str(yaml).unwrap();
assert_eq!(result.referenced.line(), 1);§Alternative: Use externally tagged enums (serde default)
Externally tagged enums (the default) work with Spanned<T> inside variants:
use serde::Deserialize;
use serde_saphyr::Spanned;
#[derive(Debug, Deserialize)]
pub enum Payload {
StringVariant { message: Spanned<String> },
IntVariant { count: Spanned<u32> },
}
let yaml = "StringVariant:\n message: hello";
let result: Payload = serde_saphyr::from_str(yaml).unwrap();
match result {
Payload::StringVariant { message } => {
assert_eq!(&message.value, "hello");
assert_eq!(message.referenced.line(), 2);
}
_ => panic!("Expected StringVariant"),
}§Alternative: Use adjacently tagged enums
Adjacently tagged enums also work with Spanned<T> inside variants:
use serde::Deserialize;
use serde_saphyr::Spanned;
#[derive(Debug, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum Payload {
StringVariant { message: Spanned<String> },
IntVariant { count: Spanned<u32> },
}
let yaml = "type: StringVariant\ndata:\n message: hello";
let result: Payload = serde_saphyr::from_str(yaml).unwrap();
match result {
Payload::StringVariant { message } => {
assert_eq!(&message.value, "hello");
assert_eq!(message.referenced.line(), 3);
}
_ => panic!("Expected StringVariant"),
}Fields§
§value: T§referenced: Location§defined: Location