1use crate::as_yaml::AsYaml;
50use crate::lex::SyntaxKind;
51use crate::value::YamlValue;
52use crate::yaml::SyntaxNode;
53use std::collections::HashMap;
54
55#[derive(Debug, Clone)]
57pub struct AnchorRegistry {
58 anchors: HashMap<String, SyntaxNode>,
60}
61
62impl AnchorRegistry {
63 pub fn new() -> Self {
65 Self {
66 anchors: HashMap::new(),
67 }
68 }
69
70 pub fn from_document(doc: &crate::yaml::Document) -> Self {
72 if let Some(node) = doc.as_node() {
73 Self::from_tree(node)
74 } else {
75 Self::new()
76 }
77 }
78
79 pub fn from_tree(root: &SyntaxNode) -> Self {
81 let mut registry = Self::new();
82 registry.collect_anchors_from_tree(root);
83 registry
84 }
85
86 fn collect_anchors_from_tree(&mut self, node: &SyntaxNode) {
88 for child in node.children_with_tokens() {
90 if let Some(token) = child.as_token() {
91 if token.kind() == SyntaxKind::ANCHOR {
92 let text = token.text();
94 if let Some(name) = text.strip_prefix('&') {
95 if let Some(value_node) = self.find_anchored_value(node) {
97 self.anchors.insert(name.to_string(), value_node);
98 }
99 }
100 }
101 } else if let Some(child_node) = child.as_node() {
102 self.collect_anchors_from_tree(child_node);
104 }
105 }
106 }
107
108 fn find_anchored_value(&self, node: &SyntaxNode) -> Option<SyntaxNode> {
110 for child in node.children() {
112 if matches!(
113 child.kind(),
114 SyntaxKind::VALUE
115 | SyntaxKind::SCALAR
116 | SyntaxKind::MAPPING
117 | SyntaxKind::SEQUENCE
118 | SyntaxKind::TAGGED_NODE
119 ) {
120 return Some(child);
121 }
122 }
123 None
124 }
125
126 pub fn resolve(&self, name: &str) -> Option<&SyntaxNode> {
128 self.anchors.get(name)
129 }
130
131 pub fn contains(&self, name: &str) -> bool {
133 self.anchors.contains_key(name)
134 }
135
136 pub fn anchor_names(&self) -> impl Iterator<Item = &str> {
138 self.anchors.keys().map(|s| s.as_str())
139 }
140}
141
142impl Default for AnchorRegistry {
143 fn default() -> Self {
144 Self::new()
145 }
146}
147
148pub trait DocumentResolvedExt {
150 fn get_resolved(&self, key: impl crate::AsYaml) -> Option<YamlValue>;
179
180 fn build_anchor_registry(&self) -> AnchorRegistry;
182}
183
184impl DocumentResolvedExt for crate::yaml::Document {
185 fn get_resolved(&self, key: impl crate::AsYaml) -> Option<YamlValue> {
186 use rowan::ast::AstNode;
187
188 let registry = self.build_anchor_registry();
190
191 let mapping = self.as_mapping()?;
193 let value = mapping
194 .get_node(&key)
195 .and_then(crate::value::YamlValue::cast)?;
196
197 if let Some(node) = mapping.get_node(&key) {
200 if let Some(alias_name) = find_alias_reference(&node) {
201 if let Some(resolved_node) = registry.resolve(&alias_name) {
203 return YamlValue::cast(resolved_node.clone());
205 }
206 }
207 }
208
209 if let Some(node) = mapping.get_node(&key) {
211 if let Some(result_mapping) = crate::yaml::Mapping::cast(node) {
212 if has_merge_keys(&result_mapping) {
213 return Some(YamlValue::Mapping(apply_merge_keys(
214 &result_mapping,
215 ®istry,
216 )));
217 }
218 }
219 }
220
221 Some(value)
222 }
223
224 fn build_anchor_registry(&self) -> AnchorRegistry {
225 AnchorRegistry::from_document(self)
226 }
227}
228
229fn find_alias_reference(node: &SyntaxNode) -> Option<String> {
231 for child in node.children_with_tokens() {
233 if let Some(token) = child.as_token() {
234 if token.kind() == SyntaxKind::REFERENCE {
235 let text = token.text();
236 return text.strip_prefix('*').map(|s| s.to_string());
238 }
239 }
240 }
241
242 if let Some(parent) = node.parent() {
244 for child in parent.children_with_tokens() {
245 if let Some(token) = child.as_token() {
246 if token.kind() == SyntaxKind::REFERENCE {
247 let text = token.text();
248 return text.strip_prefix('*').map(|s| s.to_string());
249 }
250 }
251 }
252 }
253
254 None
255}
256
257fn node_as_string(node: &crate::as_yaml::YamlNode) -> Option<String> {
259 node.as_scalar().map(|s| s.as_string())
260}
261
262fn has_merge_keys(mapping: &crate::yaml::Mapping) -> bool {
264 for (key, _) in mapping.iter() {
265 if node_as_string(&key).as_deref() == Some("<<") {
266 return true;
267 }
268 }
269 false
270}
271
272fn apply_merge_keys(
274 mapping: &crate::yaml::Mapping,
275 registry: &AnchorRegistry,
276) -> std::collections::BTreeMap<String, YamlValue> {
277 use crate::as_yaml::YamlNode;
278 use std::collections::BTreeMap;
279
280 let mut merged_pairs: BTreeMap<String, YamlValue> = BTreeMap::new();
281
282 for (key, value) in mapping.iter() {
284 let Some(key_str) = node_as_string(&key) else {
285 continue;
286 };
287 if key_str != "<<" {
288 continue;
289 }
290
291 match &value {
293 YamlNode::Scalar(_) => {
295 let Some(alias_text) = node_as_string(&value) else {
296 continue;
297 };
298 let Some(alias_name) = alias_text.strip_prefix('*') else {
299 continue;
300 };
301
302 merge_from_alias(&mut merged_pairs, alias_name, registry);
303 }
304 YamlNode::Sequence(seq) => {
306 for alias_node in seq.values() {
308 let Some(alias_text) = node_as_string(&alias_node) else {
309 continue;
310 };
311 let Some(alias_name) = alias_text.strip_prefix('*') else {
312 continue;
313 };
314
315 merge_from_alias(&mut merged_pairs, alias_name, registry);
316 }
317 }
318 _ => continue,
319 }
320 }
321
322 for (key, value) in mapping.iter() {
324 let Some(key_str) = node_as_string(&key) else {
325 continue;
326 };
327 if key_str == "<<" {
328 continue;
329 }
330 if let Some(yaml_value) = YamlValue::cast(value.syntax().clone()) {
332 merged_pairs.insert(key_str, yaml_value);
333 }
334 }
335
336 merged_pairs
337}
338
339fn merge_from_alias(
341 merged_pairs: &mut std::collections::BTreeMap<String, YamlValue>,
342 alias_name: &str,
343 registry: &AnchorRegistry,
344) {
345 use rowan::ast::AstNode;
346
347 let Some(resolved_node) = registry.resolve(alias_name) else {
348 return;
349 };
350
351 if let Some(resolved_mapping) = crate::yaml::Mapping::cast(resolved_node.clone()) {
353 for (src_key, src_value) in resolved_mapping.iter() {
354 let Some(k_str) = node_as_string(&src_key) else {
355 continue;
356 };
357 if let Some(yaml_value) = YamlValue::cast(src_value.syntax().clone()) {
359 merged_pairs.insert(k_str, yaml_value);
360 }
361 }
362 }
363}
364
365#[derive(Clone)]
415pub struct MergedMapping<'a> {
416 base: crate::yaml::Mapping,
417 registry: &'a AnchorRegistry,
418}
419
420impl<'a> MergedMapping<'a> {
421 pub fn new(base: crate::yaml::Mapping, registry: &'a AnchorRegistry) -> Self {
423 Self { base, registry }
424 }
425
426 pub fn base(&self) -> &crate::yaml::Mapping {
431 &self.base
432 }
433
434 pub fn registry(&self) -> &AnchorRegistry {
436 self.registry
437 }
438
439 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
449 if is_merge_key(&key) {
450 return None;
451 }
452
453 if let Some(node) = self.base.get(&key) {
454 return Some(resolve_alias_node(node, self.registry));
455 }
456
457 for source in merge_sources(&self.base, self.registry) {
458 if let Some(node) = source.get(&key) {
459 return Some(resolve_alias_node(node, self.registry));
460 }
461 }
462 None
463 }
464
465 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
468 self.get(key).is_some()
469 }
470
471 pub fn iter(
478 &self,
479 ) -> impl Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + '_ {
480 let mut seen: Vec<String> = Vec::new();
481 let mut out: Vec<(crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> = Vec::new();
482
483 for (key, value) in self.base.iter() {
484 let Some(key_str) = node_as_string(&key) else {
485 continue;
486 };
487 if key_str == "<<" {
488 continue;
489 }
490 seen.push(key_str);
491 out.push((key, resolve_alias_node(value, self.registry)));
492 }
493
494 for source in merge_sources(&self.base, self.registry) {
495 for (key, value) in source.iter() {
496 let Some(key_str) = node_as_string(&key) else {
497 continue;
498 };
499 if key_str == "<<" || seen.contains(&key_str) {
500 continue;
501 }
502 seen.push(key_str);
503 out.push((key, resolve_alias_node(value, self.registry)));
504 }
505 }
506
507 out.into_iter()
508 }
509
510 pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
512 self.iter().map(|(k, _)| k)
513 }
514
515 pub fn values(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
517 self.iter().map(|(_, v)| v)
518 }
519
520 pub fn len(&self) -> usize {
522 self.iter().count()
523 }
524
525 pub fn is_empty(&self) -> bool {
527 self.iter().next().is_none()
528 }
529
530 pub fn get_merged(&self, key: impl crate::AsYaml) -> Option<MergedMapping<'a>> {
536 let node = self.get(key)?;
537 let mapping = node.as_mapping().cloned()?;
538 Some(MergedMapping::new(mapping, self.registry))
539 }
540}
541
542impl std::fmt::Debug for MergedMapping<'_> {
543 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
544 f.debug_struct("MergedMapping")
545 .field("len", &self.len())
546 .finish()
547 }
548}
549
550fn is_merge_key(key: &impl crate::AsYaml) -> bool {
552 use crate::as_yaml::YamlNode;
553
554 if let Some(node) = key.as_node() {
555 if let Some(yaml) = YamlNode::from_syntax_peeled(node.clone()) {
556 return node_as_string(&yaml).as_deref() == Some("<<");
557 }
558 }
559
560 let mut builder = rowan::GreenNodeBuilder::new();
563 builder.start_node(crate::lex::SyntaxKind::ROOT.into());
564 key.build_content(&mut builder, 0, true);
565 builder.finish_node();
566 let root = crate::yaml::SyntaxNode::new_root(builder.finish());
567 root.text().to_string().trim() == "<<"
568}
569
570fn merge_sources(
573 base: &crate::yaml::Mapping,
574 registry: &AnchorRegistry,
575) -> Vec<crate::yaml::Mapping> {
576 use crate::as_yaml::YamlNode;
577 use rowan::ast::AstNode;
578
579 let mut sources = Vec::new();
580
581 for (key, value) in base.iter() {
582 if node_as_string(&key).as_deref() != Some("<<") {
583 continue;
584 }
585
586 match &value {
587 YamlNode::Scalar(_) => {
588 if let Some(alias_text) = node_as_string(&value) {
589 if let Some(alias_name) = alias_text.strip_prefix('*') {
590 if let Some(target) = registry.resolve(alias_name) {
591 if let Some(m) = crate::yaml::Mapping::cast(target.clone()) {
592 sources.push(m);
593 }
594 }
595 }
596 }
597 }
598 YamlNode::Sequence(seq) => {
599 for item in seq.values() {
600 let alias_name = match &item {
601 YamlNode::Alias(a) => a.name(),
602 YamlNode::Scalar(_) => {
603 let Some(text) = node_as_string(&item) else {
604 continue;
605 };
606 let Some(name) = text.strip_prefix('*') else {
607 continue;
608 };
609 name.to_string()
610 }
611 _ => continue,
612 };
613 let Some(target) = registry.resolve(&alias_name) else {
614 continue;
615 };
616 if let Some(m) = crate::yaml::Mapping::cast(target.clone()) {
617 sources.push(m);
618 }
619 }
620 }
621 YamlNode::Alias(alias) => {
622 if let Some(target) = registry.resolve(&alias.name()) {
623 if let Some(m) = crate::yaml::Mapping::cast(target.clone()) {
624 sources.push(m);
625 }
626 }
627 }
628 _ => continue,
629 }
630 }
631
632 sources
633}
634
635fn resolve_alias_node(
637 node: crate::as_yaml::YamlNode,
638 registry: &AnchorRegistry,
639) -> crate::as_yaml::YamlNode {
640 use crate::as_yaml::YamlNode;
641
642 if let YamlNode::Alias(alias) = &node {
643 if let Some(target) = registry.resolve(&alias.name()) {
644 if let Some(resolved) = YamlNode::from_syntax_peeled(target.clone()) {
645 return resolved;
646 }
647 }
648 }
649 node
650}
651
652pub trait MappingMergedExt {
654 fn merged<'a>(&self, registry: &'a AnchorRegistry) -> MergedMapping<'a>;
659}
660
661impl MappingMergedExt for crate::yaml::Mapping {
662 fn merged<'a>(&self, registry: &'a AnchorRegistry) -> MergedMapping<'a> {
663 MergedMapping::new(self.clone(), registry)
664 }
665}
666
667pub trait DocumentMergedExt {
669 fn merged(&self) -> Option<MergedView>;
679}
680
681impl DocumentMergedExt for crate::yaml::Document {
682 fn merged(&self) -> Option<MergedView> {
683 let mapping = self.as_mapping()?;
684 let registry = self.build_anchor_registry();
685 Some(MergedView { mapping, registry })
686 }
687}
688
689pub struct MergedView {
697 mapping: crate::yaml::Mapping,
698 registry: AnchorRegistry,
699}
700
701impl MergedView {
702 pub fn as_mapping(&self) -> MergedMapping<'_> {
704 MergedMapping::new(self.mapping.clone(), &self.registry)
705 }
706
707 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
709 self.as_mapping().get(key)
710 }
711
712 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
714 self.as_mapping().contains_key(key)
715 }
716
717 pub fn registry(&self) -> &AnchorRegistry {
719 &self.registry
720 }
721}
722
723impl std::fmt::Debug for MergedView {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 f.debug_struct("MergedView").finish()
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732 use crate::Document;
733 use std::str::FromStr;
734
735 fn doc(text: &str) -> Document {
736 Document::from_str(text).expect("parse")
737 }
738
739 #[test]
740 fn merged_basic_merge_key() {
741 let yaml = "\
742defaults: &d
743 timeout: 30
744 retries: 3
745
746prod:
747 <<: *d
748 host: prod.example.com
749";
750 let d = doc(yaml);
751 let root = d.as_mapping().unwrap();
752 let reg = d.build_anchor_registry();
753 let prod = root.get_mapping("prod").unwrap();
754 let m = prod.merged(®);
755
756 assert_eq!(m.get("timeout").unwrap().to_i64(), Some(30));
757 assert_eq!(m.get("retries").unwrap().to_i64(), Some(3));
758 assert_eq!(
759 m.get("host").unwrap().as_scalar().unwrap().as_string(),
760 "prod.example.com"
761 );
762 }
763
764 #[test]
765 fn merged_direct_key_wins() {
766 let yaml = "\
767defaults: &d
768 timeout: 30
769
770prod:
771 <<: *d
772 timeout: 60
773";
774 let d = doc(yaml);
775 let reg = d.build_anchor_registry();
776 let prod = d.as_mapping().unwrap().get_mapping("prod").unwrap();
777 let m = prod.merged(®);
778
779 assert_eq!(m.get("timeout").unwrap().to_i64(), Some(60));
780 }
781
782 #[test]
783 fn merged_merge_key_itself_is_hidden() {
784 let yaml = "\
785d: &d
786 a: 1
787m:
788 <<: *d
789";
790 let d = doc(yaml);
791 let reg = d.build_anchor_registry();
792 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
793 let m = inner.merged(®);
794
795 assert!(m.get("<<").is_none());
796 let keys: Vec<String> = m
797 .keys()
798 .map(|k| k.as_scalar().unwrap().as_string())
799 .collect();
800 assert_eq!(keys, vec!["a"]);
801 }
802
803 #[test]
804 fn merged_sequence_of_aliases() {
805 let yaml = "\
807a: &a
808 x: 1
809 y: 1
810b: &b
811 y: 2
812 z: 2
813m:
814 <<: [*a, *b]
815";
816 let d = doc(yaml);
817 let reg = d.build_anchor_registry();
818 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
819 let m = inner.merged(®);
820
821 assert_eq!(m.get("x").unwrap().to_i64(), Some(1));
823 assert_eq!(m.get("y").unwrap().to_i64(), Some(1));
825 assert_eq!(m.get("z").unwrap().to_i64(), Some(2));
827 }
828
829 #[test]
830 fn merged_missing_alias_returns_none() {
831 let yaml = "\
832m:
833 <<: *nope
834 a: 1
835";
836 let d = doc(yaml);
837 let reg = d.build_anchor_registry();
838 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
839 let m = inner.merged(®);
840
841 assert_eq!(m.get("a").unwrap().to_i64(), Some(1));
843 assert!(m.get("b").is_none());
845 }
846
847 #[test]
848 fn merged_iter_order_direct_then_merged() {
849 let yaml = "\
850d: &d
851 shared: from_d
852 only_in_d: 1
853m:
854 direct: hi
855 <<: *d
856";
857 let d = doc(yaml);
858 let reg = d.build_anchor_registry();
859 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
860 let m = inner.merged(®);
861
862 let keys: Vec<String> = m
863 .iter()
864 .map(|(k, _)| k.as_scalar().unwrap().as_string())
865 .collect();
866 assert_eq!(keys, vec!["direct", "shared", "only_in_d"]);
867 }
868
869 #[test]
870 fn merged_len_and_empty() {
871 let yaml = "\
872d: &d
873 a: 1
874 b: 2
875m:
876 <<: *d
877 c: 3
878";
879 let d = doc(yaml);
880 let reg = d.build_anchor_registry();
881 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
882 let m = inner.merged(®);
883
884 assert_eq!(m.len(), 3);
885 assert!(!m.is_empty());
886 }
887
888 #[test]
889 fn merged_alias_value_is_resolved() {
890 let yaml = "\
891target: &t
892 k: 42
893m:
894 ref: *t
895";
896 let d = doc(yaml);
897 let reg = d.build_anchor_registry();
898 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
899 let m = inner.merged(®);
900
901 let r = m.get("ref").unwrap();
902 let nested = r.as_mapping().unwrap();
903 assert_eq!(nested.get("k").unwrap().to_i64(), Some(42));
904 }
905
906 #[test]
907 fn merged_get_merged_nested() {
908 let yaml = "\
909defaults: &d
910 port: 80
911prod:
912 <<: *d
913 inner:
914 a: 1
915";
916 let d = doc(yaml);
917 let reg = d.build_anchor_registry();
918 let prod = d.as_mapping().unwrap().get_mapping("prod").unwrap();
919 let m = prod.merged(®);
920
921 let inner = m.get_merged("inner").unwrap();
922 assert_eq!(inner.get("a").unwrap().to_i64(), Some(1));
923 }
924
925 #[test]
926 fn document_merged_view() {
927 let yaml = "\
928defaults: &d
929 timeout: 30
930shared:
931 <<: *d
932 retries: 5
933";
934 let d = doc(yaml);
935 let view = d.merged().unwrap();
936
937 assert!(view.contains_key("defaults"));
939 assert!(view.contains_key("shared"));
940
941 let m = view.as_mapping();
943 let shared = m.get_merged("shared").unwrap();
944 assert_eq!(shared.get("timeout").unwrap().to_i64(), Some(30));
945 assert_eq!(shared.get("retries").unwrap().to_i64(), Some(5));
946 }
947
948 #[test]
949 fn merged_does_not_mutate_underlying_cst() {
950 let yaml = "\
951d: &d
952 a: 1
953m:
954 <<: *d
955 b: 2
956";
957 let d = doc(yaml);
958 let reg = d.build_anchor_registry();
959 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
960 let _ = inner.merged(®).get("a");
961 assert_eq!(d.to_string(), yaml);
963 }
964}