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;
5
6ast_node!(Alias, ALIAS, "A YAML alias reference (e.g., *anchor_name)");
7
8impl Alias {
9    /// Get the full text of this alias including the `*` prefix
10    pub fn value(&self) -> String {
11        self.0.text().to_string()
12    }
13
14    /// Get the anchor name this alias refers to (without the `*` prefix)
15    ///
16    /// # Examples
17    ///
18    /// ```
19    /// use yaml_edit::Document;
20    /// use std::str::FromStr;
21    ///
22    /// let yaml = r#"
23    /// anchor: &my_anchor
24    ///   key: value
25    /// reference: *my_anchor
26    /// "#;
27    ///
28    /// let doc = Document::from_str(yaml).unwrap();
29    /// let mapping = doc.as_mapping().unwrap();
30    /// let reference = mapping.get("reference").unwrap();
31    ///
32    /// if let yaml_edit::YamlNode::Alias(alias) = reference {
33    ///     assert_eq!(alias.name(), "my_anchor");
34    /// }
35    /// ```
36    pub fn name(&self) -> String {
37        let text = self.value();
38        // Remove the leading '*' character
39        if let Some(stripped) = text.strip_prefix('*') {
40            stripped.to_string()
41        } else {
42            text
43        }
44    }
45}
46
47impl AsYaml for Alias {
48    fn as_node(&self) -> Option<&SyntaxNode> {
49        Some(&self.0)
50    }
51
52    fn kind(&self) -> YamlKind {
53        YamlKind::Alias
54    }
55
56    fn build_content(
57        &self,
58        builder: &mut rowan::GreenNodeBuilder,
59        _indent: usize,
60        _flow_context: bool,
61    ) -> bool {
62        crate::as_yaml::copy_node_content(builder, &self.0);
63        // Aliases don't end with newlines
64        false
65    }
66
67    fn is_inline(&self) -> bool {
68        true
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use crate::{Document, YamlNode};
75    use std::str::FromStr;
76
77    #[test]
78    fn test_simple_alias() {
79        let yaml = r#"
80anchor: &my_anchor value
81reference: *my_anchor
82"#;
83
84        let doc = Document::from_str(yaml).unwrap();
85        let mapping = doc.as_mapping().unwrap();
86
87        // Get the reference which should be an alias
88        let reference = mapping
89            .get("reference")
90            .expect("Should have 'reference' key");
91
92        match reference {
93            YamlNode::Alias(alias) => {
94                assert_eq!(alias.name(), "my_anchor");
95                assert_eq!(alias.value(), "*my_anchor");
96            }
97            _ => panic!("Expected alias, got {:?}", reference),
98        }
99    }
100
101    #[test]
102    fn test_alias_in_sequence() {
103        let yaml = r#"
104colors:
105  - &red '#FF0000'
106  - &green '#00FF00'
107  - &blue '#0000FF'
108
109theme:
110  - *red
111  - *blue
112"#;
113
114        let doc = Document::from_str(yaml).unwrap();
115        let mapping = doc.as_mapping().unwrap();
116
117        let theme = mapping.get("theme").expect("Should have 'theme' key");
118        let theme_seq = theme.as_sequence().expect("Should be a sequence");
119        assert_eq!(theme_seq.len(), 2);
120
121        // First element should be an alias to 'red'
122        let first = theme_seq.get(0).unwrap();
123        match first {
124            YamlNode::Alias(alias) => {
125                assert_eq!(alias.name(), "red");
126            }
127            _ => panic!("Expected alias, got {:?}", first),
128        }
129
130        // Second element should be an alias to 'blue'
131        let second = theme_seq.get(1).unwrap();
132        match second {
133            YamlNode::Alias(alias) => {
134                assert_eq!(alias.name(), "blue");
135            }
136            _ => panic!("Expected alias, got {:?}", second),
137        }
138    }
139
140    #[test]
141    fn test_alias_in_mapping() {
142        let yaml = r#"
143defaults: &defaults
144  adapter: postgres
145  host: localhost
146
147development:
148  database: dev_db
149  <<: *defaults
150"#;
151
152        let doc = Document::from_str(yaml).unwrap();
153        let mapping = doc.as_mapping().unwrap();
154
155        let development = mapping
156            .get("development")
157            .expect("Should have 'development' key");
158        let dev_mapping = development.as_mapping().expect("Should be a mapping");
159
160        // Find the merge key entry by iterating (can't use get() since << is MERGE_KEY token, not STRING)
161        let merge_entry = dev_mapping
162            .iter()
163            .find(|(k, _)| {
164                k.as_scalar()
165                    .map(|s| s.as_string() == "<<")
166                    .unwrap_or(false)
167            })
168            .expect("Should have merge key entry");
169
170        // The value should be an alias
171        match merge_entry.1 {
172            YamlNode::Alias(alias) => {
173                assert_eq!(alias.name(), "defaults");
174                assert_eq!(alias.value(), "*defaults");
175            }
176            _ => panic!("Expected alias for merge key, got {:?}", merge_entry.1),
177        }
178    }
179
180    #[test]
181    fn test_multiple_aliases_to_same_anchor() {
182        let yaml = r#"
183value: &shared 42
184first: *shared
185second: *shared
186third: *shared
187"#;
188
189        let doc = Document::from_str(yaml).unwrap();
190        let mapping = doc.as_mapping().unwrap();
191
192        for key in &["first", "second", "third"] {
193            let node = mapping
194                .get(key)
195                .unwrap_or_else(|| panic!("Should have '{}' key", key));
196            match node {
197                YamlNode::Alias(alias) => {
198                    assert_eq!(alias.name(), "shared");
199                }
200                _ => panic!("Expected alias for '{}', got {:?}", key, node),
201            }
202        }
203    }
204
205    #[test]
206    fn test_alias_round_trip() {
207        let yaml = r#"anchor: &test value
208reference: *test"#;
209
210        let doc = Document::from_str(yaml).unwrap();
211        let output = doc.to_string();
212
213        // Should preserve exact formatting
214        assert_eq!(output, yaml);
215    }
216
217    #[test]
218    fn test_alias_with_complex_anchor_name() {
219        let yaml = r#"
220data: &complex_anchor_name_123 some_value
221ref: *complex_anchor_name_123
222"#;
223
224        let doc = Document::from_str(yaml).unwrap();
225        let mapping = doc.as_mapping().unwrap();
226
227        let ref_node = mapping.get("ref").expect("Should have 'ref' key");
228        match ref_node {
229            YamlNode::Alias(alias) => {
230                assert_eq!(alias.name(), "complex_anchor_name_123");
231            }
232            _ => panic!("Expected alias, got {:?}", ref_node),
233        }
234    }
235
236    #[test]
237    fn test_nested_structure_with_aliases() {
238        let yaml = r#"
239base: &base
240  x: 1
241  y: 2
242
243items:
244  - name: first
245    config: *base
246  - name: second
247    config: *base
248"#;
249
250        let doc = Document::from_str(yaml).unwrap();
251        let mapping = doc.as_mapping().unwrap();
252
253        let items = mapping.get("items").expect("Should have 'items' key");
254        let items_seq = items.as_sequence().expect("Should be a sequence");
255        assert_eq!(items_seq.len(), 2);
256
257        // Check both items have alias to 'base'
258        for i in 0..2 {
259            let item = items_seq.get(i).unwrap();
260            let item_mapping = item.as_mapping().expect("Should be a mapping");
261            let config = item_mapping
262                .get("config")
263                .expect("Should have 'config' key");
264
265            match config {
266                YamlNode::Alias(alias) => {
267                    assert_eq!(alias.name(), "base");
268                }
269                _ => panic!("Expected alias for item {}, got {:?}", i, config),
270            }
271        }
272    }
273
274    #[test]
275    fn test_alias_preserves_whitespace() {
276        // Test that whitespace around alias is preserved
277        let yaml = "reference:  *test  ";
278
279        let doc = Document::from_str(yaml).unwrap();
280        let output = doc.to_string();
281
282        // Should preserve the exact whitespace
283        assert_eq!(output, yaml);
284    }
285}