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>;
184
185 fn build_anchor_registry(&self) -> AnchorRegistry;
187}
188
189impl DocumentResolvedExt for crate::yaml::Document {
190 fn get_resolved(&self, key: impl crate::AsYaml) -> Option<YamlValue> {
191 use rowan::ast::AstNode;
192
193 let registry = self.build_anchor_registry();
195
196 let mapping = self.as_mapping()?;
198 let value = mapping
199 .get_node(&key)
200 .and_then(crate::value::YamlValue::cast)?;
201
202 if let Some(node) = mapping.get_node(&key) {
205 if let Some(alias_name) = find_alias_reference(&node) {
206 if let Some(resolved_node) = registry.resolve(&alias_name) {
208 return YamlValue::cast(resolved_node.clone());
210 }
211 }
212 }
213
214 if let Some(node) = mapping.get_node(&key) {
216 if let Some(result_mapping) = crate::yaml::Mapping::cast(node) {
217 if has_merge_keys(&result_mapping) {
218 return Some(YamlValue::Mapping(apply_merge_keys(
219 &result_mapping,
220 ®istry,
221 )));
222 }
223 }
224 }
225
226 Some(value)
227 }
228
229 fn build_anchor_registry(&self) -> AnchorRegistry {
230 AnchorRegistry::from_document(self)
231 }
232}
233
234fn find_alias_reference(node: &SyntaxNode) -> Option<String> {
236 for child in node.children_with_tokens() {
238 if let Some(token) = child.as_token() {
239 if token.kind() == SyntaxKind::REFERENCE {
240 let text = token.text();
241 return text.strip_prefix('*').map(|s| s.to_string());
243 }
244 }
245 }
246
247 if let Some(parent) = node.parent() {
249 for child in parent.children_with_tokens() {
250 if let Some(token) = child.as_token() {
251 if token.kind() == SyntaxKind::REFERENCE {
252 let text = token.text();
253 return text.strip_prefix('*').map(|s| s.to_string());
254 }
255 }
256 }
257 }
258
259 None
260}
261
262fn node_as_string(node: &crate::as_yaml::YamlNode) -> Option<String> {
264 node.as_scalar().map(|s| s.as_string())
265}
266
267fn has_merge_keys(mapping: &crate::yaml::Mapping) -> bool {
269 for (key, _) in mapping.iter() {
270 if node_as_string(&key).as_deref() == Some("<<") {
271 return true;
272 }
273 }
274 false
275}
276
277fn apply_merge_keys(
279 mapping: &crate::yaml::Mapping,
280 registry: &AnchorRegistry,
281) -> std::collections::BTreeMap<String, YamlValue> {
282 use crate::as_yaml::YamlNode;
283 use std::collections::BTreeMap;
284
285 let mut merged_pairs: BTreeMap<String, YamlValue> = BTreeMap::new();
286
287 for (key, value) in mapping.iter() {
289 let Some(key_str) = node_as_string(&key) else {
290 continue;
291 };
292 if key_str != "<<" {
293 continue;
294 }
295
296 match &value {
298 YamlNode::Alias(_) | YamlNode::Scalar(_) => {
300 let Some(alias_name) = alias_target(&value) else {
301 continue;
302 };
303 merge_from_alias(&mut merged_pairs, &alias_name, registry);
304 }
305 YamlNode::Sequence(seq) => {
307 for alias_node in seq.values() {
309 let Some(alias_name) = alias_target(&alias_node) else {
310 continue;
311 };
312 merge_from_alias(&mut merged_pairs, &alias_name, registry);
313 }
314 }
315 _ => continue,
316 }
317 }
318
319 for (key, value) in mapping.iter() {
321 let Some(key_str) = node_as_string(&key) else {
322 continue;
323 };
324 if key_str == "<<" {
325 continue;
326 }
327 if let Some(yaml_value) = resolved_value(value, registry) {
328 merged_pairs.insert(key_str, yaml_value);
329 }
330 }
331
332 merged_pairs
333}
334
335fn resolved_value(value: crate::as_yaml::YamlNode, registry: &AnchorRegistry) -> Option<YamlValue> {
341 let resolved = resolve_alias_node(value, registry);
342 if let crate::as_yaml::YamlNode::Alias(alias) = &resolved {
343 return Some(YamlValue::Scalar(crate::ScalarValue::string(alias.value())));
344 }
345 YamlValue::cast(resolved.syntax().clone())
346}
347
348fn alias_target(node: &crate::as_yaml::YamlNode) -> Option<String> {
353 match node {
354 crate::as_yaml::YamlNode::Alias(alias) => Some(alias.name()),
355 _ => node_as_string(node)?.strip_prefix('*').map(str::to_string),
356 }
357}
358
359fn merge_from_alias(
361 merged_pairs: &mut std::collections::BTreeMap<String, YamlValue>,
362 alias_name: &str,
363 registry: &AnchorRegistry,
364) {
365 use rowan::ast::AstNode;
366
367 let Some(resolved_node) = registry.resolve(alias_name) else {
368 return;
369 };
370
371 if let Some(resolved_mapping) = crate::yaml::Mapping::cast(resolved_node.clone()) {
373 for (src_key, src_value) in resolved_mapping.iter() {
374 let Some(k_str) = node_as_string(&src_key) else {
375 continue;
376 };
377 if let Some(yaml_value) = resolved_value(src_value, registry) {
378 merged_pairs.insert(k_str, yaml_value);
379 }
380 }
381 }
382}
383
384#[derive(Clone)]
437pub struct MergedMapping<'a> {
438 base: crate::yaml::Mapping,
439 registry: &'a AnchorRegistry,
440}
441
442impl<'a> MergedMapping<'a> {
443 pub fn new(base: crate::yaml::Mapping, registry: &'a AnchorRegistry) -> Self {
445 Self { base, registry }
446 }
447
448 pub fn base(&self) -> &crate::yaml::Mapping {
453 &self.base
454 }
455
456 pub fn registry(&self) -> &AnchorRegistry {
458 self.registry
459 }
460
461 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
471 if is_merge_key(&key) {
472 return None;
473 }
474
475 if let Some(node) = self.base.get(&key) {
476 return Some(resolve_alias_node(node, self.registry));
477 }
478
479 for source in merge_sources(&self.base, self.registry) {
480 if let Some(node) = source.get(&key) {
481 return Some(resolve_alias_node(node, self.registry));
482 }
483 }
484 None
485 }
486
487 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
490 self.get(key).is_some()
491 }
492
493 pub fn iter(
500 &self,
501 ) -> impl Iterator<Item = (crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> + '_ {
502 let mut seen: Vec<String> = Vec::new();
503 let mut out: Vec<(crate::as_yaml::YamlNode, crate::as_yaml::YamlNode)> = Vec::new();
504
505 for (key, value) in self.base.iter() {
506 let Some(key_str) = node_as_string(&key) else {
507 continue;
508 };
509 if key_str == "<<" {
510 continue;
511 }
512 seen.push(key_str);
513 out.push((key, resolve_alias_node(value, self.registry)));
514 }
515
516 for source in merge_sources(&self.base, self.registry) {
517 for (key, value) in source.iter() {
518 let Some(key_str) = node_as_string(&key) else {
519 continue;
520 };
521 if key_str == "<<" || seen.contains(&key_str) {
522 continue;
523 }
524 seen.push(key_str);
525 out.push((key, resolve_alias_node(value, self.registry)));
526 }
527 }
528
529 out.into_iter()
530 }
531
532 pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
534 self.iter().map(|(k, _)| k)
535 }
536
537 pub fn values(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
539 self.iter().map(|(_, v)| v)
540 }
541
542 pub fn len(&self) -> usize {
544 self.iter().count()
545 }
546
547 pub fn is_empty(&self) -> bool {
549 self.iter().next().is_none()
550 }
551
552 pub fn get_merged(&self, key: impl crate::AsYaml) -> Option<MergedMapping<'a>> {
558 let node = self.get(key)?;
559 let mapping = node.as_mapping().cloned()?;
560 Some(MergedMapping::new(mapping, self.registry))
561 }
562}
563
564impl std::fmt::Debug for MergedMapping<'_> {
565 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566 f.debug_struct("MergedMapping")
567 .field("len", &self.len())
568 .finish()
569 }
570}
571
572fn is_merge_key(key: &impl crate::AsYaml) -> bool {
574 crate::as_yaml::yaml_eq(key, &"<<")
575}
576
577fn merge_sources(
580 base: &crate::yaml::Mapping,
581 registry: &AnchorRegistry,
582) -> Vec<crate::yaml::Mapping> {
583 use crate::as_yaml::YamlNode;
584 use rowan::ast::AstNode;
585
586 let mut sources = Vec::new();
587
588 for (key, value) in base.iter() {
589 if node_as_string(&key).as_deref() != Some("<<") {
590 continue;
591 }
592
593 match &value {
594 YamlNode::Scalar(_) => {
595 if let Some(alias_text) = node_as_string(&value) {
596 if let Some(alias_name) = alias_text.strip_prefix('*') {
597 if let Some(target) = registry.resolve(alias_name) {
598 if let Some(m) = crate::yaml::Mapping::cast(target.clone()) {
599 sources.push(m);
600 }
601 }
602 }
603 }
604 }
605 YamlNode::Sequence(seq) => {
606 for item in seq.values() {
607 let alias_name = match &item {
608 YamlNode::Alias(a) => a.name(),
609 YamlNode::Scalar(_) => {
610 let Some(text) = node_as_string(&item) else {
611 continue;
612 };
613 let Some(name) = text.strip_prefix('*') else {
614 continue;
615 };
616 name.to_string()
617 }
618 _ => continue,
619 };
620 let Some(target) = registry.resolve(&alias_name) else {
621 continue;
622 };
623 if let Some(m) = crate::yaml::Mapping::cast(target.clone()) {
624 sources.push(m);
625 }
626 }
627 }
628 YamlNode::Alias(alias) => {
629 if let Some(target) = registry.resolve(&alias.name()) {
630 if let Some(m) = crate::yaml::Mapping::cast(target.clone()) {
631 sources.push(m);
632 }
633 }
634 }
635 _ => continue,
636 }
637 }
638
639 sources
640}
641
642fn resolve_alias_node(
644 node: crate::as_yaml::YamlNode,
645 registry: &AnchorRegistry,
646) -> crate::as_yaml::YamlNode {
647 use crate::as_yaml::YamlNode;
648
649 if let YamlNode::Alias(alias) = &node {
650 if let Some(target) = registry.resolve(&alias.name()) {
651 if let Some(resolved) = YamlNode::from_syntax_peeled(target.clone()) {
652 return resolved;
653 }
654 }
655 }
656 node
657}
658
659pub trait MappingMergedExt {
661 fn merged<'a>(&self, registry: &'a AnchorRegistry) -> MergedMapping<'a>;
666}
667
668impl MappingMergedExt for crate::yaml::Mapping {
669 fn merged<'a>(&self, registry: &'a AnchorRegistry) -> MergedMapping<'a> {
670 MergedMapping::new(self.clone(), registry)
671 }
672}
673
674pub trait DocumentMergedExt {
676 fn merged(&self) -> Option<MergedView>;
686}
687
688impl DocumentMergedExt for crate::yaml::Document {
689 fn merged(&self) -> Option<MergedView> {
690 let mapping = self.as_mapping()?;
691 let registry = self.build_anchor_registry();
692 Some(MergedView { mapping, registry })
693 }
694}
695
696pub struct MergedView {
704 mapping: crate::yaml::Mapping,
705 registry: AnchorRegistry,
706}
707
708impl MergedView {
709 pub fn as_mapping(&self) -> MergedMapping<'_> {
711 MergedMapping::new(self.mapping.clone(), &self.registry)
712 }
713
714 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
716 self.as_mapping().get(key)
717 }
718
719 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
721 self.as_mapping().contains_key(key)
722 }
723
724 pub fn registry(&self) -> &AnchorRegistry {
726 &self.registry
727 }
728}
729
730impl std::fmt::Debug for MergedView {
731 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
732 f.debug_struct("MergedView").finish()
733 }
734}
735
736#[cfg(test)]
737mod tests {
738 use super::*;
739 use crate::Document;
740 use std::str::FromStr;
741
742 fn doc(text: &str) -> Document {
743 Document::from_str(text).expect("parse")
744 }
745
746 #[test]
747 fn get_resolved_keys_are_decoded() {
748 for (yaml, expected) in [
753 ("a:\n '0': z\n x: 1\n", vec!["0", "x"]),
754 ("a:\n \"k\": z\n", vec!["k"]),
755 ("a:\n plain: z\n", vec!["plain"]),
756 ("s: &s\n q: r\na:\n <<: *s\n '0': z\n", vec!["0", "q"]),
758 ] {
759 let doc = Document::from_str(yaml).unwrap();
760 let resolved = doc.get_resolved("a").expect("a resolves");
761 let mapping = resolved.as_mapping().expect("a is a mapping");
762 let keys: Vec<&str> = mapping.keys().map(String::as_str).collect();
763 assert_eq!(keys, expected, "{yaml:?}");
764 for key in expected {
765 assert!(
766 mapping.contains_key(key),
767 "{yaml:?}: {key:?} not reachable by name"
768 );
769 }
770 }
771 }
772
773 #[test]
774 fn get_resolved_expands_merge_keys() {
775 for (yaml, present, absent) in [
780 (
781 "a: &a\n x: 1\nb: &b\n y: 2\nm:\n <<: [*a, *b]\n z: 3\n",
782 vec!["x", "y", "z"],
783 vec!["w"],
784 ),
785 (
786 "a: &a\n x: 1\nm:\n <<: *a\n z: 3\n",
787 vec!["x", "z"],
788 vec!["y"],
789 ),
790 ] {
791 let doc = Document::from_str(yaml).unwrap();
792 let registry = doc.build_anchor_registry();
793 let root = doc.as_mapping().unwrap();
794 let m_node = root.get("m").unwrap();
795 let m = m_node.as_mapping().unwrap();
796 let merged = m.merged(®istry);
797 let resolved = doc.get_resolved("m").expect("m resolves");
798
799 for key in present {
800 assert!(merged.get(key).is_some(), "MergedMapping lost {key:?}");
801 assert!(
802 resolved.as_mapping().and_then(|mm| mm.get(key)).is_some(),
803 "get_resolved lost {key:?} in {yaml:?}"
804 );
805 }
806 for key in absent {
807 assert!(merged.get(key).is_none(), "MergedMapping invented {key:?}");
808 assert!(
809 resolved.as_mapping().and_then(|mm| mm.get(key)).is_none(),
810 "get_resolved invented {key:?} in {yaml:?}"
811 );
812 }
813 }
814 }
815
816 #[test]
817 fn merged_basic_merge_key() {
818 let yaml = "\
819defaults: &d
820 timeout: 30
821 retries: 3
822
823prod:
824 <<: *d
825 host: prod.example.com
826";
827 let d = doc(yaml);
828 let root = d.as_mapping().unwrap();
829 let reg = d.build_anchor_registry();
830 let prod = root.get_mapping("prod").unwrap();
831 let m = prod.merged(®);
832
833 assert_eq!(m.get("timeout").unwrap().to_i64(), Some(30));
834 assert_eq!(m.get("retries").unwrap().to_i64(), Some(3));
835 assert_eq!(
836 m.get("host").unwrap().as_scalar().unwrap().as_string(),
837 "prod.example.com"
838 );
839 }
840
841 #[test]
842 fn merged_direct_key_wins() {
843 let yaml = "\
844defaults: &d
845 timeout: 30
846
847prod:
848 <<: *d
849 timeout: 60
850";
851 let d = doc(yaml);
852 let reg = d.build_anchor_registry();
853 let prod = d.as_mapping().unwrap().get_mapping("prod").unwrap();
854 let m = prod.merged(®);
855
856 assert_eq!(m.get("timeout").unwrap().to_i64(), Some(60));
857 }
858
859 #[test]
860 fn merged_merge_key_itself_is_hidden() {
861 let yaml = "\
862d: &d
863 a: 1
864m:
865 <<: *d
866";
867 let d = doc(yaml);
868 let reg = d.build_anchor_registry();
869 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
870 let m = inner.merged(®);
871
872 assert!(m.get("<<").is_none());
873 let keys: Vec<String> = m
874 .keys()
875 .map(|k| k.as_scalar().unwrap().as_string())
876 .collect();
877 assert_eq!(keys, vec!["a"]);
878 }
879
880 #[test]
881 fn merged_sequence_of_aliases() {
882 let yaml = "\
884a: &a
885 x: 1
886 y: 1
887b: &b
888 y: 2
889 z: 2
890m:
891 <<: [*a, *b]
892";
893 let d = doc(yaml);
894 let reg = d.build_anchor_registry();
895 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
896 let m = inner.merged(®);
897
898 assert_eq!(m.get("x").unwrap().to_i64(), Some(1));
900 assert_eq!(m.get("y").unwrap().to_i64(), Some(1));
902 assert_eq!(m.get("z").unwrap().to_i64(), Some(2));
904 }
905
906 #[test]
907 fn merged_missing_alias_returns_none() {
908 let yaml = "\
909m:
910 <<: *nope
911 a: 1
912";
913 let d = doc(yaml);
914 let reg = d.build_anchor_registry();
915 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
916 let m = inner.merged(®);
917
918 assert_eq!(m.get("a").unwrap().to_i64(), Some(1));
920 assert!(m.get("b").is_none());
922 }
923
924 #[test]
925 fn merged_iter_order_direct_then_merged() {
926 let yaml = "\
927d: &d
928 shared: from_d
929 only_in_d: 1
930m:
931 direct: hi
932 <<: *d
933";
934 let d = doc(yaml);
935 let reg = d.build_anchor_registry();
936 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
937 let m = inner.merged(®);
938
939 let keys: Vec<String> = m
940 .iter()
941 .map(|(k, _)| k.as_scalar().unwrap().as_string())
942 .collect();
943 assert_eq!(keys, vec!["direct", "shared", "only_in_d"]);
944 }
945
946 #[test]
947 fn merged_len_and_empty() {
948 let yaml = "\
949d: &d
950 a: 1
951 b: 2
952m:
953 <<: *d
954 c: 3
955";
956 let d = doc(yaml);
957 let reg = d.build_anchor_registry();
958 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
959 let m = inner.merged(®);
960
961 assert_eq!(m.len(), 3);
962 assert!(!m.is_empty());
963 }
964
965 #[test]
966 fn merged_alias_value_is_resolved() {
967 let yaml = "\
968target: &t
969 k: 42
970m:
971 ref: *t
972";
973 let d = doc(yaml);
974 let reg = d.build_anchor_registry();
975 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
976 let m = inner.merged(®);
977
978 let r = m.get("ref").unwrap();
979 let nested = r.as_mapping().unwrap();
980 assert_eq!(nested.get("k").unwrap().to_i64(), Some(42));
981 }
982
983 #[test]
984 fn merged_get_merged_nested() {
985 let yaml = "\
986defaults: &d
987 port: 80
988prod:
989 <<: *d
990 inner:
991 a: 1
992";
993 let d = doc(yaml);
994 let reg = d.build_anchor_registry();
995 let prod = d.as_mapping().unwrap().get_mapping("prod").unwrap();
996 let m = prod.merged(®);
997
998 let inner = m.get_merged("inner").unwrap();
999 assert_eq!(inner.get("a").unwrap().to_i64(), Some(1));
1000 }
1001
1002 #[test]
1003 fn document_merged_view() {
1004 let yaml = "\
1005defaults: &d
1006 timeout: 30
1007shared:
1008 <<: *d
1009 retries: 5
1010";
1011 let d = doc(yaml);
1012 let view = d.merged().unwrap();
1013
1014 assert!(view.contains_key("defaults"));
1016 assert!(view.contains_key("shared"));
1017
1018 let m = view.as_mapping();
1020 let shared = m.get_merged("shared").unwrap();
1021 assert_eq!(shared.get("timeout").unwrap().to_i64(), Some(30));
1022 assert_eq!(shared.get("retries").unwrap().to_i64(), Some(5));
1023 }
1024
1025 #[test]
1026 fn merged_does_not_mutate_underlying_cst() {
1027 let yaml = "\
1028d: &d
1029 a: 1
1030m:
1031 <<: *d
1032 b: 2
1033";
1034 let d = doc(yaml);
1035 let reg = d.build_anchor_registry();
1036 let inner = d.as_mapping().unwrap().get_mapping("m").unwrap();
1037 let _ = inner.merged(®).get("a");
1038 assert_eq!(d.to_string(), yaml);
1040 }
1041}