use crate::as_yaml::AsYaml;
use crate::lex::SyntaxKind;
use crate::value::YamlValue;
use crate::yaml::SyntaxNode;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct AnchorRegistry {
anchors: HashMap<String, SyntaxNode>,
}
impl AnchorRegistry {
pub fn new() -> Self {
Self {
anchors: HashMap::new(),
}
}
pub fn from_document(doc: &crate::yaml::Document) -> Self {
if let Some(node) = doc.as_node() {
Self::from_tree(node)
} else {
Self::new()
}
}
pub fn from_tree(root: &SyntaxNode) -> Self {
let mut registry = Self::new();
registry.collect_anchors_from_tree(root);
registry
}
fn collect_anchors_from_tree(&mut self, node: &SyntaxNode) {
for child in node.children_with_tokens() {
if let Some(token) = child.as_token() {
if token.kind() == SyntaxKind::ANCHOR {
let text = token.text();
if let Some(name) = text.strip_prefix('&') {
if let Some(value_node) = self.find_anchored_value(node) {
self.anchors.insert(name.to_string(), value_node);
}
}
}
} else if let Some(child_node) = child.as_node() {
self.collect_anchors_from_tree(child_node);
}
}
}
fn find_anchored_value(&self, node: &SyntaxNode) -> Option<SyntaxNode> {
for child in node.children() {
if matches!(
child.kind(),
SyntaxKind::VALUE
| SyntaxKind::SCALAR
| SyntaxKind::MAPPING
| SyntaxKind::SEQUENCE
| SyntaxKind::TAGGED_NODE
) {
return Some(child);
}
}
None
}
pub fn resolve(&self, name: &str) -> Option<&SyntaxNode> {
self.anchors.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.anchors.contains_key(name)
}
pub fn anchor_names(&self) -> impl Iterator<Item = &str> {
self.anchors.keys().map(|s| s.as_str())
}
}
impl Default for AnchorRegistry {
fn default() -> Self {
Self::new()
}
}
pub trait DocumentResolvedExt {
fn get_resolved(&self, key: impl crate::AsYaml) -> Option<YamlValue>;
fn build_anchor_registry(&self) -> AnchorRegistry;
}
impl DocumentResolvedExt for crate::yaml::Document {
fn get_resolved(&self, key: impl crate::AsYaml) -> Option<YamlValue> {
use rowan::ast::AstNode;
let registry = self.build_anchor_registry();
let mapping = self.as_mapping()?;
let value = mapping
.get_node(&key)
.and_then(crate::value::YamlValue::cast)?;
if let Some(node) = mapping.get_node(&key) {
if let Some(alias_name) = find_alias_reference(&node) {
if let Some(resolved_node) = registry.resolve(&alias_name) {
return YamlValue::cast(resolved_node.clone());
}
}
}
if let Some(node) = mapping.get_node(&key) {
if let Some(result_mapping) = crate::yaml::Mapping::cast(node) {
if has_merge_keys(&result_mapping) {
return Some(YamlValue::Mapping(apply_merge_keys(
&result_mapping,
®istry,
)));
}
}
}
Some(value)
}
fn build_anchor_registry(&self) -> AnchorRegistry {
AnchorRegistry::from_document(self)
}
}
fn find_alias_reference(node: &SyntaxNode) -> Option<String> {
for child in node.children_with_tokens() {
if let Some(token) = child.as_token() {
if token.kind() == SyntaxKind::REFERENCE {
let text = token.text();
return text.strip_prefix('*').map(|s| s.to_string());
}
}
}
if let Some(parent) = node.parent() {
for child in parent.children_with_tokens() {
if let Some(token) = child.as_token() {
if token.kind() == SyntaxKind::REFERENCE {
let text = token.text();
return text.strip_prefix('*').map(|s| s.to_string());
}
}
}
}
None
}
fn node_as_string(node: &crate::as_yaml::YamlNode) -> Option<String> {
node.as_scalar().map(|s| s.as_string())
}
fn has_merge_keys(mapping: &crate::yaml::Mapping) -> bool {
for (key, _) in mapping.iter() {
if node_as_string(&key).as_deref() == Some("<<") {
return true;
}
}
false
}
fn apply_merge_keys(
mapping: &crate::yaml::Mapping,
registry: &AnchorRegistry,
) -> std::collections::BTreeMap<String, YamlValue> {
use crate::as_yaml::YamlNode;
use std::collections::BTreeMap;
let mut merged_pairs: BTreeMap<String, YamlValue> = BTreeMap::new();
for (key, value) in mapping.iter() {
let Some(key_str) = node_as_string(&key) else {
continue;
};
if key_str != "<<" {
continue;
}
match &value {
YamlNode::Scalar(_) => {
let Some(alias_text) = node_as_string(&value) else {
continue;
};
let Some(alias_name) = alias_text.strip_prefix('*') else {
continue;
};
merge_from_alias(&mut merged_pairs, alias_name, registry);
}
YamlNode::Sequence(seq) => {
for alias_node in seq.values() {
let Some(alias_text) = node_as_string(&alias_node) else {
continue;
};
let Some(alias_name) = alias_text.strip_prefix('*') else {
continue;
};
merge_from_alias(&mut merged_pairs, alias_name, registry);
}
}
_ => continue,
}
}
for (key, value) in mapping.iter() {
let Some(key_str) = node_as_string(&key) else {
continue;
};
if key_str == "<<" {
continue;
}
if let Some(yaml_value) = YamlValue::cast(value.syntax().clone()) {
merged_pairs.insert(key_str, yaml_value);
}
}
merged_pairs
}
fn merge_from_alias(
merged_pairs: &mut std::collections::BTreeMap<String, YamlValue>,
alias_name: &str,
registry: &AnchorRegistry,
) {
use rowan::ast::AstNode;
let Some(resolved_node) = registry.resolve(alias_name) else {
return;
};
if let Some(resolved_mapping) = crate::yaml::Mapping::cast(resolved_node.clone()) {
for (src_key, src_value) in resolved_mapping.iter() {
let Some(k_str) = node_as_string(&src_key) else {
continue;
};
if let Some(yaml_value) = YamlValue::cast(src_value.syntax().clone()) {
merged_pairs.insert(k_str, yaml_value);
}
}
}
}
#[derive(Clone)]
pub struct MergedMapping<'a> {
base: crate::yaml::Mapping,
registry: &'a AnchorRegistry,
}
impl<'a> MergedMapping<'a> {
pub fn new(base: crate::yaml::Mapping, registry: &'a AnchorRegistry) -> Self {
Self { base, registry }
}
pub fn base(&self) -> &crate::yaml::Mapping {
&self.base
}
pub fn registry(&self) -> &AnchorRegistry {
self.registry
}
pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
if is_merge_key(&key) {
return None;
}
if let Some(node) = self.base.get(&key) {
return Some(resolve_alias_node(node, self.registry));
}
for source in merge_sources(&self.base, self.registry) {
if let Some(node) = source.get(&key) {
return Some(resolve_alias_node(node, self.registry));
}
}
None
}
pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
self.get(key).is_some()
}
pub fn iter(
&self,
) -> impl Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + '_ {
let mut seen: Vec<String> = Vec::new();
let mut out: Vec<(crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> = Vec::new();
for (key, value) in self.base.iter() {
let Some(key_str) = node_as_string(&key) else {
continue;
};
if key_str == "<<" {
continue;
}
seen.push(key_str);
out.push((key, resolve_alias_node(value, self.registry)));
}
for source in merge_sources(&self.base, self.registry) {
for (key, value) in source.iter() {
let Some(key_str) = node_as_string(&key) else {
continue;
};
if key_str == "<<" || seen.contains(&key_str) {
continue;
}
seen.push(key_str);
out.push((key, resolve_alias_node(value, self.registry)));
}
}
out.into_iter()
}
pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
self.iter().map(|(k, _)| k)
}
pub fn values(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
self.iter().map(|(_, v)| v)
}
pub fn len(&self) -> usize {
self.iter().count()
}
pub fn is_empty(&self) -> bool {
self.iter().next().is_none()
}
pub fn get_merged(&self, key: impl crate::AsYaml) -> Option<MergedMapping<'a>> {
let node = self.get(key)?;
let mapping = node.as_mapping().cloned()?;
Some(MergedMapping::new(mapping, self.registry))
}
}
impl std::fmt::Debug for MergedMapping<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MergedMapping")
.field("len", &self.len())
.finish()
}
}
fn is_merge_key(key: &impl crate::AsYaml) -> bool {
use crate::as_yaml::YamlNode;
if let Some(node) = key.as_node() {
if let Some(yaml) = YamlNode::from_syntax_peeled(node.clone()) {
return node_as_string(&yaml).as_deref() == Some("<<");
}
}
let mut builder = rowan::GreenNodeBuilder::new();
builder.start_node(crate::lex::SyntaxKind::ROOT.into());
key.build_content(&mut builder, 0, true);
builder.finish_node();
let root = crate::yaml::SyntaxNode::new_root(builder.finish());
root.text().to_string().trim() == "<<"
}
fn merge_sources(
base: &crate::yaml::Mapping,
registry: &AnchorRegistry,
) -> Vec<crate::yaml::Mapping> {
use crate::as_yaml::YamlNode;
use rowan::ast::AstNode;
let mut sources = Vec::new();
for (key, value) in base.iter() {
if node_as_string(&key).as_deref() != Some("<<") {
continue;
}
match &value {
YamlNode::Scalar(_) => {
if let Some(alias_text) = node_as_string(&value) {
if let Some(alias_name) = alias_text.strip_prefix('*') {
if let Some(target) = registry.resolve(alias_name) {
if let Some(m) = crate::yaml::Mapping::cast(target.clone()) {
sources.push(m);
}
}
}
}
}
YamlNode::Sequence(seq) => {
for item in seq.values() {
let alias_name = match &item {
YamlNode::Alias(a) => a.name(),
YamlNode::Scalar(_) => {
let Some(text) = node_as_string(&item) else {
continue;
};
let Some(name) = text.strip_prefix('*') else {
continue;
};
name.to_string()
}
_ => continue,
};
let Some(target) = registry.resolve(&alias_name) else {
continue;
};
if let Some(m) = crate::yaml::Mapping::cast(target.clone()) {
sources.push(m);
}
}
}
YamlNode::Alias(alias) => {
if let Some(target) = registry.resolve(&alias.name()) {
if let Some(m) = crate::yaml::Mapping::cast(target.clone()) {
sources.push(m);
}
}
}
_ => continue,
}
}
sources
}
fn resolve_alias_node(
node: crate::as_yaml::YamlNode,
registry: &AnchorRegistry,
) -> crate::as_yaml::YamlNode {
use crate::as_yaml::YamlNode;
if let YamlNode::Alias(alias) = &node {
if let Some(target) = registry.resolve(&alias.name()) {
if let Some(resolved) = YamlNode::from_syntax_peeled(target.clone()) {
return resolved;
}
}
}
node
}
pub trait MappingMergedExt {
fn merged<'a>(&self, registry: &'a AnchorRegistry) -> MergedMapping<'a>;
}
impl MappingMergedExt for crate::yaml::Mapping {
fn merged<'a>(&self, registry: &'a AnchorRegistry) -> MergedMapping<'a> {
MergedMapping::new(self.clone(), registry)
}
}
pub trait DocumentMergedExt {
fn merged(&self) -> Option<MergedView>;
}
impl DocumentMergedExt for crate::yaml::Document {
fn merged(&self) -> Option<MergedView> {
let mapping = self.as_mapping()?;
let registry = self.build_anchor_registry();
Some(MergedView { mapping, registry })
}
}
pub struct MergedView {
mapping: crate::yaml::Mapping,
registry: AnchorRegistry,
}
impl MergedView {
pub fn as_mapping(&self) -> MergedMapping<'_> {
MergedMapping::new(self.mapping.clone(), &self.registry)
}
pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
self.as_mapping().get(key)
}
pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
self.as_mapping().contains_key(key)
}
pub fn registry(&self) -> &AnchorRegistry {
&self.registry
}
}
impl std::fmt::Debug for MergedView {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MergedView").finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Document;
use std::str::FromStr;
fn doc(text: &str) -> Document {
Document::from_str(text).expect("parse")
}
#[test]
fn merged_basic_merge_key() {
let yaml = "\
defaults: &d
timeout: 30
retries: 3
prod:
<<: *d
host: prod.example.com
";
let d = doc(yaml);
let root = d.as_mapping().unwrap();
let reg = d.build_anchor_registry();
let prod = root.get_mapping("prod").unwrap();
let m = prod.merged(®);
assert_eq!(m.get("timeout").unwrap().to_i64(), Some(30));
assert_eq!(m.get("retries").unwrap().to_i64(), Some(3));
assert_eq!(
m.get("host").unwrap().as_scalar().unwrap().as_string(),
"prod.example.com"
);
}
#[test]
fn merged_direct_key_wins() {
let yaml = "\
defaults: &d
timeout: 30
prod:
<<: *d
timeout: 60
";
let d = doc(yaml);
let reg = d.build_anchor_registry();
let prod = d.as_mapping().unwrap().get_mapping("prod").unwrap();
let m = prod.merged(®);
assert_eq!(m.get("timeout").unwrap().to_i64(), Some(60));
}
#[test]
fn merged_merge_key_itself_is_hidden() {
let yaml = "\
d: &d
a: 1
m:
<<: *d
";
let d = doc(yaml);
let reg = d.build_anchor_registry();
let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
let m = inner.merged(®);
assert!(m.get("<<").is_none());
let keys: Vec<String> = m
.keys()
.map(|k| k.as_scalar().unwrap().as_string())
.collect();
assert_eq!(keys, vec!["a"]);
}
#[test]
fn merged_sequence_of_aliases() {
let yaml = "\
a: &a
x: 1
y: 1
b: &b
y: 2
z: 2
m:
<<: [*a, *b]
";
let d = doc(yaml);
let reg = d.build_anchor_registry();
let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
let m = inner.merged(®);
assert_eq!(m.get("x").unwrap().to_i64(), Some(1));
assert_eq!(m.get("y").unwrap().to_i64(), Some(1));
assert_eq!(m.get("z").unwrap().to_i64(), Some(2));
}
#[test]
fn merged_missing_alias_returns_none() {
let yaml = "\
m:
<<: *nope
a: 1
";
let d = doc(yaml);
let reg = d.build_anchor_registry();
let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
let m = inner.merged(®);
assert_eq!(m.get("a").unwrap().to_i64(), Some(1));
assert!(m.get("b").is_none());
}
#[test]
fn merged_iter_order_direct_then_merged() {
let yaml = "\
d: &d
shared: from_d
only_in_d: 1
m:
direct: hi
<<: *d
";
let d = doc(yaml);
let reg = d.build_anchor_registry();
let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
let m = inner.merged(®);
let keys: Vec<String> = m
.iter()
.map(|(k, _)| k.as_scalar().unwrap().as_string())
.collect();
assert_eq!(keys, vec!["direct", "shared", "only_in_d"]);
}
#[test]
fn merged_len_and_empty() {
let yaml = "\
d: &d
a: 1
b: 2
m:
<<: *d
c: 3
";
let d = doc(yaml);
let reg = d.build_anchor_registry();
let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
let m = inner.merged(®);
assert_eq!(m.len(), 3);
assert!(!m.is_empty());
}
#[test]
fn merged_alias_value_is_resolved() {
let yaml = "\
target: &t
k: 42
m:
ref: *t
";
let d = doc(yaml);
let reg = d.build_anchor_registry();
let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
let m = inner.merged(®);
let r = m.get("ref").unwrap();
let nested = r.as_mapping().unwrap();
assert_eq!(nested.get("k").unwrap().to_i64(), Some(42));
}
#[test]
fn merged_get_merged_nested() {
let yaml = "\
defaults: &d
port: 80
prod:
<<: *d
inner:
a: 1
";
let d = doc(yaml);
let reg = d.build_anchor_registry();
let prod = d.as_mapping().unwrap().get_mapping("prod").unwrap();
let m = prod.merged(®);
let inner = m.get_merged("inner").unwrap();
assert_eq!(inner.get("a").unwrap().to_i64(), Some(1));
}
#[test]
fn document_merged_view() {
let yaml = "\
defaults: &d
timeout: 30
shared:
<<: *d
retries: 5
";
let d = doc(yaml);
let view = d.merged().unwrap();
assert!(view.contains_key("defaults"));
assert!(view.contains_key("shared"));
let m = view.as_mapping();
let shared = m.get_merged("shared").unwrap();
assert_eq!(shared.get("timeout").unwrap().to_i64(), Some(30));
assert_eq!(shared.get("retries").unwrap().to_i64(), Some(5));
}
#[test]
fn merged_does_not_mutate_underlying_cst() {
let yaml = "\
d: &d
a: 1
m:
<<: *d
b: 2
";
let d = doc(yaml);
let reg = d.build_anchor_registry();
let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
let _ = inner.merged(®).get("a");
assert_eq!(d.to_string(), yaml);
}
}