Skip to main content

yaml_edit/nodes/
alias_node.rs

1use super::{Lang, SyntaxNode};
2use crate::as_yaml::{AsYaml, YamlKind};
3use crate::lex::SyntaxKind;
4use rowan::ast::AstNode;
5use rowan::GreenNodeBuilder;
6
7ast_node!(Alias, ALIAS, "A YAML alias reference (e.g., *anchor_name)");
8
9impl Alias {
10    /// Construct an alias that serializes as `*name`.
11    ///
12    /// `name` is the anchor name without the `*` prefix. The node implements
13    /// [`AsYaml`], so it can be passed to [`Mapping::set`](crate::Mapping::set)
14    /// and other mutation APIs without parsing a snippet.
15    ///
16    /// # Panics
17    ///
18    /// Panics if `name` is empty or contains whitespace or YAML special
19    /// characters (`:+-?[]{},'|>&*!%"#`). Such names would not round-trip
20    /// through the lexer as a single alias token.
21    ///
22    /// # Examples
23    ///
24    /// ```
25    /// use std::str::FromStr;
26    /// use yaml_edit::{Alias, Document};
27    ///
28    /// let doc = Document::from_str("shared: &shared value\nservice_a: old\n").unwrap();
29    /// let mapping = doc.as_mapping().unwrap();
30    /// mapping.set("service_a", Alias::new("shared"));
31    /// assert_eq!(doc.to_string(), "shared: &shared value\nservice_a: *shared\n");
32    /// ```
33    pub fn new(name: impl AsRef<str>) -> Self {
34        let name = name.as_ref();
35        assert!(
36            crate::lex::is_valid_anchor_name(name),
37            "alias name must be non-empty and contain no whitespace or YAML special characters, got {name:?}"
38        );
39        let text = format!("*{name}");
40        let mut builder = GreenNodeBuilder::new();
41        builder.start_node(SyntaxKind::ALIAS.into());
42        builder.token(SyntaxKind::REFERENCE.into(), &text);
43        builder.finish_node();
44        Alias(SyntaxNode::new_root_mut(builder.finish()))
45    }
46
47    /// Get the full text of this alias including the `*` prefix
48    pub fn value(&self) -> String {
49        self.0.text().to_string()
50    }
51
52    /// Get the anchor name this alias refers to (without the `*` prefix)
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use yaml_edit::Document;
58    /// use std::str::FromStr;
59    ///
60    /// let yaml = r#"
61    /// anchor: &my_anchor
62    ///   key: value
63    /// reference: *my_anchor
64    /// "#;
65    ///
66    /// let doc = Document::from_str(yaml).unwrap();
67    /// let mapping = doc.as_mapping().unwrap();
68    /// let reference = mapping.get("reference").unwrap();
69    ///
70    /// if let yaml_edit::YamlNode::Alias(alias) = reference {
71    ///     assert_eq!(alias.name(), "my_anchor");
72    /// }
73    /// ```
74    pub fn name(&self) -> String {
75        let text = self.value();
76        // Remove the leading '*' character
77        if let Some(stripped) = text.strip_prefix('*') {
78            stripped.to_string()
79        } else {
80            text
81        }
82    }
83}
84
85impl AsYaml for Alias {
86    fn as_node(&self) -> Option<&SyntaxNode> {
87        Some(&self.0)
88    }
89
90    fn kind(&self) -> YamlKind {
91        YamlKind::Alias
92    }
93
94    fn build_content(
95        &self,
96        builder: &mut rowan::GreenNodeBuilder,
97        _indent: usize,
98        _flow_context: bool,
99    ) -> bool {
100        builder.start_node(SyntaxKind::ALIAS.into());
101        crate::as_yaml::copy_node_content(builder, &self.0);
102        builder.finish_node();
103        false
104    }
105
106    fn is_inline(&self) -> bool {
107        true
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use crate::{Document, YamlNode};
114    use std::str::FromStr;
115
116    #[test]
117    fn test_simple_alias() {
118        let yaml = r#"
119anchor: &my_anchor value
120reference: *my_anchor
121"#;
122
123        let doc = Document::from_str(yaml).unwrap();
124        let mapping = doc.as_mapping().unwrap();
125
126        // Get the reference which should be an alias
127        let reference = mapping
128            .get("reference")
129            .expect("Should have 'reference' key");
130
131        match reference {
132            YamlNode::Alias(alias) => {
133                assert_eq!(alias.name(), "my_anchor");
134                assert_eq!(alias.value(), "*my_anchor");
135            }
136            _ => panic!("Expected alias, got {:?}", reference),
137        }
138    }
139
140    #[test]
141    fn test_alias_in_sequence() {
142        let yaml = r#"
143colors:
144  - &red '#FF0000'
145  - &green '#00FF00'
146  - &blue '#0000FF'
147
148theme:
149  - *red
150  - *blue
151"#;
152
153        let doc = Document::from_str(yaml).unwrap();
154        let mapping = doc.as_mapping().unwrap();
155
156        let theme = mapping.get("theme").expect("Should have 'theme' key");
157        let theme_seq = theme.as_sequence().expect("Should be a sequence");
158        assert_eq!(theme_seq.len(), 2);
159
160        // First element should be an alias to 'red'
161        let first = theme_seq.get(0).unwrap();
162        match first {
163            YamlNode::Alias(alias) => {
164                assert_eq!(alias.name(), "red");
165            }
166            _ => panic!("Expected alias, got {:?}", first),
167        }
168
169        // Second element should be an alias to 'blue'
170        let second = theme_seq.get(1).unwrap();
171        match second {
172            YamlNode::Alias(alias) => {
173                assert_eq!(alias.name(), "blue");
174            }
175            _ => panic!("Expected alias, got {:?}", second),
176        }
177    }
178
179    #[test]
180    fn test_alias_in_mapping() {
181        let yaml = r#"
182defaults: &defaults
183  adapter: postgres
184  host: localhost
185
186development:
187  database: dev_db
188  <<: *defaults
189"#;
190
191        let doc = Document::from_str(yaml).unwrap();
192        let mapping = doc.as_mapping().unwrap();
193
194        let development = mapping
195            .get("development")
196            .expect("Should have 'development' key");
197        let dev_mapping = development.as_mapping().expect("Should be a mapping");
198
199        // Find the merge key entry by iterating (can't use get() since << is MERGE_KEY token, not STRING)
200        let merge_entry = dev_mapping
201            .iter()
202            .find(|(k, _)| k.as_scalar().is_some_and(|s| s.as_string() == "<<"))
203            .expect("Should have merge key entry");
204
205        // The value should be an alias
206        match merge_entry.1 {
207            YamlNode::Alias(alias) => {
208                assert_eq!(alias.name(), "defaults");
209                assert_eq!(alias.value(), "*defaults");
210            }
211            _ => panic!("Expected alias for merge key, got {:?}", merge_entry.1),
212        }
213    }
214
215    #[test]
216    fn test_multiple_aliases_to_same_anchor() {
217        let yaml = r#"
218value: &shared 42
219first: *shared
220second: *shared
221third: *shared
222"#;
223
224        let doc = Document::from_str(yaml).unwrap();
225        let mapping = doc.as_mapping().unwrap();
226
227        for key in &["first", "second", "third"] {
228            let node = mapping
229                .get(key)
230                .unwrap_or_else(|| panic!("Should have '{}' key", key));
231            match node {
232                YamlNode::Alias(alias) => {
233                    assert_eq!(alias.name(), "shared");
234                }
235                _ => panic!("Expected alias for '{}', got {:?}", key, node),
236            }
237        }
238    }
239
240    #[test]
241    fn test_alias_round_trip() {
242        let yaml = r#"anchor: &test value
243reference: *test"#;
244
245        let doc = Document::from_str(yaml).unwrap();
246        let output = doc.to_string();
247
248        // Should preserve exact formatting
249        assert_eq!(output, yaml);
250    }
251
252    #[test]
253    fn test_alias_with_complex_anchor_name() {
254        let yaml = r#"
255data: &complex_anchor_name_123 some_value
256ref: *complex_anchor_name_123
257"#;
258
259        let doc = Document::from_str(yaml).unwrap();
260        let mapping = doc.as_mapping().unwrap();
261
262        let ref_node = mapping.get("ref").expect("Should have 'ref' key");
263        match ref_node {
264            YamlNode::Alias(alias) => {
265                assert_eq!(alias.name(), "complex_anchor_name_123");
266            }
267            _ => panic!("Expected alias, got {:?}", ref_node),
268        }
269    }
270
271    #[test]
272    fn test_nested_structure_with_aliases() {
273        let yaml = r#"
274base: &base
275  x: 1
276  y: 2
277
278items:
279  - name: first
280    config: *base
281  - name: second
282    config: *base
283"#;
284
285        let doc = Document::from_str(yaml).unwrap();
286        let mapping = doc.as_mapping().unwrap();
287
288        let items = mapping.get("items").expect("Should have 'items' key");
289        let items_seq = items.as_sequence().expect("Should be a sequence");
290        assert_eq!(items_seq.len(), 2);
291
292        // Check both items have alias to 'base'
293        for i in 0..2 {
294            let item = items_seq.get(i).unwrap();
295            let item_mapping = item.as_mapping().expect("Should be a mapping");
296            let config = item_mapping
297                .get("config")
298                .expect("Should have 'config' key");
299
300            match config {
301                YamlNode::Alias(alias) => {
302                    assert_eq!(alias.name(), "base");
303                }
304                _ => panic!("Expected alias for item {}, got {:?}", i, config),
305            }
306        }
307    }
308
309    #[test]
310    fn test_alias_preserves_whitespace() {
311        // Test that whitespace around alias is preserved
312        let yaml = "reference:  *test  ";
313
314        let doc = Document::from_str(yaml).unwrap();
315        let output = doc.to_string();
316
317        // Should preserve the exact whitespace
318        assert_eq!(output, yaml);
319    }
320
321    #[test]
322    fn test_alias_new_name_and_value() {
323        let alias = crate::Alias::new("shared");
324        assert_eq!(alias.name(), "shared");
325        assert_eq!(alias.value(), "*shared");
326        assert_eq!(alias.to_string(), "*shared");
327    }
328
329    #[test]
330    fn test_alias_new_set_replaces_value_and_keeps_anchor() {
331        use crate::Alias;
332
333        let doc = Document::from_str("shared: &shared value\nservice_a: old\n").unwrap();
334        let mapping = doc.as_mapping().unwrap();
335        mapping.set("service_a", Alias::new("shared"));
336        assert_eq!(
337            doc.to_string(),
338            "shared: &shared value\nservice_a: *shared\n"
339        );
340
341        let service_a = mapping.get("service_a").unwrap();
342        match service_a {
343            YamlNode::Alias(alias) => {
344                assert_eq!(alias.name(), "shared");
345                assert_eq!(alias.value(), "*shared");
346            }
347            other => panic!("Expected alias, got {:?}", other),
348        }
349    }
350
351    #[test]
352    fn test_alias_new_set_into_sequence() {
353        use crate::Alias;
354
355        let doc = Document::from_str("colors:\n  - &red '#FF0000'\n  - old\n").unwrap();
356        let mapping = doc.as_mapping().unwrap();
357        let colors_node = mapping.get("colors").unwrap();
358        let colors = colors_node.as_sequence().unwrap();
359        colors.set(1, Alias::new("red"));
360        assert_eq!(doc.to_string(), "colors:\n  - &red '#FF0000'\n  - *red\n");
361    }
362
363    #[test]
364    #[should_panic(expected = "alias name must be non-empty")]
365    fn test_alias_new_rejects_empty() {
366        let _ = crate::Alias::new("");
367    }
368
369    #[test]
370    #[should_panic(expected = "alias name must be non-empty")]
371    fn test_alias_new_rejects_whitespace() {
372        let _ = crate::Alias::new("bad name");
373    }
374
375    #[test]
376    #[should_panic(expected = "alias name must be non-empty")]
377    fn test_alias_new_rejects_leading_star() {
378        let _ = crate::Alias::new("*already_starred");
379    }
380
381    #[test]
382    #[should_panic(expected = "alias name must be non-empty")]
383    fn test_alias_new_rejects_flow_indicator() {
384        let _ = crate::Alias::new("bad,name");
385    }
386
387    #[test]
388    fn test_alias_new_accepts_unicode_letters() {
389        let alias = crate::Alias::new("naïve_anchor_ij");
390        assert_eq!(alias.name(), "naïve_anchor_ij");
391        assert_eq!(alias.value(), "*naïve_anchor_ij");
392    }
393}