yaml_edit/nodes/
alias_node.rs1use 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 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 pub fn value(&self) -> String {
49 self.0.text().to_string()
50 }
51
52 pub fn name(&self) -> String {
75 let text = self.value();
76 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 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 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 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 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 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 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 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 let yaml = "reference: *test ";
313
314 let doc = Document::from_str(yaml).unwrap();
315 let output = doc.to_string();
316
317 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}