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_at(&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_at`](Self::resolve_alias_at), 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 = self
368 .resolve_alias_at(&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 /// The node `alias` refers to: the last definition of its anchor before
389 /// the alias, following an anchored alias such as `&b *a` to the node it
390 /// ends at.
391 ///
392 /// Unlike [`resolve`](Self::resolve), which looks an anchor up by name
393 /// and returns its last definition anywhere, this sees an anchor
394 /// redefined later in the document only from the aliases after the
395 /// redefinition. `alias` must belong to the tree this registry was built
396 /// from.
397 ///
398 /// Fails if the anchor is not defined before the alias, or if a chain of
399 /// anchored aliases loops back on itself.
400 ///
401 /// ```
402 /// use std::str::FromStr;
403 /// use yaml_edit::{AnchorRegistry, Document};
404 ///
405 /// let doc = Document::from_str("- &x 1\n- *x\n- &x 2\n- *x\n").unwrap();
406 /// let registry = AnchorRegistry::from_document(&doc);
407 /// let seq = doc.as_sequence().unwrap();
408 /// let resolve = |i| {
409 /// let alias = seq.get(i).unwrap().as_alias().unwrap().clone();
410 /// registry.resolve_alias(&alias).unwrap().to_i64()
411 /// };
412 /// assert_eq!(resolve(1), Some(1));
413 /// assert_eq!(resolve(3), Some(2));
414 /// ```
415 pub fn resolve_alias(
416 &self,
417 alias: &crate::yaml::Alias,
418 ) -> Result<crate::as_yaml::YamlNode, ResolveError> {
419 Ok(resolve_alias_node(
420 crate::as_yaml::YamlNode::Alias(alias.clone()),
421 self,
422 )?)
423 }
424
425 /// Check if an anchor is defined
426 pub fn contains(&self, name: &str) -> bool {
427 self.anchors.contains_key(name)
428 }
429
430 /// Get all anchor names
431 pub fn anchor_names(&self) -> impl Iterator<Item = &str> {
432 self.anchors.keys().map(|s| s.as_str())
433 }
434}
435
436impl Default for AnchorRegistry {
437 fn default() -> Self {
438 Self::new()
439 }
440}
441
442/// Extension trait for Document to support resolved lookups
443pub trait DocumentResolvedExt {
444 /// Get a value with anchor/alias resolution
445 ///
446 /// This method resolves aliases (*alias) to their anchored values (&anchor),
447 /// and supports merge keys (<<) for combining mappings.
448 ///
449 /// Returns `Ok(None)` if the document root is not a mapping or has no
450 /// such key, and an error if the value contains an undefined or
451 /// recursive alias, an invalid merge key, a mapping with the same key
452 /// twice, or distinct keys that have the same string form, such as `1`
453 /// and `'1'`.
454 ///
455 /// Resolution is read-only. To write a merge or alias back, pass a mapping
456 /// that already contains `<<: *name` (or an alias node) to
457 /// [`Mapping::set`](crate::Mapping::set). `set` does not infer a merge
458 /// from a resolved object.
459 ///
460 /// # Examples
461 ///
462 /// ```
463 /// use yaml_edit::Document;
464 /// use yaml_edit::anchor_resolution::DocumentResolvedExt;
465 /// use std::str::FromStr;
466 ///
467 /// let yaml = r#"
468 /// config: &cfg
469 /// port: 8080
470 /// server: *cfg
471 /// "#;
472 /// let doc = Document::from_str(yaml).unwrap();
473 ///
474 /// // Get with resolution - expands the *cfg alias
475 /// if let Some(resolved_value) = doc.get_resolved("server").unwrap() {
476 /// if let Some(server) = resolved_value.as_mapping() {
477 /// if let Some(port) = server.get("port") {
478 /// assert_eq!(port.to_i64(), Some(8080));
479 /// }
480 /// }
481 /// }
482 /// ```
483 fn get_resolved(&self, key: impl crate::AsYaml) -> Result<Option<YamlValue>, ResolveError>;
484
485 /// Build the anchor registry for this document
486 fn build_anchor_registry(&self) -> AnchorRegistry;
487}
488
489impl DocumentResolvedExt for crate::yaml::Document {
490 fn get_resolved(&self, key: impl crate::AsYaml) -> Result<Option<YamlValue>, ResolveError> {
491 let Some(node) = self.as_mapping().and_then(|m| m.get(&key)) else {
492 return Ok(None);
493 };
494 let registry = self.build_anchor_registry();
495 crate::value::convert::resolved_value(node, Some(®istry), &mut Vec::new())
496 }
497
498 fn build_anchor_registry(&self) -> AnchorRegistry {
499 AnchorRegistry::from_document(self)
500 }
501}
502
503/// Whether `key` is the merge key: a plain `<<`, or `<<` with the merge tag
504/// written out, as in `!!merge <<`. A quoted `'<<'` is an ordinary string.
505pub(crate) fn is_merge_key(key: &crate::as_yaml::YamlNode) -> bool {
506 use crate::as_yaml::YamlNode;
507 match key {
508 YamlNode::Scalar(s) => s.value() == "<<",
509 YamlNode::TaggedNode(t) => {
510 t.resolved_tag().as_deref() == Some("!!merge")
511 && t.value().is_some_and(|s| s.value() == "<<")
512 }
513 _ => false,
514 }
515}
516
517/// A view over a [`Mapping`](crate::yaml::Mapping) that transparently expands
518/// alias references (`*name`) and merge keys (`<<:`).
519///
520/// `MergedMapping` borrows an [`AnchorRegistry`] and collects its entries
521/// when it is created: the merge sources are followed and alias values are
522/// resolved then, so an undefined alias or invalid merge key is reported
523/// by [`new`](Self::new). The underlying syntax tree is not modified and
524/// the returned nodes are live CST nodes, but entries added, removed or
525/// replaced after the view was created are not seen.
526///
527/// Direct keys in the base mapping always shadow keys contributed by merge
528/// sources, matching the YAML 1.1 merge-key semantics. When a merge value is
529/// a sequence of aliases (`<<: [*a, *b]`), earlier aliases take precedence
530/// over later ones, again per the YAML 1.1 spec.
531///
532/// Returned values are [`YamlNode`](crate::as_yaml::YamlNode)s backed by real
533/// CST nodes, so they preserve the original formatting and quoting style.
534///
535/// This view is read-only. [`Mapping::set`](crate::Mapping::set) replaces the
536/// value node you pass; it does not turn a resolved map back into `<<: *name`.
537///
538/// # Examples
539///
540/// ```
541/// use yaml_edit::{Document, anchor_resolution::{DocumentResolvedExt, MappingMergedExt}};
542/// use std::str::FromStr;
543///
544/// let yaml = r#"
545/// defaults: &defaults
546/// timeout: 30
547/// retries: 3
548///
549/// production:
550/// <<: *defaults
551/// host: prod.example.com
552/// timeout: 60
553/// "#;
554///
555/// let doc = Document::from_str(yaml).unwrap();
556/// let root = doc.as_mapping().unwrap();
557/// let registry = doc.build_anchor_registry();
558///
559/// let prod = root.get_mapping("production").unwrap();
560/// let merged = prod.merged(®istry).unwrap();
561///
562/// // Direct key wins over merged value.
563/// assert_eq!(merged.get("timeout").unwrap().to_i64(), Some(60));
564/// // Merged key from defaults is visible.
565/// assert_eq!(merged.get("retries").unwrap().to_i64(), Some(3));
566/// // Direct-only key is visible.
567/// assert!(merged.get("host").is_some());
568/// // `<<` itself is hidden.
569/// assert!(merged.get("<<").is_none());
570/// ```
571#[derive(Clone)]
572pub struct MergedMapping<'a> {
573 base: crate::yaml::Mapping,
574 registry: &'a AnchorRegistry,
575 entries: Vec<MergedEntry>,
576}
577
578impl<'a> MergedMapping<'a> {
579 /// Create a new merged view over `base`, resolving aliases against `registry`.
580 ///
581 /// Fails if a `<<` operand in `base`, or in a mapping merged into it,
582 /// is an undefined alias or not a mapping, if a value visible
583 /// through the view is an undefined alias, or if a key is or holds an
584 /// undefined or recursive alias.
585 pub fn new(
586 base: crate::yaml::Mapping,
587 registry: &'a AnchorRegistry,
588 ) -> Result<Self, ResolveError> {
589 Ok(Self::build(base, registry)?)
590 }
591
592 /// Like [`new`](Self::new), failing with the errors only a merged view
593 /// can meet.
594 pub(crate) fn build(
595 base: crate::yaml::Mapping,
596 registry: &'a AnchorRegistry,
597 ) -> Result<Self, MergeError> {
598 let entries = merged_entries(&base, registry)?;
599 Ok(Self {
600 base,
601 registry,
602 entries,
603 })
604 }
605
606 /// Return the underlying (un-merged) [`Mapping`](crate::yaml::Mapping).
607 ///
608 /// Useful when you want to mutate the original mapping or read its raw
609 /// (non-resolved) contents.
610 pub fn base(&self) -> &crate::yaml::Mapping {
611 &self.base
612 }
613
614 /// Return the [`AnchorRegistry`] this view resolves against.
615 pub fn registry(&self) -> &AnchorRegistry {
616 self.registry
617 }
618
619 /// Get the value associated with `key`, resolving aliases and merge keys.
620 ///
621 /// Direct entries in the base mapping take precedence over keys
622 /// contributed by `<<:` merge sources. The merge key itself is hidden:
623 /// looking up `"<<"` only finds a quoted `'<<'` key, which is an ordinary
624 /// string.
625 ///
626 /// If the matched value is an alias (`*name`), the resolved target node
627 /// is returned.
628 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
629 // A key that cannot be resolved, such as an undefined alias, is in
630 // no mapping.
631 let identity = crate::as_yaml::identity_of(&key, Some(self.registry))
632 .ok()
633 .flatten()?;
634 self.entries
635 .iter()
636 .find(|(k, _, _)| *k == identity)
637 .map(|(_, _, v)| v.clone())
638 }
639
640 /// Returns `true` if a value would be returned by [`get`](Self::get) for
641 /// `key`.
642 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
643 self.get(key).is_some()
644 }
645
646 /// Iterate over `(key, value)` pairs in the merged view.
647 ///
648 /// Direct entries appear first, in their original document order.
649 /// Merged-in entries follow, with duplicates (and any direct-key matches
650 /// already yielded) filtered out. Keys are compared as YAML values, so
651 /// the integer `1` and the string `'1'` are distinct. The `<<` merge key
652 /// itself is never yielded.
653 pub fn iter(
654 &self,
655 ) -> impl Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + '_ {
656 self.entries.iter().map(|(_, k, v)| (k.clone(), v.clone()))
657 }
658
659 /// Iterate over the keys of the merged view.
660 pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
661 self.iter().map(|(k, _)| k)
662 }
663
664 /// Iterate over the values of the merged view.
665 pub fn values(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
666 self.iter().map(|(_, v)| v)
667 }
668
669 /// Number of distinct keys visible through the merged view.
670 pub fn len(&self) -> usize {
671 self.iter().count()
672 }
673
674 /// Returns `true` if the merged view has no entries.
675 pub fn is_empty(&self) -> bool {
676 self.iter().next().is_none()
677 }
678
679 /// Get a nested mapping by `key` and return it as another `MergedMapping`
680 /// that shares the same registry.
681 ///
682 /// Returns `Ok(None)` if the key does not exist or the value is not a
683 /// mapping, and fails as [`new`](Self::new) does on the nested mapping.
684 pub fn get_merged(
685 &self,
686 key: impl crate::AsYaml,
687 ) -> Result<Option<MergedMapping<'a>>, ResolveError> {
688 let Some(mapping) = self.get(key).and_then(|node| node.as_mapping().cloned()) else {
689 return Ok(None);
690 };
691 MergedMapping::new(mapping, self.registry).map(Some)
692 }
693}
694
695impl std::fmt::Debug for MergedMapping<'_> {
696 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
697 f.debug_struct("MergedMapping")
698 .field("len", &self.len())
699 .finish()
700 }
701}
702
703/// Walk the `<<:` entries of `base` and return the resolved source mappings,
704/// in precedence order (highest first).
705///
706/// Each source is followed by the sources it merges in itself, since its own
707/// keys shadow those it inherits. A mapping is only collected once, which
708/// also stops a merge that refers back to an enclosing anchor.
709pub(crate) fn merge_sources(
710 base: &crate::yaml::Mapping,
711 registry: &AnchorRegistry,
712) -> Result<Vec<crate::yaml::Mapping>, MergeError> {
713 let mut sources = Vec::new();
714 collect_merge_sources(base, registry, &mut sources)?;
715 Ok(sources)
716}
717
718/// The operands of a `<<` entry with value `value`: the items of the
719/// sequence it is, or else the value itself.
720pub(crate) fn merge_operands(
721 value: crate::as_yaml::YamlNode,
722 registry: &AnchorRegistry,
723) -> Result<Vec<crate::as_yaml::YamlNode>, AliasError> {
724 use crate::as_yaml::YamlNode;
725
726 // An alias stands for its node, so it can name a sequence of mappings
727 // too.
728 let sequence = match resolve_alias_node(value.clone(), registry)? {
729 YamlNode::Sequence(seq) => Some(seq),
730 YamlNode::TaggedNode(tagged) => tagged.as_sequence(),
731 _ => None,
732 };
733 Ok(match sequence {
734 Some(seq) => seq.values().collect(),
735 None => vec![value],
736 })
737}
738
739/// The mapping `operand`, an operand of a `<<` entry, merges in, or `None`
740/// if it is not a mapping.
741pub(crate) fn merge_source(
742 operand: &crate::as_yaml::YamlNode,
743 registry: &AnchorRegistry,
744) -> Result<Option<crate::yaml::Mapping>, AliasError> {
745 use crate::as_yaml::YamlNode;
746
747 Ok(match resolve_alias_node(operand.clone(), registry)? {
748 YamlNode::Mapping(m) => Some(m),
749 YamlNode::TaggedNode(tagged) => tagged.as_mapping(),
750 _ => None,
751 })
752}
753
754fn collect_merge_sources(
755 base: &crate::yaml::Mapping,
756 registry: &AnchorRegistry,
757 sources: &mut Vec<crate::yaml::Mapping>,
758) -> Result<(), MergeError> {
759 for (key, value) in base.iter() {
760 if !is_merge_key(&key) {
761 continue;
762 }
763 for operand in merge_operands(value, registry)? {
764 let source =
765 merge_source(&operand, registry)?.ok_or_else(|| MergeError::InvalidOperand {
766 operand: operand.to_string(),
767 })?;
768 if sources.iter().any(|s| s.syntax() == source.syntax()) {
769 continue;
770 }
771 sources.push(source.clone());
772 collect_merge_sources(&source, registry, sources)?;
773 }
774 }
775 Ok(())
776}
777
778/// The entries visible through a merged view of `base`: its own entries,
779/// then those of its merge sources that are not shadowed, with alias
780/// values resolved.
781fn merged_entries(
782 base: &crate::yaml::Mapping,
783 registry: &AnchorRegistry,
784) -> Result<Vec<MergedEntry>, MergeError> {
785 let mut entries: Vec<MergedEntry> = Vec::new();
786 for (key, value) in base.iter().filter(|(k, _)| !is_merge_key(k)) {
787 let identity = key_identity(&key, Some(registry))?;
788 entries.push((identity, key, resolve_alias_node(value, registry)?));
789 }
790 for source in merge_sources(base, registry)? {
791 for (key, value) in source.iter() {
792 if is_merge_key(&key) {
793 continue;
794 }
795 let identity = key_identity(&key, Some(registry))?;
796 if entries.iter().any(|(k, _, _)| *k == identity) {
797 continue;
798 }
799 entries.push((identity, key, resolve_alias_node(value, registry)?));
800 }
801 }
802 Ok(entries)
803}
804
805/// An entry of a merged view: the key's identity, the key as written and
806/// the value, with an alias resolved.
807type MergedEntry = (
808 NodeIdentity,
809 crate::as_yaml::YamlNode,
810 crate::as_yaml::YamlNode,
811);
812
813/// The document `node` is in, or the root of its tree outside one.
814/// Anchors are scoped to a single document.
815pub(crate) fn document_scope(node: &SyntaxNode) -> SyntaxNode {
816 node.ancestors()
817 .find(|n| n.kind() == SyntaxKind::DOCUMENT)
818 .unwrap_or_else(|| node.ancestors().last().expect("ancestors has self"))
819}
820
821/// If `node` is an alias, resolve it via `registry`; otherwise return it as-is.
822pub(crate) fn resolve_alias_node(
823 node: crate::as_yaml::YamlNode,
824 registry: &AnchorRegistry,
825) -> Result<crate::as_yaml::YamlNode, AliasError> {
826 use crate::as_yaml::YamlNode;
827
828 let YamlNode::Alias(alias) = &node else {
829 return Ok(node);
830 };
831 resolve_alias_with(alias, registry, SyntaxNode::clone)
832}
833
834/// Resolve `alias` via `registry` to the node built from `adjust` applied
835/// to the anchored node.
836pub(crate) fn resolve_alias_with(
837 alias: &crate::yaml::Alias,
838 registry: &AnchorRegistry,
839 adjust: impl FnOnce(&SyntaxNode) -> SyntaxNode,
840) -> Result<crate::as_yaml::YamlNode, AliasError> {
841 let target = adjust(registry.resolve_alias_through(alias)?);
842 crate::as_yaml::YamlNode::from_syntax_peeled(target).ok_or(AliasError::Undefined {
843 alias: alias.name(),
844 })
845}
846
847/// Extension trait that adds [`merged`](Self::merged) to [`Mapping`](crate::yaml::Mapping).
848pub trait MappingMergedExt {
849 /// Return a [`MergedMapping`] view over this mapping that resolves
850 /// aliases and merge keys against `registry`.
851 ///
852 /// The view is read-only and does not modify the underlying CST.
853 ///
854 /// Fails as [`MergedMapping::new`] does.
855 fn merged<'a>(&self, registry: &'a AnchorRegistry) -> Result<MergedMapping<'a>, ResolveError>;
856}
857
858impl MappingMergedExt for crate::yaml::Mapping {
859 fn merged<'a>(&self, registry: &'a AnchorRegistry) -> Result<MergedMapping<'a>, ResolveError> {
860 MergedMapping::new(self.clone(), registry)
861 }
862}
863
864/// Extension trait that adds [`merged`](Self::merged) to [`Document`](crate::yaml::Document).
865pub trait DocumentMergedExt {
866 /// Return a [`MergedMapping`] view over the document's root mapping.
867 ///
868 /// The anchor registry is built from this document automatically.
869 /// Returns `Ok(None)` if the document's root is not a mapping, and fails
870 /// as [`MergedMapping::new`] does.
871 ///
872 /// Because the registry is owned by the returned view, this method
873 /// returns an owned [`MergedView`] container rather than a borrowing
874 /// `MergedMapping<'_>`. Use [`MergedView::as_mapping`] to obtain the
875 /// borrowing view for lookups.
876 fn merged(&self) -> Result<Option<MergedView>, ResolveError>;
877}
878
879impl DocumentMergedExt for crate::yaml::Document {
880 fn merged(&self) -> Result<Option<MergedView>, ResolveError> {
881 let Some(mapping) = self.as_mapping() else {
882 return Ok(None);
883 };
884 let registry = self.build_anchor_registry();
885 let entries = merged_entries(&mapping, ®istry).map_err(ResolveError::from)?;
886 Ok(Some(MergedView {
887 mapping,
888 registry,
889 entries,
890 }))
891 }
892}
893
894/// An owning container for a [`MergedMapping`] view of a document's root.
895///
896/// Holds both the root [`Mapping`](crate::yaml::Mapping) and the
897/// [`AnchorRegistry`] built from the document, so the view can be returned
898/// from a function without lifetime gymnastics. Call
899/// [`as_mapping`](Self::as_mapping) (or use the convenience methods
900/// directly) to query the view.
901pub struct MergedView {
902 mapping: crate::yaml::Mapping,
903 registry: AnchorRegistry,
904 entries: Vec<MergedEntry>,
905}
906
907impl MergedView {
908 /// Borrow this view as a [`MergedMapping`] for lookups.
909 pub fn as_mapping(&self) -> MergedMapping<'_> {
910 MergedMapping {
911 base: self.mapping.clone(),
912 registry: &self.registry,
913 entries: self.entries.clone(),
914 }
915 }
916
917 /// Get a value through the merged view.
918 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
919 self.as_mapping().get(key)
920 }
921
922 /// Check whether `key` is reachable through the merged view.
923 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
924 self.as_mapping().contains_key(key)
925 }
926
927 /// Return the registry built from the document.
928 pub fn registry(&self) -> &AnchorRegistry {
929 &self.registry
930 }
931}
932
933impl std::fmt::Debug for MergedView {
934 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
935 f.debug_struct("MergedView").finish()
936 }
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942 use crate::Document;
943 use std::str::FromStr;
944
945 fn doc(text: &str) -> Document {
946 Document::from_str(text).expect("parse")
947 }
948
949 #[test]
950 fn get_resolved_tagged_alias() {
951 // The alias's own tag applies to what it refers to, as when an
952 // edit replaces it with a copy.
953 for (yaml, expected) in [
954 ("x: &x 1\na: !t *x\n", "a: !t 1\n"),
955 ("x: &x '1'\na: !!int *x\n", "a: !!int '1'\n"),
956 ("x: &x [1]\na: !t *x\n", "a: !t [1]\n"),
957 ("x: &x {k: 1}\na: !t *x\n", "a: !t {k: 1}\n"),
958 ("x: &x {k: }\na: !!set *x\n", "a: !!set {k: }\n"),
959 ("x: &x [{k: 1}]\na: !!omap *x\n", "a: !!omap [{k: 1}]\n"),
960 ("y: &y 2\nx: &x [*y]\na: !t *x\n", "a: !t [2]\n"),
961 ("x: &x !u 1\na: !t *x\n", "a: !t 1\n"),
962 ("x: !u &x [1]\na: !t *x\n", "a: !t [1]\n"),
963 ("x: &x !u {k: }\na: !!set *x\n", "a: !!set {k: }\n"),
964 ] {
965 assert_eq!(
966 doc(yaml).get_resolved("a"),
967 doc(expected).get_resolved("a"),
968 "{yaml:?}"
969 );
970 }
971 assert_eq!(
972 doc("x: &x 1\na: !t *x\n").get_resolved("a"),
973 Ok(Some(YamlValue::Scalar(
974 crate::ScalarValue::parse("1").with_resolved_tag("!t".to_string())
975 )))
976 );
977 }
978}