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**: Undefined aliases return None instead of panicking
13//!
14//! # Examples
15//!
16//! ```
17//! use yaml_edit::Document;
18//! use yaml_edit::anchor_resolution::DocumentResolvedExt;
19//! use std::str::FromStr;
20//!
21//! let yaml = r#"
22//! defaults: &defaults
23//! timeout: 30
24//! retries: 3
25//!
26//! production:
27//! <<: *defaults
28//! host: prod.example.com
29//! "#;
30//!
31//! let doc = Document::from_str(yaml).unwrap();
32//!
33//! // Regular get() doesn't resolve - returns the alias syntax
34//! // get_resolved() expands aliases and merge keys
35//! if let Some(resolved_value) = doc.get_resolved("production") {
36//! if let Some(prod) = resolved_value.as_mapping() {
37//! // This will find 'timeout: 30' from the merged defaults
38//! if let Some(timeout) = prod.get("timeout") {
39//! assert_eq!(timeout.to_i64(), Some(30));
40//! }
41//! // This will find the overridden value
42//! if let Some(host) = prod.get("host") {
43//! assert_eq!(host.as_scalar().map(|s| s.to_string()).as_deref(), Some("prod.example.com"));
44//! }
45//! }
46//! }
47//! ```
48
49use crate::as_yaml::AsYaml;
50use crate::lex::SyntaxKind;
51use crate::value::YamlValue;
52use crate::yaml::SyntaxNode;
53use std::collections::HashMap;
54
55/// A registry of anchors defined in a YAML document
56#[derive(Debug, Clone)]
57pub struct AnchorRegistry {
58 /// Map from anchor name to the syntax node it refers to
59 anchors: HashMap<String, SyntaxNode>,
60}
61
62impl AnchorRegistry {
63 /// Create a new empty anchor registry
64 pub fn new() -> Self {
65 Self {
66 anchors: HashMap::new(),
67 }
68 }
69
70 /// Build a registry from a Document
71 pub fn from_document(doc: &crate::yaml::Document) -> Self {
72 if let Some(node) = doc.as_node() {
73 Self::from_tree(node)
74 } else {
75 Self::new()
76 }
77 }
78
79 /// Build a registry by scanning a syntax tree
80 pub fn from_tree(root: &SyntaxNode) -> Self {
81 let mut registry = Self::new();
82 registry.collect_anchors_from_tree(root);
83 registry
84 }
85
86 /// Recursively collect all anchors from a syntax tree
87 fn collect_anchors_from_tree(&mut self, node: &SyntaxNode) {
88 // Look for ANCHOR tokens in this node
89 for child in node.children_with_tokens() {
90 if let Some(token) = child.as_token() {
91 if token.kind() == SyntaxKind::ANCHOR {
92 // Extract anchor name (remove '&' prefix)
93 let text = token.text();
94 if let Some(name) = text.strip_prefix('&') {
95 // Find the associated value node
96 if let Some(value_node) = self.find_anchored_value(node) {
97 self.anchors.insert(name.to_string(), value_node);
98 }
99 }
100 }
101 } else if let Some(child_node) = child.as_node() {
102 // Recurse into child nodes
103 self.collect_anchors_from_tree(child_node);
104 }
105 }
106 }
107
108 /// Find the value node that an anchor refers to (low-level helper)
109 fn find_anchored_value(&self, node: &SyntaxNode) -> Option<SyntaxNode> {
110 // The anchored value is typically a sibling MAPPING/SEQUENCE/SCALAR node
111 for child in node.children() {
112 if matches!(
113 child.kind(),
114 SyntaxKind::VALUE
115 | SyntaxKind::SCALAR
116 | SyntaxKind::MAPPING
117 | SyntaxKind::SEQUENCE
118 | SyntaxKind::TAGGED_NODE
119 ) {
120 return Some(child);
121 }
122 }
123 None
124 }
125
126 /// Look up an anchor by name
127 pub fn resolve(&self, name: &str) -> Option<&SyntaxNode> {
128 self.anchors.get(name)
129 }
130
131 /// Check if an anchor is defined
132 pub fn contains(&self, name: &str) -> bool {
133 self.anchors.contains_key(name)
134 }
135
136 /// Get all anchor names
137 pub fn anchor_names(&self) -> impl Iterator<Item = &str> {
138 self.anchors.keys().map(|s| s.as_str())
139 }
140}
141
142impl Default for AnchorRegistry {
143 fn default() -> Self {
144 Self::new()
145 }
146}
147
148/// Extension trait for Document to support resolved lookups
149pub trait DocumentResolvedExt {
150 /// Get a value with anchor/alias resolution
151 ///
152 /// This method resolves aliases (*alias) to their anchored values (&anchor),
153 /// and supports merge keys (<<) for combining mappings.
154 ///
155 /// # Examples
156 ///
157 /// ```
158 /// use yaml_edit::Document;
159 /// use yaml_edit::anchor_resolution::DocumentResolvedExt;
160 /// use std::str::FromStr;
161 ///
162 /// let yaml = r#"
163 /// config: &cfg
164 /// port: 8080
165 /// server: *cfg
166 /// "#;
167 /// let doc = Document::from_str(yaml).unwrap();
168 ///
169 /// // Get with resolution - expands the *cfg alias
170 /// if let Some(resolved_value) = doc.get_resolved("server") {
171 /// if let Some(server) = resolved_value.as_mapping() {
172 /// if let Some(port) = server.get("port") {
173 /// assert_eq!(port.to_i64(), Some(8080));
174 /// }
175 /// }
176 /// }
177 /// ```
178 fn get_resolved(&self, key: impl crate::AsYaml) -> Option<YamlValue>;
179
180 /// Build the anchor registry for this document
181 fn build_anchor_registry(&self) -> AnchorRegistry;
182}
183
184impl DocumentResolvedExt for crate::yaml::Document {
185 fn get_resolved(&self, key: impl crate::AsYaml) -> Option<YamlValue> {
186 use rowan::ast::AstNode;
187
188 // Build the anchor registry from this document
189 let registry = self.build_anchor_registry();
190
191 // Get the value from the document (assuming it's a mapping)
192 let mapping = self.as_mapping()?;
193 let value = mapping
194 .get_node(&key)
195 .and_then(crate::value::YamlValue::cast)?;
196
197 // Check if we need to resolve an alias
198 // Get the syntax node to check for REFERENCE tokens
199 if let Some(node) = mapping.get_node(&key) {
200 if let Some(alias_name) = find_alias_reference(&node) {
201 // Resolve the alias
202 if let Some(resolved_node) = registry.resolve(&alias_name) {
203 // Convert the resolved node back to YamlValue
204 return YamlValue::cast(resolved_node.clone());
205 }
206 }
207 }
208
209 // Check for merge keys — must use CST Mapping, not YamlValue::Mapping (BTreeMap)
210 if let Some(node) = mapping.get_node(&key) {
211 if let Some(result_mapping) = crate::yaml::Mapping::cast(node) {
212 if has_merge_keys(&result_mapping) {
213 return Some(YamlValue::Mapping(apply_merge_keys(
214 &result_mapping,
215 ®istry,
216 )));
217 }
218 }
219 }
220
221 Some(value)
222 }
223
224 fn build_anchor_registry(&self) -> AnchorRegistry {
225 AnchorRegistry::from_document(self)
226 }
227}
228
229/// Find if a node is an alias reference (contains a REFERENCE token)
230fn find_alias_reference(node: &SyntaxNode) -> Option<String> {
231 // Check this node and its children for REFERENCE tokens
232 for child in node.children_with_tokens() {
233 if let Some(token) = child.as_token() {
234 if token.kind() == SyntaxKind::REFERENCE {
235 let text = token.text();
236 // Remove the '*' prefix
237 return text.strip_prefix('*').map(|s| s.to_string());
238 }
239 }
240 }
241
242 // Check parent nodes too
243 if let Some(parent) = node.parent() {
244 for child in parent.children_with_tokens() {
245 if let Some(token) = child.as_token() {
246 if token.kind() == SyntaxKind::REFERENCE {
247 let text = token.text();
248 return text.strip_prefix('*').map(|s| s.to_string());
249 }
250 }
251 }
252 }
253
254 None
255}
256
257/// Extract a string value from a YamlNode scalar.
258fn node_as_string(node: &crate::as_yaml::YamlNode) -> Option<String> {
259 node.as_scalar().map(|s| s.as_string())
260}
261
262/// Check if a mapping contains merge keys (<<)
263fn has_merge_keys(mapping: &crate::yaml::Mapping) -> bool {
264 for (key, _) in mapping.iter() {
265 if node_as_string(&key).as_deref() == Some("<<") {
266 return true;
267 }
268 }
269 false
270}
271
272/// Apply merge keys to create a new merged mapping (returns BTreeMap for YamlValue::Mapping)
273fn apply_merge_keys(
274 mapping: &crate::yaml::Mapping,
275 registry: &AnchorRegistry,
276) -> std::collections::BTreeMap<String, YamlValue> {
277 use crate::as_yaml::YamlNode;
278 use std::collections::BTreeMap;
279
280 let mut merged_pairs: BTreeMap<String, YamlValue> = BTreeMap::new();
281
282 // First pass: process merge keys and collect merged values
283 for (key, value) in mapping.iter() {
284 let Some(key_str) = node_as_string(&key) else {
285 continue;
286 };
287 if key_str != "<<" {
288 continue;
289 }
290
291 // Handle both single alias and sequence of aliases
292 match &value {
293 // Single alias: <<: *alias
294 YamlNode::Scalar(_) => {
295 let Some(alias_text) = node_as_string(&value) else {
296 continue;
297 };
298 let Some(alias_name) = alias_text.strip_prefix('*') else {
299 continue;
300 };
301
302 merge_from_alias(&mut merged_pairs, alias_name, registry);
303 }
304 // Multiple aliases: <<: [*alias1, *alias2]
305 YamlNode::Sequence(seq) => {
306 // Process aliases in order - later aliases override earlier ones
307 for alias_node in seq.values() {
308 let Some(alias_text) = node_as_string(&alias_node) else {
309 continue;
310 };
311 let Some(alias_name) = alias_text.strip_prefix('*') else {
312 continue;
313 };
314
315 merge_from_alias(&mut merged_pairs, alias_name, registry);
316 }
317 }
318 _ => continue,
319 }
320 }
321
322 // Second pass: add direct keys (override merged keys)
323 for (key, value) in mapping.iter() {
324 let Some(key_str) = node_as_string(&key) else {
325 continue;
326 };
327 if key_str == "<<" {
328 continue;
329 }
330 // Convert YamlNode back to YamlValue for storage
331 if let Some(yaml_value) = YamlValue::cast(value.syntax().clone()) {
332 merged_pairs.insert(key_str, yaml_value);
333 }
334 }
335
336 merged_pairs
337}
338
339/// Helper function to merge keys from a single alias
340fn merge_from_alias(
341 merged_pairs: &mut std::collections::BTreeMap<String, YamlValue>,
342 alias_name: &str,
343 registry: &AnchorRegistry,
344) {
345 use rowan::ast::AstNode;
346
347 let Some(resolved_node) = registry.resolve(alias_name) else {
348 return;
349 };
350
351 // Get the resolved mapping and merge its keys
352 if let Some(resolved_mapping) = crate::yaml::Mapping::cast(resolved_node.clone()) {
353 for (src_key, src_value) in resolved_mapping.iter() {
354 let Some(k_str) = node_as_string(&src_key) else {
355 continue;
356 };
357 // Convert YamlNode back to YamlValue for storage; insert or override
358 if let Some(yaml_value) = YamlValue::cast(src_value.syntax().clone()) {
359 merged_pairs.insert(k_str, yaml_value);
360 }
361 }
362 }
363}