use super::{Lang, SyntaxNode};
use crate::as_yaml::{AsYaml, YamlKind};
use crate::lex::SyntaxKind;
use rowan::ast::AstNode;
use rowan::GreenNodeBuilder;
ast_node!(Alias, ALIAS, "A YAML alias reference (e.g., *anchor_name)");
impl Alias {
pub fn new(name: impl AsRef<str>) -> Self {
let name = name.as_ref();
assert!(
crate::lex::is_valid_anchor_name(name),
"alias name must be non-empty and contain no whitespace or YAML special characters, got {name:?}"
);
let text = format!("*{name}");
let mut builder = GreenNodeBuilder::new();
builder.start_node(SyntaxKind::ALIAS.into());
builder.token(SyntaxKind::REFERENCE.into(), &text);
builder.finish_node();
Alias(SyntaxNode::new_root_mut(builder.finish()))
}
pub fn value(&self) -> String {
self.0.text().to_string()
}
pub fn name(&self) -> String {
let text = self.value();
if let Some(stripped) = text.strip_prefix('*') {
stripped.to_string()
} else {
text
}
}
}
impl AsYaml for Alias {
fn as_node(&self) -> Option<&SyntaxNode> {
Some(&self.0)
}
fn kind(&self) -> YamlKind {
YamlKind::Alias
}
fn build_content(
&self,
builder: &mut rowan::GreenNodeBuilder,
_indent: usize,
_flow_context: bool,
) -> bool {
builder.start_node(SyntaxKind::ALIAS.into());
crate::as_yaml::copy_node_content(builder, &self.0);
builder.finish_node();
false
}
fn is_inline(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use crate::{Document, YamlNode};
use std::str::FromStr;
#[test]
fn test_simple_alias() {
let yaml = r#"
anchor: &my_anchor value
reference: *my_anchor
"#;
let doc = Document::from_str(yaml).unwrap();
let mapping = doc.as_mapping().unwrap();
let reference = mapping
.get("reference")
.expect("Should have 'reference' key");
match reference {
YamlNode::Alias(alias) => {
assert_eq!(alias.name(), "my_anchor");
assert_eq!(alias.value(), "*my_anchor");
}
_ => panic!("Expected alias, got {:?}", reference),
}
}
#[test]
fn test_alias_in_sequence() {
let yaml = r#"
colors:
- &red '#FF0000'
- &green '#00FF00'
- &blue '#0000FF'
theme:
- *red
- *blue
"#;
let doc = Document::from_str(yaml).unwrap();
let mapping = doc.as_mapping().unwrap();
let theme = mapping.get("theme").expect("Should have 'theme' key");
let theme_seq = theme.as_sequence().expect("Should be a sequence");
assert_eq!(theme_seq.len(), 2);
let first = theme_seq.get(0).unwrap();
match first {
YamlNode::Alias(alias) => {
assert_eq!(alias.name(), "red");
}
_ => panic!("Expected alias, got {:?}", first),
}
let second = theme_seq.get(1).unwrap();
match second {
YamlNode::Alias(alias) => {
assert_eq!(alias.name(), "blue");
}
_ => panic!("Expected alias, got {:?}", second),
}
}
#[test]
fn test_alias_in_mapping() {
let yaml = r#"
defaults: &defaults
adapter: postgres
host: localhost
development:
database: dev_db
<<: *defaults
"#;
let doc = Document::from_str(yaml).unwrap();
let mapping = doc.as_mapping().unwrap();
let development = mapping
.get("development")
.expect("Should have 'development' key");
let dev_mapping = development.as_mapping().expect("Should be a mapping");
let merge_entry = dev_mapping
.iter()
.find(|(k, _)| k.as_scalar().is_some_and(|s| s.as_string() == "<<"))
.expect("Should have merge key entry");
match merge_entry.1 {
YamlNode::Alias(alias) => {
assert_eq!(alias.name(), "defaults");
assert_eq!(alias.value(), "*defaults");
}
_ => panic!("Expected alias for merge key, got {:?}", merge_entry.1),
}
}
#[test]
fn test_multiple_aliases_to_same_anchor() {
let yaml = r#"
value: &shared 42
first: *shared
second: *shared
third: *shared
"#;
let doc = Document::from_str(yaml).unwrap();
let mapping = doc.as_mapping().unwrap();
for key in &["first", "second", "third"] {
let node = mapping
.get(key)
.unwrap_or_else(|| panic!("Should have '{}' key", key));
match node {
YamlNode::Alias(alias) => {
assert_eq!(alias.name(), "shared");
}
_ => panic!("Expected alias for '{}', got {:?}", key, node),
}
}
}
#[test]
fn test_alias_round_trip() {
let yaml = r#"anchor: &test value
reference: *test"#;
let doc = Document::from_str(yaml).unwrap();
let output = doc.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_alias_with_complex_anchor_name() {
let yaml = r#"
data: &complex_anchor_name_123 some_value
ref: *complex_anchor_name_123
"#;
let doc = Document::from_str(yaml).unwrap();
let mapping = doc.as_mapping().unwrap();
let ref_node = mapping.get("ref").expect("Should have 'ref' key");
match ref_node {
YamlNode::Alias(alias) => {
assert_eq!(alias.name(), "complex_anchor_name_123");
}
_ => panic!("Expected alias, got {:?}", ref_node),
}
}
#[test]
fn test_nested_structure_with_aliases() {
let yaml = r#"
base: &base
x: 1
y: 2
items:
- name: first
config: *base
- name: second
config: *base
"#;
let doc = Document::from_str(yaml).unwrap();
let mapping = doc.as_mapping().unwrap();
let items = mapping.get("items").expect("Should have 'items' key");
let items_seq = items.as_sequence().expect("Should be a sequence");
assert_eq!(items_seq.len(), 2);
for i in 0..2 {
let item = items_seq.get(i).unwrap();
let item_mapping = item.as_mapping().expect("Should be a mapping");
let config = item_mapping
.get("config")
.expect("Should have 'config' key");
match config {
YamlNode::Alias(alias) => {
assert_eq!(alias.name(), "base");
}
_ => panic!("Expected alias for item {}, got {:?}", i, config),
}
}
}
#[test]
fn test_alias_preserves_whitespace() {
let yaml = "reference: *test ";
let doc = Document::from_str(yaml).unwrap();
let output = doc.to_string();
assert_eq!(output, yaml);
}
#[test]
fn test_alias_new_name_and_value() {
let alias = crate::Alias::new("shared");
assert_eq!(alias.name(), "shared");
assert_eq!(alias.value(), "*shared");
assert_eq!(alias.to_string(), "*shared");
}
#[test]
fn test_alias_new_set_replaces_value_and_keeps_anchor() {
use crate::Alias;
let doc = Document::from_str("shared: &shared value\nservice_a: old\n").unwrap();
let mapping = doc.as_mapping().unwrap();
mapping.set("service_a", Alias::new("shared"));
assert_eq!(
doc.to_string(),
"shared: &shared value\nservice_a: *shared\n"
);
let service_a = mapping.get("service_a").unwrap();
match service_a {
YamlNode::Alias(alias) => {
assert_eq!(alias.name(), "shared");
assert_eq!(alias.value(), "*shared");
}
other => panic!("Expected alias, got {:?}", other),
}
}
#[test]
fn test_alias_new_set_into_sequence() {
use crate::Alias;
let doc = Document::from_str("colors:\n - &red '#FF0000'\n - old\n").unwrap();
let mapping = doc.as_mapping().unwrap();
let colors_node = mapping.get("colors").unwrap();
let colors = colors_node.as_sequence().unwrap();
colors.set(1, Alias::new("red"));
assert_eq!(doc.to_string(), "colors:\n - &red '#FF0000'\n - *red\n");
}
#[test]
#[should_panic(expected = "alias name must be non-empty")]
fn test_alias_new_rejects_empty() {
let _ = crate::Alias::new("");
}
#[test]
#[should_panic(expected = "alias name must be non-empty")]
fn test_alias_new_rejects_whitespace() {
let _ = crate::Alias::new("bad name");
}
#[test]
#[should_panic(expected = "alias name must be non-empty")]
fn test_alias_new_rejects_leading_star() {
let _ = crate::Alias::new("*already_starred");
}
#[test]
#[should_panic(expected = "alias name must be non-empty")]
fn test_alias_new_rejects_flow_indicator() {
let _ = crate::Alias::new("bad,name");
}
#[test]
fn test_alias_new_accepts_unicode_letters() {
let alias = crate::Alias::new("naïve_anchor_ij");
assert_eq!(alias.name(), "naïve_anchor_ij");
assert_eq!(alias.value(), "*naïve_anchor_ij");
}
}