yaml_edit/anchor_resolution.rs
1//! Anchor and alias resolution for semantic YAML operations
2//!
3//! This module provides functionality to resolve YAML anchors and aliases,
4//! enabling semantic lookups while preserving the lossless nature of the
5//! syntax tree.
6//!
7//! # Design Philosophy
8//!
9//! - **Opt-in**: Resolution is explicit via `get_resolved()` methods
10//! - **Preserves structure**: Original syntax tree remains unchanged
11//! - **Lazy evaluation**: Anchors are resolved on-demand, not eagerly
12//! - **Error handling**: An undefined or recursive alias, a merge key
13//! whose operand is not a mapping, a key repeated within a mapping,
14//! distinct keys that would collide in a resolved mapping, or `!!set`, `!!omap` and `!!pairs` content
15//! that [`YamlValue`] cannot hold are reported as a [`ResolveError`]
16//!
17//! # Examples
18//!
19//! ```
20//! use yaml_edit::Document;
21//! use yaml_edit::anchor_resolution::DocumentResolvedExt;
22//! use std::str::FromStr;
23//!
24//! let yaml = r#"
25//! defaults: &defaults
26//! timeout: 30
27//! retries: 3
28//!
29//! production:
30//! <<: *defaults
31//! host: prod.example.com
32//! "#;
33//!
34//! let doc = Document::from_str(yaml).unwrap();
35//!
36//! // Regular get() doesn't resolve - returns the alias syntax
37//! // get_resolved() expands aliases and merge keys
38//! if let Some(resolved_value) = doc.get_resolved("production").unwrap() {
39//! if let Some(prod) = resolved_value.as_mapping() {
40//! // This will find 'timeout: 30' from the merged defaults
41//! if let Some(timeout) = prod.get("timeout") {
42//! assert_eq!(timeout.to_i64(), Some(30));
43//! }
44//! // This will find the overridden value
45//! if let Some(host) = prod.get("host") {
46//! assert_eq!(host.as_scalar().map(|s| s.to_string()).as_deref(), Some("prod.example.com"));
47//! }
48//! }
49//! }
50//! ```
51
52use crate::as_yaml::{key_identity, AsYaml, NodeIdentity};
53use crate::lex::SyntaxKind;
54use crate::value::YamlValue;
55use crate::yaml::SyntaxNode;
56use rowan::ast::AstNode;
57use std::collections::HashMap;
58
59/// An error resolving aliases or merge keys, or converting a node to a
60/// [`YamlValue`] that cannot hold it.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum ResolveError {
63 /// An alias refers to an anchor that is not defined before it in the
64 /// document.
65 UndefinedAlias {
66 /// The anchor name the alias references, without the `*`.
67 alias: String,
68 },
69 /// An alias refers to an anchor it is nested in, so expanding it
70 /// would not terminate.
71 RecursiveAlias {
72 /// The anchor name the alias references, without the `*`.
73 alias: String,
74 },
75 /// A merge key (`<<`) operand is neither a mapping nor an alias to
76 /// one.
77 InvalidMergeOperand {
78 /// The source text of the offending operand.
79 operand: String,
80 },
81 /// A mapping has two equal keys, such as `a` and `'a'`, or a key and
82 /// an alias to an equal node, so it cannot keep both values. A direct
83 /// key shadowing a key from a merge key (`<<`) is not a duplicate. The
84 /// members of a `!!set` and the keys of a `!!omap` have to be distinct
85 /// too.
86 DuplicateKey {
87 /// The source text of the key that was seen first.
88 first: String,
89 /// The source text of the key that repeats it.
90 second: String,
91 },
92 /// Two distinct keys of a mapping have the same string form, such as
93 /// the int `1` and the string `'1'`, so they cannot both be kept in a
94 /// [`YamlValue::Mapping`]. The same goes for the members of a
95 /// [`YamlValue::Set`] and the keys of a [`YamlValue::OrderedMapping`].
96 KeyCollision {
97 /// The source text of the key that was seen first.
98 first: String,
99 /// The source text of the key that collides with it.
100 second: String,
101 },
102 /// The content of a `!!set` is not a mapping, so it has no members.
103 InvalidSetContent {
104 /// The source text of the content.
105 content: String,
106 },
107 /// A `!!set` member is not a scalar, so it cannot be kept in a
108 /// [`YamlValue::Set`].
109 InvalidSetMember {
110 /// The source text of the member.
111 member: String,
112 },
113 /// A `!!set` member has a value other than null, which a
114 /// [`YamlValue::Set`] cannot keep.
115 NonNullSetValue {
116 /// The source text of the member.
117 member: String,
118 /// The source text of its value.
119 value: String,
120 },
121 /// An item of a `!!omap` or `!!pairs` sequence is not a mapping with a
122 /// single entry.
123 InvalidPairsItem {
124 /// The tag of the sequence, `!!omap` or `!!pairs`.
125 tag: String,
126 /// The source text of the item.
127 item: String,
128 },
129 /// The content of a `!!omap` or `!!pairs` is not a sequence, so it has
130 /// no entries.
131 InvalidPairsContent {
132 /// The tag, `!!omap` or `!!pairs`.
133 tag: String,
134 /// The source text of the content.
135 content: String,
136 },
137 /// [`YamlValue::cast`] met an alias, which it cannot resolve without
138 /// the document's anchors. [`YamlValue::from_document`] and
139 /// [`DocumentResolvedExt::get_resolved`] resolve aliases.
140 UnresolvedAlias {
141 /// The anchor name the alias references, without the `*`.
142 alias: String,
143 },
144 /// A document, key, value, sequence entry or tagged node holds more
145 /// than one node, as in `k: [q] x`, which YAML does not allow but the
146 /// parser accepts. A [`YamlValue`] can only keep one of them.
147 ExtraNode {
148 /// The source text of the node after the first.
149 node: String,
150 },
151}
152
153impl std::fmt::Display for ResolveError {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 ResolveError::UndefinedAlias { alias } => {
157 write!(f, "alias to undefined anchor {alias:?}")
158 }
159 ResolveError::RecursiveAlias { alias } => {
160 write!(f, "alias {alias:?} refers to an anchor it is nested in")
161 }
162 ResolveError::InvalidMergeOperand { operand } => {
163 write!(f, "merge key operand {operand:?} is not a mapping")
164 }
165 ResolveError::KeyCollision { first, second } => {
166 write!(
167 f,
168 "distinct keys {first:?} and {second:?} have the same string form"
169 )
170 }
171 ResolveError::DuplicateKey { first, second } => {
172 write!(f, "duplicate mapping key: {second:?} equals {first:?}")
173 }
174 ResolveError::InvalidSetContent { content } => {
175 write!(f, "!!set content {content:?} is not a mapping")
176 }
177 ResolveError::InvalidSetMember { member } => {
178 write!(f, "!!set member {member:?} is not a scalar")
179 }
180 ResolveError::NonNullSetValue { member, value } => {
181 write!(f, "!!set member {member:?} has non-null value {value:?}")
182 }
183 ResolveError::InvalidPairsItem { tag, item } => {
184 write!(
185 f,
186 "{tag} item {item:?} is not a mapping with a single entry"
187 )
188 }
189 ResolveError::InvalidPairsContent { tag, content } => {
190 write!(f, "{tag} content {content:?} is not a sequence")
191 }
192 ResolveError::UnresolvedAlias { alias } => {
193 write!(
194 f,
195 "alias {alias:?} cannot be converted without resolving it; \
196 use get_resolved to resolve aliases"
197 )
198 }
199 ResolveError::ExtraNode { node } => {
200 write!(f, "node {node:?} follows another in the same place")
201 }
202 }
203 }
204}
205
206impl std::error::Error for ResolveError {}
207
208/// An alias that does not refer to a node.
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub(crate) enum AliasError {
211 /// No anchor of the alias's name is defined before it.
212 Undefined { alias: String },
213 /// The alias refers to an anchor it is nested in.
214 Recursive { alias: String },
215}
216
217impl From<AliasError> for ResolveError {
218 fn from(err: AliasError) -> Self {
219 match err {
220 AliasError::Undefined { alias } => ResolveError::UndefinedAlias { alias },
221 AliasError::Recursive { alias } => ResolveError::RecursiveAlias { alias },
222 }
223 }
224}
225
226/// An error collecting the entries of a [`MergedMapping`].
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub(crate) enum MergeError {
229 Alias(AliasError),
230 /// A merge key operand is not a mapping; holds its source text.
231 InvalidOperand {
232 operand: String,
233 },
234}
235
236impl From<AliasError> for MergeError {
237 fn from(err: AliasError) -> Self {
238 MergeError::Alias(err)
239 }
240}
241
242impl From<MergeError> for ResolveError {
243 fn from(err: MergeError) -> Self {
244 match err {
245 MergeError::Alias(err) => err.into(),
246 MergeError::InvalidOperand { operand } => ResolveError::InvalidMergeOperand { operand },
247 }
248 }
249}
250
251/// A registry of anchors defined in a YAML document
252#[derive(Debug, Clone)]
253pub struct AnchorRegistry {
254 /// Map from anchor name to every definition of it, in document order,
255 /// as (offset of the `&name` token, anchored node)
256 anchors: HashMap<String, Vec<(rowan::TextSize, SyntaxNode)>>,
257}
258
259impl AnchorRegistry {
260 /// Create a new empty anchor registry
261 pub fn new() -> Self {
262 Self {
263 anchors: HashMap::new(),
264 }
265 }
266
267 /// Build a registry from a Document
268 pub fn from_document(doc: &crate::yaml::Document) -> Self {
269 if let Some(node) = doc.as_node() {
270 Self::from_tree(node)
271 } else {
272 Self::new()
273 }
274 }
275
276 /// Build a registry by scanning a syntax tree
277 pub fn from_tree(root: &SyntaxNode) -> Self {
278 let mut registry = Self::new();
279 registry.collect_anchors_from_tree(root);
280 registry
281 }
282
283 /// Recursively collect all anchors from a syntax tree
284 fn collect_anchors_from_tree(&mut self, node: &SyntaxNode) {
285 // Look for ANCHOR tokens in this node
286 for child in node.children_with_tokens() {
287 if let Some(token) = child.as_token() {
288 if token.kind() == SyntaxKind::ANCHOR {
289 // Extract anchor name (remove '&' prefix)
290 let text = token.text();
291 if let Some(name) = text.strip_prefix('&') {
292 // Find the associated value node
293 if let Some(value_node) = self.find_anchored_value(node) {
294 self.anchors
295 .entry(name.to_string())
296 .or_default()
297 .push((token.text_range().start(), value_node));
298 }
299 }
300 }
301 } else if let Some(child_node) = child.as_node() {
302 // Recurse into child nodes
303 self.collect_anchors_from_tree(child_node);
304 }
305 }
306 }
307
308 /// Find the value node that an anchor refers to (low-level helper)
309 fn find_anchored_value(&self, node: &SyntaxNode) -> Option<SyntaxNode> {
310 // Node properties come in either order. In `!!map &x` the anchor
311 // sits inside the TAGGED_NODE and anchors it, tag included.
312 if node.kind() == SyntaxKind::TAGGED_NODE {
313 return Some(node.clone());
314 }
315 // The anchored value is typically a sibling MAPPING/SEQUENCE/SCALAR node
316 for child in node.children() {
317 if matches!(
318 child.kind(),
319 SyntaxKind::VALUE
320 | SyntaxKind::SCALAR
321 | SyntaxKind::MAPPING
322 | SyntaxKind::SEQUENCE
323 | SyntaxKind::TAGGED_NODE
324 | SyntaxKind::ALIAS
325 ) {
326 return Some(child);
327 }
328 }
329 None
330 }
331
332 /// Look up an anchor by name
333 ///
334 /// If the anchor is defined more than once, this returns the last
335 /// definition in the document.
336 pub fn resolve(&self, name: &str) -> Option<&SyntaxNode> {
337 self.anchors.get(name)?.last().map(|(_, node)| node)
338 }
339
340 /// Look up the node that an alias at `alias` refers to.
341 ///
342 /// An alias refers to the most recent definition of its anchor that
343 /// precedes it in the document, so a redefined anchor only affects the
344 /// aliases that follow the redefinition. `alias` must belong to the same
345 /// tree this registry was built from.
346 pub(crate) fn resolve_alias(&self, name: &str, alias: &SyntaxNode) -> Option<&SyntaxNode> {
347 let position = alias.text_range().start();
348 self.anchors
349 .get(name)?
350 .iter()
351 .rev()
352 .find(|(anchor, _)| *anchor < position)
353 .map(|(_, node)| node)
354 }
355
356 /// Look up the node that `alias` refers to like
357 /// [`resolve_alias`](Self::resolve_alias), following anchors that are
358 /// on aliases themselves, as in `&b *a`, to the node they end at.
359 pub(crate) fn resolve_alias_through(
360 &self,
361 alias: &crate::yaml::Alias,
362 ) -> Result<&SyntaxNode, AliasError> {
363 let mut alias = alias.clone();
364 let mut seen = Vec::new();
365 loop {
366 let name = alias.name();
367 let target =
368 self.resolve_alias(&name, alias.syntax())
369 .ok_or_else(|| AliasError::Undefined {
370 alias: name.clone(),
371 })?;
372 let content = if target.kind() == SyntaxKind::VALUE {
373 target.children().next()
374 } else {
375 Some(target.clone())
376 };
377 let Some(next) = content.and_then(crate::yaml::Alias::cast) else {
378 return Ok(target);
379 };
380 if seen.contains(target) {
381 return Err(AliasError::Recursive { alias: name });
382 }
383 seen.push(target.clone());
384 alias = next;
385 }
386 }
387
388 /// Check if an anchor is defined
389 pub fn contains(&self, name: &str) -> bool {
390 self.anchors.contains_key(name)
391 }
392
393 /// Get all anchor names
394 pub fn anchor_names(&self) -> impl Iterator<Item = &str> {
395 self.anchors.keys().map(|s| s.as_str())
396 }
397}
398
399impl Default for AnchorRegistry {
400 fn default() -> Self {
401 Self::new()
402 }
403}
404
405/// Extension trait for Document to support resolved lookups
406pub trait DocumentResolvedExt {
407 /// Get a value with anchor/alias resolution
408 ///
409 /// This method resolves aliases (*alias) to their anchored values (&anchor),
410 /// and supports merge keys (<<) for combining mappings.
411 ///
412 /// Returns `Ok(None)` if the document root is not a mapping or has no
413 /// such key, and an error if the value contains an undefined or
414 /// recursive alias, an invalid merge key, a mapping with the same key
415 /// twice, or distinct keys that have the same string form, such as `1`
416 /// and `'1'`.
417 ///
418 /// Resolution is read-only. To write a merge or alias back, pass a mapping
419 /// that already contains `<<: *name` (or an alias node) to
420 /// [`Mapping::set`](crate::Mapping::set). `set` does not infer a merge
421 /// from a resolved object.
422 ///
423 /// # Examples
424 ///
425 /// ```
426 /// use yaml_edit::Document;
427 /// use yaml_edit::anchor_resolution::DocumentResolvedExt;
428 /// use std::str::FromStr;
429 ///
430 /// let yaml = r#"
431 /// config: &cfg
432 /// port: 8080
433 /// server: *cfg
434 /// "#;
435 /// let doc = Document::from_str(yaml).unwrap();
436 ///
437 /// // Get with resolution - expands the *cfg alias
438 /// if let Some(resolved_value) = doc.get_resolved("server").unwrap() {
439 /// if let Some(server) = resolved_value.as_mapping() {
440 /// if let Some(port) = server.get("port") {
441 /// assert_eq!(port.to_i64(), Some(8080));
442 /// }
443 /// }
444 /// }
445 /// ```
446 fn get_resolved(&self, key: impl crate::AsYaml) -> Result<Option<YamlValue>, ResolveError>;
447
448 /// Build the anchor registry for this document
449 fn build_anchor_registry(&self) -> AnchorRegistry;
450}
451
452impl DocumentResolvedExt for crate::yaml::Document {
453 fn get_resolved(&self, key: impl crate::AsYaml) -> Result<Option<YamlValue>, ResolveError> {
454 let Some(node) = self.as_mapping().and_then(|m| m.get(&key)) else {
455 return Ok(None);
456 };
457 let registry = self.build_anchor_registry();
458 crate::value::convert::resolved_value(node, Some(®istry), &mut Vec::new())
459 }
460
461 fn build_anchor_registry(&self) -> AnchorRegistry {
462 AnchorRegistry::from_document(self)
463 }
464}
465
466/// Whether `key` is the merge key. Only a plain `<<` is one; a quoted
467/// `'<<'` is an ordinary string.
468pub(crate) fn is_merge_key(key: &crate::as_yaml::YamlNode) -> bool {
469 key.as_scalar().is_some_and(|s| s.value() == "<<")
470}
471
472/// A view over a [`Mapping`](crate::yaml::Mapping) that transparently expands
473/// alias references (`*name`) and merge keys (`<<:`).
474///
475/// `MergedMapping` borrows an [`AnchorRegistry`] and collects its entries
476/// when it is created: the merge sources are followed and alias values are
477/// resolved then, so an undefined alias or invalid merge key is reported
478/// by [`new`](Self::new). The underlying syntax tree is not modified and
479/// the returned nodes are live CST nodes, but entries added, removed or
480/// replaced after the view was created are not seen.
481///
482/// Direct keys in the base mapping always shadow keys contributed by merge
483/// sources, matching the YAML 1.1 merge-key semantics. When a merge value is
484/// a sequence of aliases (`<<: [*a, *b]`), earlier aliases take precedence
485/// over later ones, again per the YAML 1.1 spec.
486///
487/// Returned values are [`YamlNode`](crate::as_yaml::YamlNode)s backed by real
488/// CST nodes, so they preserve the original formatting and quoting style.
489///
490/// This view is read-only. [`Mapping::set`](crate::Mapping::set) replaces the
491/// value node you pass; it does not turn a resolved map back into `<<: *name`.
492///
493/// # Examples
494///
495/// ```
496/// use yaml_edit::{Document, anchor_resolution::{DocumentResolvedExt, MappingMergedExt}};
497/// use std::str::FromStr;
498///
499/// let yaml = r#"
500/// defaults: &defaults
501/// timeout: 30
502/// retries: 3
503///
504/// production:
505/// <<: *defaults
506/// host: prod.example.com
507/// timeout: 60
508/// "#;
509///
510/// let doc = Document::from_str(yaml).unwrap();
511/// let root = doc.as_mapping().unwrap();
512/// let registry = doc.build_anchor_registry();
513///
514/// let prod = root.get_mapping("production").unwrap();
515/// let merged = prod.merged(®istry).unwrap();
516///
517/// // Direct key wins over merged value.
518/// assert_eq!(merged.get("timeout").unwrap().to_i64(), Some(60));
519/// // Merged key from defaults is visible.
520/// assert_eq!(merged.get("retries").unwrap().to_i64(), Some(3));
521/// // Direct-only key is visible.
522/// assert!(merged.get("host").is_some());
523/// // `<<` itself is hidden.
524/// assert!(merged.get("<<").is_none());
525/// ```
526#[derive(Clone)]
527pub struct MergedMapping<'a> {
528 base: crate::yaml::Mapping,
529 registry: &'a AnchorRegistry,
530 entries: Vec<MergedEntry>,
531}
532
533impl<'a> MergedMapping<'a> {
534 /// Create a new merged view over `base`, resolving aliases against `registry`.
535 ///
536 /// Fails if a `<<` operand in `base`, or in a mapping merged into it,
537 /// is an undefined alias or not a mapping, if a value visible
538 /// through the view is an undefined alias, or if a key is or holds an
539 /// undefined or recursive alias.
540 pub fn new(
541 base: crate::yaml::Mapping,
542 registry: &'a AnchorRegistry,
543 ) -> Result<Self, ResolveError> {
544 Ok(Self::build(base, registry)?)
545 }
546
547 /// Like [`new`](Self::new), failing with the errors only a merged view
548 /// can meet.
549 pub(crate) fn build(
550 base: crate::yaml::Mapping,
551 registry: &'a AnchorRegistry,
552 ) -> Result<Self, MergeError> {
553 let entries = merged_entries(&base, registry)?;
554 Ok(Self {
555 base,
556 registry,
557 entries,
558 })
559 }
560
561 /// Return the underlying (un-merged) [`Mapping`](crate::yaml::Mapping).
562 ///
563 /// Useful when you want to mutate the original mapping or read its raw
564 /// (non-resolved) contents.
565 pub fn base(&self) -> &crate::yaml::Mapping {
566 &self.base
567 }
568
569 /// Return the [`AnchorRegistry`] this view resolves against.
570 pub fn registry(&self) -> &AnchorRegistry {
571 self.registry
572 }
573
574 /// Get the value associated with `key`, resolving aliases and merge keys.
575 ///
576 /// Direct entries in the base mapping take precedence over keys
577 /// contributed by `<<:` merge sources. The merge key itself is hidden:
578 /// looking up `"<<"` only finds a quoted `'<<'` key, which is an ordinary
579 /// string.
580 ///
581 /// If the matched value is an alias (`*name`), the resolved target node
582 /// is returned.
583 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
584 // A key that cannot be resolved, such as an undefined alias, is in
585 // no mapping.
586 let identity = crate::as_yaml::identity_of(&key, Some(self.registry))
587 .ok()
588 .flatten()?;
589 self.entries
590 .iter()
591 .find(|(k, _, _)| *k == identity)
592 .map(|(_, _, v)| v.clone())
593 }
594
595 /// Returns `true` if a value would be returned by [`get`](Self::get) for
596 /// `key`.
597 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
598 self.get(key).is_some()
599 }
600
601 /// Iterate over `(key, value)` pairs in the merged view.
602 ///
603 /// Direct entries appear first, in their original document order.
604 /// Merged-in entries follow, with duplicates (and any direct-key matches
605 /// already yielded) filtered out. Keys are compared as YAML values, so
606 /// the integer `1` and the string `'1'` are distinct. The `<<` merge key
607 /// itself is never yielded.
608 pub fn iter(
609 &self,
610 ) -> impl Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + '_ {
611 self.entries.iter().map(|(_, k, v)| (k.clone(), v.clone()))
612 }
613
614 /// Iterate over the keys of the merged view.
615 pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
616 self.iter().map(|(k, _)| k)
617 }
618
619 /// Iterate over the values of the merged view.
620 pub fn values(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
621 self.iter().map(|(_, v)| v)
622 }
623
624 /// Number of distinct keys visible through the merged view.
625 pub fn len(&self) -> usize {
626 self.iter().count()
627 }
628
629 /// Returns `true` if the merged view has no entries.
630 pub fn is_empty(&self) -> bool {
631 self.iter().next().is_none()
632 }
633
634 /// Get a nested mapping by `key` and return it as another `MergedMapping`
635 /// that shares the same registry.
636 ///
637 /// Returns `Ok(None)` if the key does not exist or the value is not a
638 /// mapping, and fails as [`new`](Self::new) does on the nested mapping.
639 pub fn get_merged(
640 &self,
641 key: impl crate::AsYaml,
642 ) -> Result<Option<MergedMapping<'a>>, ResolveError> {
643 let Some(mapping) = self.get(key).and_then(|node| node.as_mapping().cloned()) else {
644 return Ok(None);
645 };
646 MergedMapping::new(mapping, self.registry).map(Some)
647 }
648}
649
650impl std::fmt::Debug for MergedMapping<'_> {
651 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
652 f.debug_struct("MergedMapping")
653 .field("len", &self.len())
654 .finish()
655 }
656}
657
658/// Walk the `<<:` entries of `base` and return the resolved source mappings,
659/// in precedence order (highest first).
660///
661/// Each source is followed by the sources it merges in itself, since its own
662/// keys shadow those it inherits. A mapping is only collected once, which
663/// also stops a merge that refers back to an enclosing anchor.
664pub(crate) fn merge_sources(
665 base: &crate::yaml::Mapping,
666 registry: &AnchorRegistry,
667) -> Result<Vec<crate::yaml::Mapping>, MergeError> {
668 let mut sources = Vec::new();
669 collect_merge_sources(base, registry, &mut sources)?;
670 Ok(sources)
671}
672
673/// The operands of a `<<` entry with value `value`: the items of the
674/// sequence it is, or else the value itself.
675pub(crate) fn merge_operands(
676 value: crate::as_yaml::YamlNode,
677 registry: &AnchorRegistry,
678) -> Result<Vec<crate::as_yaml::YamlNode>, AliasError> {
679 use crate::as_yaml::YamlNode;
680
681 // An alias stands for its node, so it can name a sequence of mappings
682 // too.
683 let sequence = match resolve_alias_node(value.clone(), registry)? {
684 YamlNode::Sequence(seq) => Some(seq),
685 YamlNode::TaggedNode(tagged) => tagged.as_sequence(),
686 _ => None,
687 };
688 Ok(match sequence {
689 Some(seq) => seq.values().collect(),
690 None => vec![value],
691 })
692}
693
694/// The mapping `operand`, an operand of a `<<` entry, merges in, or `None`
695/// if it is not a mapping.
696pub(crate) fn merge_source(
697 operand: &crate::as_yaml::YamlNode,
698 registry: &AnchorRegistry,
699) -> Result<Option<crate::yaml::Mapping>, AliasError> {
700 use crate::as_yaml::YamlNode;
701
702 Ok(match resolve_alias_node(operand.clone(), registry)? {
703 YamlNode::Mapping(m) => Some(m),
704 YamlNode::TaggedNode(tagged) => tagged.as_mapping(),
705 _ => None,
706 })
707}
708
709fn collect_merge_sources(
710 base: &crate::yaml::Mapping,
711 registry: &AnchorRegistry,
712 sources: &mut Vec<crate::yaml::Mapping>,
713) -> Result<(), MergeError> {
714 for (key, value) in base.iter() {
715 if !is_merge_key(&key) {
716 continue;
717 }
718 for operand in merge_operands(value, registry)? {
719 let source =
720 merge_source(&operand, registry)?.ok_or_else(|| MergeError::InvalidOperand {
721 operand: operand.to_string(),
722 })?;
723 if sources.iter().any(|s| s.syntax() == source.syntax()) {
724 continue;
725 }
726 sources.push(source.clone());
727 collect_merge_sources(&source, registry, sources)?;
728 }
729 }
730 Ok(())
731}
732
733/// The entries visible through a merged view of `base`: its own entries,
734/// then those of its merge sources that are not shadowed, with alias
735/// values resolved.
736fn merged_entries(
737 base: &crate::yaml::Mapping,
738 registry: &AnchorRegistry,
739) -> Result<Vec<MergedEntry>, MergeError> {
740 let mut entries: Vec<MergedEntry> = Vec::new();
741 for (key, value) in base.iter().filter(|(k, _)| !is_merge_key(k)) {
742 let identity = key_identity(&key, Some(registry))?;
743 entries.push((identity, key, resolve_alias_node(value, registry)?));
744 }
745 for source in merge_sources(base, registry)? {
746 for (key, value) in source.iter() {
747 if is_merge_key(&key) {
748 continue;
749 }
750 let identity = key_identity(&key, Some(registry))?;
751 if entries.iter().any(|(k, _, _)| *k == identity) {
752 continue;
753 }
754 entries.push((identity, key, resolve_alias_node(value, registry)?));
755 }
756 }
757 Ok(entries)
758}
759
760/// An entry of a merged view: the key's identity, the key as written and
761/// the value, with an alias resolved.
762type MergedEntry = (
763 NodeIdentity,
764 crate::as_yaml::YamlNode,
765 crate::as_yaml::YamlNode,
766);
767
768/// The document `node` is in, or the root of its tree outside one.
769/// Anchors are scoped to a single document.
770pub(crate) fn document_scope(node: &SyntaxNode) -> SyntaxNode {
771 node.ancestors()
772 .find(|n| n.kind() == SyntaxKind::DOCUMENT)
773 .unwrap_or_else(|| node.ancestors().last().expect("ancestors has self"))
774}
775
776/// If `node` is an alias, resolve it via `registry`; otherwise return it as-is.
777pub(crate) fn resolve_alias_node(
778 node: crate::as_yaml::YamlNode,
779 registry: &AnchorRegistry,
780) -> Result<crate::as_yaml::YamlNode, AliasError> {
781 use crate::as_yaml::YamlNode;
782
783 let YamlNode::Alias(alias) = &node else {
784 return Ok(node);
785 };
786 resolve_alias_with(alias, registry, SyntaxNode::clone)
787}
788
789/// Resolve `alias` via `registry` to the node built from `adjust` applied
790/// to the anchored node.
791pub(crate) fn resolve_alias_with(
792 alias: &crate::yaml::Alias,
793 registry: &AnchorRegistry,
794 adjust: impl FnOnce(&SyntaxNode) -> SyntaxNode,
795) -> Result<crate::as_yaml::YamlNode, AliasError> {
796 let target = adjust(registry.resolve_alias_through(alias)?);
797 crate::as_yaml::YamlNode::from_syntax_peeled(target).ok_or(AliasError::Undefined {
798 alias: alias.name(),
799 })
800}
801
802/// Extension trait that adds [`merged`](Self::merged) to [`Mapping`](crate::yaml::Mapping).
803pub trait MappingMergedExt {
804 /// Return a [`MergedMapping`] view over this mapping that resolves
805 /// aliases and merge keys against `registry`.
806 ///
807 /// The view is read-only and does not modify the underlying CST.
808 ///
809 /// Fails as [`MergedMapping::new`] does.
810 fn merged<'a>(&self, registry: &'a AnchorRegistry) -> Result<MergedMapping<'a>, ResolveError>;
811}
812
813impl MappingMergedExt for crate::yaml::Mapping {
814 fn merged<'a>(&self, registry: &'a AnchorRegistry) -> Result<MergedMapping<'a>, ResolveError> {
815 MergedMapping::new(self.clone(), registry)
816 }
817}
818
819/// Extension trait that adds [`merged`](Self::merged) to [`Document`](crate::yaml::Document).
820pub trait DocumentMergedExt {
821 /// Return a [`MergedMapping`] view over the document's root mapping.
822 ///
823 /// The anchor registry is built from this document automatically.
824 /// Returns `Ok(None)` if the document's root is not a mapping, and fails
825 /// as [`MergedMapping::new`] does.
826 ///
827 /// Because the registry is owned by the returned view, this method
828 /// returns an owned [`MergedView`] container rather than a borrowing
829 /// `MergedMapping<'_>`. Use [`MergedView::as_mapping`] to obtain the
830 /// borrowing view for lookups.
831 fn merged(&self) -> Result<Option<MergedView>, ResolveError>;
832}
833
834impl DocumentMergedExt for crate::yaml::Document {
835 fn merged(&self) -> Result<Option<MergedView>, ResolveError> {
836 let Some(mapping) = self.as_mapping() else {
837 return Ok(None);
838 };
839 let registry = self.build_anchor_registry();
840 let entries = merged_entries(&mapping, ®istry).map_err(ResolveError::from)?;
841 Ok(Some(MergedView {
842 mapping,
843 registry,
844 entries,
845 }))
846 }
847}
848
849/// An owning container for a [`MergedMapping`] view of a document's root.
850///
851/// Holds both the root [`Mapping`](crate::yaml::Mapping) and the
852/// [`AnchorRegistry`] built from the document, so the view can be returned
853/// from a function without lifetime gymnastics. Call
854/// [`as_mapping`](Self::as_mapping) (or use the convenience methods
855/// directly) to query the view.
856pub struct MergedView {
857 mapping: crate::yaml::Mapping,
858 registry: AnchorRegistry,
859 entries: Vec<MergedEntry>,
860}
861
862impl MergedView {
863 /// Borrow this view as a [`MergedMapping`] for lookups.
864 pub fn as_mapping(&self) -> MergedMapping<'_> {
865 MergedMapping {
866 base: self.mapping.clone(),
867 registry: &self.registry,
868 entries: self.entries.clone(),
869 }
870 }
871
872 /// Get a value through the merged view.
873 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
874 self.as_mapping().get(key)
875 }
876
877 /// Check whether `key` is reachable through the merged view.
878 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
879 self.as_mapping().contains_key(key)
880 }
881
882 /// Return the registry built from the document.
883 pub fn registry(&self) -> &AnchorRegistry {
884 &self.registry
885 }
886}
887
888impl std::fmt::Debug for MergedView {
889 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
890 f.debug_struct("MergedView").finish()
891 }
892}
893
894#[cfg(test)]
895mod tests {
896 use super::*;
897 use crate::Document;
898 use std::str::FromStr;
899
900 fn doc(text: &str) -> Document {
901 Document::from_str(text).expect("parse")
902 }
903
904 #[test]
905 fn get_resolved_tagged_alias() {
906 // The alias's own tag applies to what it refers to, as when an
907 // edit replaces it with a copy.
908 for (yaml, expected) in [
909 ("x: &x 1\na: !t *x\n", "a: !t 1\n"),
910 ("x: &x '1'\na: !!int *x\n", "a: !!int '1'\n"),
911 ("x: &x [1]\na: !t *x\n", "a: !t [1]\n"),
912 ("x: &x {k: 1}\na: !t *x\n", "a: !t {k: 1}\n"),
913 ("x: &x {k: }\na: !!set *x\n", "a: !!set {k: }\n"),
914 ("x: &x [{k: 1}]\na: !!omap *x\n", "a: !!omap [{k: 1}]\n"),
915 ("y: &y 2\nx: &x [*y]\na: !t *x\n", "a: !t [2]\n"),
916 ("x: &x !u 1\na: !t *x\n", "a: !t 1\n"),
917 ("x: !u &x [1]\na: !t *x\n", "a: !t [1]\n"),
918 ("x: &x !u {k: }\na: !!set *x\n", "a: !!set {k: }\n"),
919 ] {
920 assert_eq!(
921 doc(yaml).get_resolved("a"),
922 doc(expected).get_resolved("a"),
923 "{yaml:?}"
924 );
925 }
926 assert_eq!(
927 doc("x: &x 1\na: !t *x\n").get_resolved("a"),
928 Ok(Some(YamlValue::Scalar(
929 crate::ScalarValue::parse("1").with_resolved_tag("!t".to_string())
930 )))
931 );
932 }
933}