1use crate::yaml::Mapping;
62
63pub trait YamlPath {
81 fn try_get_path(&self, path: &str) -> Result<crate::as_yaml::YamlNode, PathError> {
97 self.try_get_path_with(path, AliasPolicy::Follow)
98 }
99
100 fn try_get_path_with(
110 &self,
111 path: &str,
112 policy: AliasPolicy,
113 ) -> Result<crate::as_yaml::YamlNode, PathError>;
114
115 fn try_set_path(&self, path: &str, value: impl crate::AsYaml) -> Result<(), PathError> {
128 self.try_set_path_with(path, value, AliasPolicy::Refuse)
129 }
130
131 fn try_set_path_with(
145 &self,
146 path: &str,
147 value: impl crate::AsYaml,
148 policy: AliasPolicy,
149 ) -> Result<(), PathError>;
150
151 fn try_remove_path(&self, path: &str) -> Result<crate::as_yaml::YamlNode, PathError> {
164 self.try_remove_path_with(path, AliasPolicy::Refuse)
165 }
166
167 fn try_remove_path_with(
180 &self,
181 path: &str,
182 policy: AliasPolicy,
183 ) -> Result<crate::as_yaml::YamlNode, PathError>;
184
185 #[deprecated(
205 since = "0.4.0",
206 note = "use try_get_path; get_path swallows PathError as None"
207 )]
208 fn get_path(&self, path: &str) -> Option<crate::as_yaml::YamlNode> {
209 self.try_get_path(path).ok()
210 }
211
212 #[deprecated(
231 since = "0.4.0",
232 note = "use try_set_path; set_path silently ignores PathError"
233 )]
234 fn set_path(&self, path: &str, value: impl crate::AsYaml) {
235 let _ = self.try_set_path(path, value);
236 }
237
238 #[deprecated(
259 since = "0.4.0",
260 note = "use try_remove_path; remove_path swallows PathError as false"
261 )]
262 fn remove_path(&self, path: &str) -> bool {
263 self.try_remove_path(path).is_ok()
264 }
265}
266
267#[derive(Debug, Clone, PartialEq)]
269pub enum PathSegment {
270 Key(String),
272 Index(usize),
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
278pub enum PathParseError {
279 UnclosedIndex,
281 InvalidIndex(String),
283}
284
285impl std::fmt::Display for PathParseError {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 match self {
288 PathParseError::UnclosedIndex => {
289 write!(f, "unclosed '[' in path")
290 }
291 PathParseError::InvalidIndex(text) => {
292 write!(f, "invalid path index [{text}]")
293 }
294 }
295 }
296}
297
298impl std::error::Error for PathParseError {}
299
300pub const MAX_INDEX_GROWTH: usize = 256;
308
309#[derive(Debug, Clone, PartialEq, Eq)]
317pub enum PathError {
318 Parse(PathParseError),
320 EmptyPath,
323 NoRoot,
327 TypeMismatch {
332 at: String,
334 },
335 NotFound {
341 at: String,
343 },
344 AliasRefused {
348 at: String,
350 alias: String,
352 },
353 IndexTooFar {
360 at: String,
362 len: usize,
364 index: usize,
366 },
367 UndefinedAlias {
370 at: String,
372 alias: String,
374 },
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
387pub enum AliasPolicy {
388 #[default]
392 Refuse,
393 Follow,
399 Expand,
407}
408
409impl std::fmt::Display for PathError {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 match self {
412 PathError::Parse(e) => write!(f, "path parse error: {e}"),
413 PathError::EmptyPath => write!(f, "path is empty (no segments)"),
414 PathError::NoRoot => write!(f, "document has no root to descend into"),
415 PathError::TypeMismatch { at } => {
416 write!(f, "path segment {at:?} is not a container")
417 }
418 PathError::NotFound { at } => write!(f, "path segment {at:?} not found"),
419 PathError::AliasRefused { at, alias } => write!(
420 f,
421 "path segment {at:?} is an alias to {alias:?}; refusing to descend through it"
422 ),
423 PathError::IndexTooFar { at, len, index } => write!(
424 f,
425 "path segment {at:?} would grow a sequence of {len} by {} null entries to reach index {index}",
426 index + 1 - len
427 ),
428 PathError::UndefinedAlias { at, alias } => write!(
429 f,
430 "path segment {at:?} is an alias to undefined anchor {alias:?}"
431 ),
432 }
433 }
434}
435
436impl std::error::Error for PathError {}
437
438impl From<PathParseError> for PathError {
439 fn from(e: PathParseError) -> Self {
440 PathError::Parse(e)
441 }
442}
443
444pub fn try_parse_path(path: &str) -> Result<Vec<PathSegment>, PathParseError> {
486 if path.is_empty() {
487 return Ok(vec![]);
488 }
489
490 let mut segments = Vec::new();
491 let mut current = String::new();
492 let mut chars = path.chars().peekable();
493 let mut escaped = false;
494
495 while let Some(ch) = chars.next() {
496 if escaped {
497 current.push(ch);
499 escaped = false;
500 continue;
501 }
502
503 match ch {
504 '\\' => {
505 escaped = true;
507 }
508 '.' => {
509 if !current.is_empty() {
511 if let Ok(index) = current.parse::<usize>() {
513 segments.push(PathSegment::Index(index));
514 } else {
515 segments.push(PathSegment::Key(current.clone()));
516 }
517 current.clear();
518 }
519 }
520 '[' => {
521 if !current.is_empty() {
523 segments.push(PathSegment::Key(current.clone()));
524 current.clear();
525 }
526
527 let mut index_str = String::new();
529 let mut closed = false;
530 while let Some(&next_ch) = chars.peek() {
531 if next_ch == ']' {
532 chars.next(); closed = true;
534 break;
535 }
536 index_str.push(chars.next().unwrap());
537 }
538
539 if !closed {
540 return Err(PathParseError::UnclosedIndex);
541 }
542 match index_str.parse::<usize>() {
543 Ok(index) => segments.push(PathSegment::Index(index)),
544 Err(_) => return Err(PathParseError::InvalidIndex(index_str)),
545 }
546 }
547 _ => {
548 current.push(ch);
549 }
550 }
551 }
552
553 if !current.is_empty() {
555 if let Ok(index) = current.parse::<usize>() {
556 segments.push(PathSegment::Index(index));
557 } else {
558 segments.push(PathSegment::Key(current));
559 }
560 }
561
562 Ok(segments)
563}
564
565#[deprecated(note = "use try_parse_path; a bad [index] used to return an empty Vec")]
570pub fn parse_path(path: &str) -> Vec<PathSegment> {
571 try_parse_path(path).unwrap_or_default()
572}
573
574fn path_segments_required(path: &str) -> Result<Vec<PathSegment>, PathError> {
578 let segments = try_parse_path(path)?;
579 if segments.is_empty() {
580 Err(PathError::EmptyPath)
581 } else {
582 Ok(segments)
583 }
584}
585
586fn segment_display(segment: &PathSegment) -> String {
589 match segment {
590 PathSegment::Key(key) => key.clone(),
591 PathSegment::Index(index) => format!("[{index}]"),
592 }
593}
594
595fn node_as_mapping(node: &crate::as_yaml::YamlNode) -> Option<Mapping> {
600 match node {
601 crate::as_yaml::YamlNode::Mapping(m) => Some(m.clone()),
602 crate::as_yaml::YamlNode::TaggedNode(t) => t.as_mapping(),
603 _ => None,
604 }
605}
606
607fn node_as_sequence(node: &crate::as_yaml::YamlNode) -> Option<crate::yaml::Sequence> {
610 match node {
611 crate::as_yaml::YamlNode::Sequence(s) => Some(s.clone()),
612 crate::as_yaml::YamlNode::TaggedNode(t) => t.as_sequence(),
613 _ => None,
614 }
615}
616
617fn is_replaceable_scalar(node: &crate::as_yaml::YamlNode) -> bool {
621 matches!(node, crate::as_yaml::YamlNode::Scalar(_))
622}
623
624fn navigate_path(
632 mut current: crate::as_yaml::YamlNode,
633 segments: &[PathSegment],
634 policy: AliasPolicy,
635) -> Result<crate::as_yaml::YamlNode, PathError> {
636 for segment in segments {
637 current = descend_one(current, segment, policy)?;
638 }
639 Ok(current)
640}
641
642fn registry_for(node: &crate::as_yaml::YamlNode) -> crate::anchor_resolution::AnchorRegistry {
646 let root = node
647 .syntax()
648 .ancestors()
649 .last()
650 .unwrap_or_else(|| node.syntax().clone());
651 crate::anchor_resolution::AnchorRegistry::from_tree(&root)
652}
653
654fn resolve_for_read(
660 current: crate::as_yaml::YamlNode,
661 segment: &PathSegment,
662 policy: AliasPolicy,
663) -> Result<crate::as_yaml::YamlNode, PathError> {
664 let crate::as_yaml::YamlNode::Alias(alias) = ¤t else {
665 return Ok(current);
666 };
667 let name = alias.name();
668
669 if policy == AliasPolicy::Refuse {
670 return Err(PathError::AliasRefused {
671 at: segment_display(segment),
672 alias: name,
673 });
674 }
675
676 let registry = registry_for(¤t);
677 let target = registry
678 .resolve(&name)
679 .cloned()
680 .ok_or_else(|| PathError::UndefinedAlias {
681 at: segment_display(segment),
682 alias: name.clone(),
683 })?;
684
685 crate::as_yaml::YamlNode::from_syntax_peeled(target).ok_or(PathError::UndefinedAlias {
686 at: segment_display(segment),
687 alias: name,
688 })
689}
690
691fn lookup_merged(mapping: &Mapping, key: &str) -> Option<crate::as_yaml::YamlNode> {
697 if let Some(found) = mapping.get(key) {
698 return Some(found);
699 }
700 use crate::anchor_resolution::MappingMergedExt;
701 let registry = registry_for(&crate::as_yaml::YamlNode::Mapping(mapping.clone()));
702 mapping.merged(®istry).get(key)
703}
704
705fn descend_one(
706 current: crate::as_yaml::YamlNode,
707 segment: &PathSegment,
708 policy: AliasPolicy,
709) -> Result<crate::as_yaml::YamlNode, PathError> {
710 let current = resolve_for_read(current, segment, policy)?;
711 match segment {
712 PathSegment::Key(key) => {
713 let mapping = node_as_mapping(¤t).ok_or_else(|| PathError::TypeMismatch {
714 at: segment_display(segment),
715 })?;
716 lookup_merged(&mapping, key).ok_or_else(|| PathError::NotFound {
717 at: segment_display(segment),
718 })
719 }
720 PathSegment::Index(index) => {
721 if let Some(seq) = node_as_sequence(¤t) {
722 seq.get(*index).ok_or_else(|| PathError::NotFound {
723 at: segment_display(segment),
724 })
725 } else if let Some(map) = node_as_mapping(¤t) {
726 lookup_merged(&map, &index.to_string()).ok_or_else(|| PathError::NotFound {
728 at: segment_display(segment),
729 })
730 } else {
731 Err(PathError::TypeMismatch {
732 at: segment_display(segment),
733 })
734 }
735 }
736 }
737}
738
739fn segment_key(segment: &PathSegment) -> String {
743 match segment {
744 PathSegment::Key(key) => key.clone(),
745 PathSegment::Index(index) => index.to_string(),
746 }
747}
748
749impl YamlPath for crate::yaml::Document {
751 fn try_get_path_with(
752 &self,
753 path: &str,
754 policy: AliasPolicy,
755 ) -> Result<crate::as_yaml::YamlNode, PathError> {
756 let segments = path_segments_required(path)?;
757
758 let root = if let Some(m) = self.as_mapping() {
760 crate::as_yaml::YamlNode::Mapping(m)
761 } else if let Some(s) = self.as_sequence() {
762 crate::as_yaml::YamlNode::Sequence(s)
763 } else if let Some(sc) = self.as_scalar() {
764 crate::as_yaml::YamlNode::Scalar(sc)
765 } else {
766 return Err(PathError::NoRoot);
767 };
768
769 navigate_path(root, &segments, policy)
770 }
771
772 fn try_set_path_with(
773 &self,
774 path: &str,
775 value: impl crate::AsYaml,
776 policy: AliasPolicy,
777 ) -> Result<(), PathError> {
778 let segments = path_segments_required(path)?;
779
780 let mapping = self.as_mapping().ok_or(PathError::NoRoot)?;
783
784 set_path_on_mapping(&mapping, &segments, value, policy)
785 }
786
787 fn try_remove_path_with(
788 &self,
789 path: &str,
790 policy: AliasPolicy,
791 ) -> Result<crate::as_yaml::YamlNode, PathError> {
792 let segments = path_segments_required(path)?;
793
794 let root = if let Some(m) = self.as_mapping() {
795 crate::as_yaml::YamlNode::Mapping(m)
796 } else if let Some(s) = self.as_sequence() {
797 crate::as_yaml::YamlNode::Sequence(s)
798 } else {
799 return Err(PathError::NoRoot);
800 };
801
802 remove_path_impl(root, &segments, policy)
803 }
804}
805
806fn remove_path_impl(
814 root: crate::as_yaml::YamlNode,
815 segments: &[PathSegment],
816 policy: AliasPolicy,
817) -> Result<crate::as_yaml::YamlNode, PathError> {
818 debug_assert!(!segments.is_empty(), "caller must reject empty paths");
819
820 let root = resolve_for_read(root, &segments[0], policy)?;
824
825 if segments.len() == 1 {
826 let seg = &segments[0];
827 let key = match seg {
828 PathSegment::Key(key) => key.clone(),
829 PathSegment::Index(index) => {
830 if node_as_mapping(&root).is_none() {
834 return Err(PathError::TypeMismatch {
835 at: segment_display(seg),
836 });
837 }
838 index.to_string()
839 }
840 };
841 let mapping = node_as_mapping(&root).ok_or_else(|| PathError::TypeMismatch {
842 at: segment_display(seg),
843 })?;
844 let value = mapping
848 .get(key.as_str())
849 .ok_or_else(|| PathError::NotFound {
850 at: segment_display(seg),
851 })?;
852 mapping.remove(key.as_str());
853 return Ok(value);
854 }
855
856 let nested = descend_one(root, &segments[0], policy)?;
858 remove_path_impl(nested, &segments[1..], policy)
859}
860
861impl YamlPath for Mapping {
863 fn try_get_path_with(
864 &self,
865 path: &str,
866 policy: AliasPolicy,
867 ) -> Result<crate::as_yaml::YamlNode, PathError> {
868 let segments = path_segments_required(path)?;
869 navigate_path(
870 crate::as_yaml::YamlNode::Mapping(self.clone()),
871 &segments,
872 policy,
873 )
874 }
875
876 fn try_set_path_with(
877 &self,
878 path: &str,
879 value: impl crate::AsYaml,
880 policy: AliasPolicy,
881 ) -> Result<(), PathError> {
882 let segments = path_segments_required(path)?;
883 set_path_on_mapping(self, &segments, value, policy)
884 }
885
886 fn try_remove_path_with(
887 &self,
888 path: &str,
889 policy: AliasPolicy,
890 ) -> Result<crate::as_yaml::YamlNode, PathError> {
891 let segments = path_segments_required(path)?;
892 remove_path_impl(
893 crate::as_yaml::YamlNode::Mapping(self.clone()),
894 &segments,
895 policy,
896 )
897 }
898}
899
900fn resolve_alias_for_write(
909 alias: &crate::yaml::Alias,
910 segment: &PathSegment,
911 policy: AliasPolicy,
912) -> Result<crate::as_yaml::YamlNode, PathError> {
913 use rowan::ast::AstNode;
914
915 let name = alias.name();
916 if policy == AliasPolicy::Refuse {
917 return Err(PathError::AliasRefused {
918 at: segment_display(segment),
919 alias: name,
920 });
921 }
922
923 let undefined = || PathError::UndefinedAlias {
924 at: segment_display(segment),
925 alias: name.clone(),
926 };
927
928 let root = alias
929 .syntax()
930 .ancestors()
931 .last()
932 .unwrap_or_else(|| alias.syntax().clone());
933 let target = crate::anchor_resolution::AnchorRegistry::from_tree(&root)
934 .resolve(&name)
935 .cloned()
936 .ok_or_else(undefined)?;
937
938 let target = if policy == AliasPolicy::Expand {
939 crate::yaml::SyntaxNode::new_root_mut(target.green().into_owned())
942 } else {
943 target
944 };
945
946 crate::as_yaml::YamlNode::from_syntax_peeled(target).ok_or_else(undefined)
947}
948
949fn set_path_on_node<V: crate::AsYaml>(
953 node: crate::as_yaml::YamlNode,
954 segments: &[PathSegment],
955 value: V,
956 policy: AliasPolicy,
957 via: &PathSegment,
958) -> Result<(), PathError> {
959 if let Some(m) = node.as_mapping() {
960 set_path_on_mapping(m, segments, value, policy)
961 } else if let Some(seq) = node.as_sequence() {
962 set_path_on_sequence(seq, segments, value, policy)
963 } else {
964 Err(PathError::TypeMismatch {
965 at: segment_display(via),
966 })
967 }
968}
969
970fn set_path_on_mapping<V: crate::AsYaml>(
980 mapping: &Mapping,
981 segments: &[PathSegment],
982 value: V,
983 policy: AliasPolicy,
984) -> Result<(), PathError> {
985 debug_assert!(!segments.is_empty(), "caller must reject empty paths");
986
987 let first_key_owned = segment_key(&segments[0]);
990 let first_key = first_key_owned.as_str();
991
992 if segments.len() == 1 {
993 mapping.set(first_key, value);
997 return Ok(());
998 }
999
1000 if let Some(crate::as_yaml::YamlNode::Alias(alias)) = mapping.get(first_key) {
1004 let resolved = resolve_alias_for_write(&alias, &segments[0], policy)?;
1005 let target = if policy == AliasPolicy::Expand {
1006 mapping.set(first_key, resolved);
1007 mapping
1010 .get(first_key)
1011 .ok_or_else(|| PathError::TypeMismatch {
1012 at: segment_display(&segments[0]),
1013 })?
1014 } else {
1015 resolved
1016 };
1017 return set_path_on_node(target, &segments[1..], value, policy, &segments[0]);
1018 }
1019
1020 let next_wants_sequence = matches!(segments[1], PathSegment::Index(_));
1022
1023 if let Some(existing) = mapping.get(first_key) {
1028 if let Some(sc) = existing.as_scalar() {
1029 let s = sc.as_string();
1030 let is_null_placeholder = s.is_empty() || s.eq_ignore_ascii_case("null") || s == "~";
1031 if !is_null_placeholder {
1032 return Err(PathError::TypeMismatch {
1033 at: segment_display(&segments[0]),
1034 });
1035 }
1036 }
1037 }
1038
1039 let existing = mapping.get(first_key);
1040
1041 if next_wants_sequence {
1042 if let Some(nested) = existing.as_ref().and_then(node_as_sequence) {
1043 return set_path_on_sequence(&nested, &segments[1..], value, policy);
1044 }
1045 if let Some(nested) = existing.as_ref().and_then(node_as_mapping) {
1048 return set_path_on_mapping(&nested, &segments[1..], value, policy);
1049 }
1050 if existing.as_ref().is_some_and(|n| !is_replaceable_scalar(n)) {
1051 return Err(PathError::TypeMismatch {
1052 at: segment_display(&segments[0]),
1053 });
1054 }
1055 if let PathSegment::Index(index) = segments[1] {
1060 if index >= MAX_INDEX_GROWTH {
1061 return Err(PathError::IndexTooFar {
1062 at: segment_display(&segments[1]),
1063 len: 0,
1064 index,
1065 });
1066 }
1067 }
1068 if mapping.is_flow_style() {
1071 mapping.set(first_key, crate::yaml::Sequence::new_flow());
1072 } else {
1073 mapping.set(first_key, crate::yaml::Sequence::new_pending_block());
1074 }
1075 let nested = mapping
1076 .get_sequence(first_key)
1077 .expect("we just inserted this key as a sequence");
1078 return set_path_on_sequence(&nested, &segments[1..], value, policy);
1079 }
1080
1081 if let Some(nested) = existing.as_ref().and_then(node_as_mapping) {
1082 return set_path_on_mapping(&nested, &segments[1..], value, policy);
1083 }
1084 if existing.as_ref().is_some_and(|n| !is_replaceable_scalar(n)) {
1085 return Err(PathError::TypeMismatch {
1086 at: segment_display(&segments[0]),
1087 });
1088 }
1089
1090 if mapping.is_flow_style() {
1093 mapping.set(first_key, Mapping::new_flow());
1094 } else {
1095 mapping.set(first_key, Mapping::new_pending_block());
1096 }
1097
1098 let nested = mapping
1099 .get_mapping(first_key)
1100 .expect("we just inserted this key as a mapping");
1101 set_path_on_mapping(&nested, &segments[1..], value, policy)
1102}
1103
1104fn set_path_on_sequence<V: crate::AsYaml>(
1111 sequence: &crate::yaml::Sequence,
1112 segments: &[PathSegment],
1113 value: V,
1114 policy: AliasPolicy,
1115) -> Result<(), PathError> {
1116 debug_assert!(!segments.is_empty(), "caller must reject empty paths");
1117
1118 let index = match &segments[0] {
1119 PathSegment::Index(i) => *i,
1120 PathSegment::Key(_) => {
1124 return Err(PathError::TypeMismatch {
1125 at: segment_display(&segments[0]),
1126 });
1127 }
1128 };
1129
1130 let len = sequence.len();
1134 if index >= len && index - len >= MAX_INDEX_GROWTH {
1137 return Err(PathError::IndexTooFar {
1138 at: segment_display(&segments[0]),
1139 len,
1140 index,
1141 });
1142 }
1143 if let Some(growth) = (index + 1).checked_sub(len) {
1144 sequence.extend_with(crate::scalar::ScalarValue::null(), growth);
1145 }
1146
1147 if segments.len() == 1 {
1148 sequence.set(index, value);
1151 return Ok(());
1152 }
1153
1154 if let Some(crate::as_yaml::YamlNode::Alias(alias)) = sequence.get(index) {
1157 let resolved = resolve_alias_for_write(&alias, &segments[0], policy)?;
1158 let target = if policy == AliasPolicy::Expand {
1159 sequence.set(index, resolved);
1160 sequence.get(index).ok_or_else(|| PathError::TypeMismatch {
1161 at: segment_display(&segments[0]),
1162 })?
1163 } else {
1164 resolved
1165 };
1166 return set_path_on_node(target, &segments[1..], value, policy, &segments[0]);
1167 }
1168
1169 if let Some(existing) = sequence.get(index) {
1174 if let Some(sc) = existing.as_scalar() {
1175 let s = sc.as_string();
1176 let is_null_placeholder = s.is_empty() || s.eq_ignore_ascii_case("null") || s == "~";
1177 if !is_null_placeholder {
1178 return Err(PathError::TypeMismatch {
1179 at: segment_display(&segments[0]),
1180 });
1181 }
1182 }
1183 }
1184
1185 let next_wants_sequence = matches!(segments[1], PathSegment::Index(_));
1186 let existing = sequence.get(index);
1187
1188 if next_wants_sequence {
1189 if let Some(nested) = existing.as_ref().and_then(node_as_sequence) {
1190 return set_path_on_sequence(&nested, &segments[1..], value, policy);
1191 }
1192 if let Some(nested) = existing.as_ref().and_then(node_as_mapping) {
1193 return set_path_on_mapping(&nested, &segments[1..], value, policy);
1194 }
1195 if existing.as_ref().is_some_and(|n| !is_replaceable_scalar(n)) {
1196 return Err(PathError::TypeMismatch {
1197 at: segment_display(&segments[0]),
1198 });
1199 }
1200 let flow_empty = crate::builder::SequenceBuilder::new()
1207 .build_document()
1208 .as_sequence()
1209 .expect("SequenceBuilder always produces a sequence");
1210 sequence.set(index, flow_empty);
1211 let nested = sequence
1212 .get(index)
1213 .and_then(|n| n.as_sequence().cloned())
1214 .expect("we just inserted a sequence at this index");
1215 return set_path_on_sequence(&nested, &segments[1..], value, policy);
1216 }
1217
1218 if let Some(nested) = existing.as_ref().and_then(node_as_mapping) {
1219 return set_path_on_mapping(&nested, &segments[1..], value, policy);
1220 }
1221 if existing.as_ref().is_some_and(|n| !is_replaceable_scalar(n)) {
1222 return Err(PathError::TypeMismatch {
1223 at: segment_display(&segments[0]),
1224 });
1225 }
1226 let flow_empty = crate::builder::MappingBuilder::new()
1227 .build_document()
1228 .as_mapping()
1229 .expect("MappingBuilder always produces a mapping");
1230 sequence.set(index, flow_empty);
1231 let nested = sequence
1232 .get(index)
1233 .and_then(|n| n.as_mapping().cloned())
1234 .expect("we just inserted a mapping at this index");
1235 set_path_on_mapping(&nested, &segments[1..], value, policy)
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240 use super::*;
1241
1242 #[test]
1243 fn set_path_bounds_sequence_growth() {
1244 use crate::Document;
1245 use std::str::FromStr;
1246
1247 let doc = Document::from_str("m: {}\n").unwrap();
1251 let err = doc
1252 .try_set_path("999.hh.hh.999.9aa", "")
1253 .expect_err("a far index must be refused");
1254 assert!(
1255 matches!(err, PathError::IndexTooFar { .. }),
1256 "unexpected error: {err}"
1257 );
1258 assert!(doc.to_string().len() < 64, "{:?}", doc.to_string());
1260
1261 let doc = Document::from_str("m: {}\n").unwrap();
1264 doc.try_set_path("s[5]", "v").expect("small index is fine");
1265 assert_eq!(doc.try_get_path("s[5]").unwrap().to_string(), "v");
1266
1267 let doc = Document::from_str("m: {}\n").unwrap();
1268 assert!(matches!(
1269 doc.try_set_path(&format!("s[{MAX_INDEX_GROWTH}]"), "v"),
1270 Err(PathError::IndexTooFar { .. })
1271 ));
1272 }
1273
1274 fn merge_doc() -> crate::yaml::YamlFile {
1275 use std::str::FromStr;
1276 crate::yaml::YamlFile::from_str(
1277 "base: &b\n a: 1\n shared: x\nderived:\n <<: *b\n own: 2\n a: 99\n",
1278 )
1279 .unwrap()
1280 }
1281
1282 #[test]
1283 fn get_path_resolves_merged_keys() {
1284 let file = merge_doc();
1285 let doc = file.document().unwrap();
1286
1287 assert_eq!(doc.try_get_path("derived.shared").unwrap().to_string(), "x");
1289 assert_eq!(doc.try_get_path("derived.a").unwrap().to_string(), "99");
1291 assert_eq!(doc.try_get_path("derived.own").unwrap().to_string(), "2");
1293 assert_eq!(doc.try_get_path("base.a").unwrap().to_string(), "1");
1294 }
1295
1296 #[test]
1297 fn get_path_still_reports_genuinely_absent_keys() {
1298 let file = merge_doc();
1299 let doc = file.document().unwrap();
1300 assert!(doc.try_get_path("derived.nope").is_err());
1301 }
1302
1303 #[test]
1304 fn remove_path_stays_literal_for_merged_keys() {
1305 let file = merge_doc();
1308 let before = file.to_string();
1309 let doc = file.document().unwrap();
1310 assert!(doc.try_remove_path("derived.shared").is_err());
1311 assert_eq!(file.to_string(), before);
1312 }
1313
1314 #[test]
1315 fn test_parse_path_basic() {
1316 assert_eq!(try_parse_path("").unwrap(), Vec::<PathSegment>::new());
1317 assert_eq!(
1318 try_parse_path("key").unwrap(),
1319 vec![PathSegment::Key("key".to_string())]
1320 );
1321 assert_eq!(
1322 try_parse_path("a.b").unwrap(),
1323 vec![
1324 PathSegment::Key("a".to_string()),
1325 PathSegment::Key("b".to_string())
1326 ]
1327 );
1328 assert_eq!(
1329 try_parse_path("a.b.c.d").unwrap(),
1330 vec![
1331 PathSegment::Key("a".to_string()),
1332 PathSegment::Key("b".to_string()),
1333 PathSegment::Key("c".to_string()),
1334 PathSegment::Key("d".to_string())
1335 ]
1336 );
1337 }
1338
1339 #[test]
1340 fn test_try_parse_path_invalid_brackets() {
1341 assert_eq!(
1342 try_parse_path("items[abc].name"),
1343 Err(PathParseError::InvalidIndex("abc".to_string()))
1344 );
1345 assert_eq!(
1346 try_parse_path("items[].name"),
1347 Err(PathParseError::InvalidIndex(String::new()))
1348 );
1349 assert_eq!(
1350 try_parse_path("items[0"),
1351 Err(PathParseError::UnclosedIndex)
1352 );
1353 }
1354
1355 #[test]
1356 #[allow(deprecated)]
1357 fn test_parse_path_deprecated_wrapper_empty_on_bad_index() {
1358 assert_eq!(parse_path("items[abc].name"), Vec::<PathSegment>::new());
1359 assert_eq!(parse_path("items[0"), Vec::<PathSegment>::new());
1360 }
1361
1362 #[test]
1363 fn test_parse_path_with_array_indices() {
1364 assert_eq!(
1365 try_parse_path("items[0]").unwrap(),
1366 vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
1367 );
1368 assert_eq!(
1369 try_parse_path("items[0].name").unwrap(),
1370 vec![
1371 PathSegment::Key("items".to_string()),
1372 PathSegment::Index(0),
1373 PathSegment::Key("name".to_string())
1374 ]
1375 );
1376 assert_eq!(
1377 try_parse_path("data.items[5].value").unwrap(),
1378 vec![
1379 PathSegment::Key("data".to_string()),
1380 PathSegment::Key("items".to_string()),
1381 PathSegment::Index(5),
1382 PathSegment::Key("value".to_string())
1383 ]
1384 );
1385 }
1386
1387 #[test]
1388 fn test_parse_path_with_numeric_indices() {
1389 assert_eq!(
1390 try_parse_path("items.0").unwrap(),
1391 vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
1392 );
1393 assert_eq!(
1394 try_parse_path("items.0.name").unwrap(),
1395 vec![
1396 PathSegment::Key("items".to_string()),
1397 PathSegment::Index(0),
1398 PathSegment::Key("name".to_string())
1399 ]
1400 );
1401 }
1402
1403 #[test]
1404 fn test_parse_path_with_escaping() {
1405 assert_eq!(
1406 try_parse_path("key\\.with\\.dots").unwrap(),
1407 vec![PathSegment::Key("key.with.dots".to_string())]
1408 );
1409 assert_eq!(
1410 try_parse_path("a.key\\.with\\.dots.b").unwrap(),
1411 vec![
1412 PathSegment::Key("a".to_string()),
1413 PathSegment::Key("key.with.dots".to_string()),
1414 PathSegment::Key("b".to_string())
1415 ]
1416 );
1417 }
1418
1419 #[test]
1420 fn test_get_path_with_array_index() {
1421 use crate::yaml::Document;
1422 use std::str::FromStr;
1423
1424 let yaml = r#"
1425items:
1426 - name: first
1427 value: 1
1428 - name: second
1429 value: 2
1430"#;
1431 let doc = Document::from_str(yaml).unwrap();
1432
1433 let name = doc.try_get_path("items[0].name").ok();
1435 assert_eq!(
1436 name.as_ref()
1437 .and_then(|v| v.as_scalar())
1438 .map(|s| s.as_string()),
1439 Some("first".to_string())
1440 );
1441
1442 let value = doc.try_get_path("items[1].value").ok();
1443 assert_eq!(
1444 value
1445 .as_ref()
1446 .and_then(|v| v.as_scalar())
1447 .map(|s| s.as_string()),
1448 Some("2".to_string())
1449 );
1450 }
1451
1452 #[test]
1453 fn test_get_path_with_numeric_index() {
1454 use crate::yaml::Document;
1455 use std::str::FromStr;
1456
1457 let yaml = r#"
1458items:
1459 - name: first
1460 value: 1
1461 - name: second
1462 value: 2
1463"#;
1464 let doc = Document::from_str(yaml).unwrap();
1465
1466 let name = doc.try_get_path("items.0.name").ok();
1468 assert_eq!(
1469 name.as_ref()
1470 .and_then(|v| v.as_scalar())
1471 .map(|s| s.as_string()),
1472 Some("first".to_string())
1473 );
1474
1475 let value = doc.try_get_path("items.1.value").ok();
1476 assert_eq!(
1477 value
1478 .as_ref()
1479 .and_then(|v| v.as_scalar())
1480 .map(|s| s.as_string()),
1481 Some("2".to_string())
1482 );
1483 }
1484
1485 #[test]
1486 fn test_get_path_with_escaping() {
1487 use crate::yaml::Document;
1488
1489 let doc = Document::new();
1490 doc.set("key.with.dots", "test value");
1491
1492 assert!(doc.try_get_path("key.with.dots").is_err());
1494
1495 let value = doc.try_get_path("key\\.with\\.dots").ok();
1497 assert_eq!(
1498 value
1499 .as_ref()
1500 .and_then(|v| v.as_scalar())
1501 .map(|s| s.as_string()),
1502 Some("test value".to_string())
1503 );
1504 }
1505
1506 #[test]
1507 fn test_get_path_array_only() {
1508 use crate::yaml::Document;
1509 use std::str::FromStr;
1510
1511 let yaml = r#"
1512- first
1513- second
1514- third
1515"#;
1516 let doc = Document::from_str(yaml).unwrap();
1517
1518 let item = doc.try_get_path("0").ok();
1520 assert_eq!(
1521 item.as_ref()
1522 .and_then(|v| v.as_scalar())
1523 .map(|s| s.as_string()),
1524 Some("first".to_string())
1525 );
1526
1527 let item = doc.try_get_path("2").ok();
1528 assert_eq!(
1529 item.as_ref()
1530 .and_then(|v| v.as_scalar())
1531 .map(|s| s.as_string()),
1532 Some("third".to_string())
1533 );
1534 }
1535
1536 #[test]
1537 fn test_remove_path_with_array_index() {
1538 use crate::yaml::Document;
1539 use std::str::FromStr;
1540
1541 let yaml = r#"
1542items:
1543 - name: first
1544 nested:
1545 key: value
1546"#;
1547 let doc = Document::from_str(yaml).unwrap();
1548
1549 assert!(doc.try_remove_path("items[0].nested.key").is_ok());
1551 assert!(doc.try_get_path("items[0].nested.key").is_err());
1552
1553 assert!(doc.try_get_path("items[0].nested").is_ok());
1555 }
1556
1557 #[test]
1558 fn test_mapping_get_path_with_indices() {
1559 use crate::yaml::Document;
1560 use std::str::FromStr;
1561
1562 let yaml = r#"
1563config:
1564 servers:
1565 - host: server1.com
1566 port: 8080
1567 - host: server2.com
1568 port: 9090
1569"#;
1570 let doc = Document::from_str(yaml).unwrap();
1571 let mapping = doc.as_mapping().unwrap();
1572
1573 let host = mapping.try_get_path("config.servers[0].host").ok();
1575 assert_eq!(
1576 host.as_ref()
1577 .and_then(|v| v.as_scalar())
1578 .map(|s| s.as_string()),
1579 Some("server1.com".to_string())
1580 );
1581
1582 let port = mapping.try_get_path("config.servers.1.port").ok();
1583 assert_eq!(
1584 port.as_ref()
1585 .and_then(|v| v.as_scalar())
1586 .map(|s| s.as_string()),
1587 Some("9090".to_string())
1588 );
1589 }
1590
1591 #[test]
1592 fn test_get_path_simple() {
1593 use crate::yaml::Document;
1594 use std::str::FromStr;
1595
1596 let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
1597
1598 let name = yaml.try_get_path("name").ok();
1599 assert_eq!(
1600 name.as_ref()
1601 .and_then(|v| v.as_scalar())
1602 .map(|s| s.to_string()),
1603 Some("Alice".to_string())
1604 );
1605
1606 let age = yaml.try_get_path("age").ok();
1607 assert_eq!(
1608 age.as_ref()
1609 .and_then(|v| v.as_scalar())
1610 .map(|s| s.to_string()),
1611 Some("30".to_string())
1612 );
1613 }
1614
1615 #[test]
1616 fn test_get_path_nested() {
1617 use crate::yaml::Document;
1618 use std::str::FromStr;
1619
1620 let yaml = Document::from_str("server:\n host: localhost\n port: 8080\n").unwrap();
1621
1622 let host = yaml.try_get_path("server.host").ok();
1623 assert_eq!(
1624 host.as_ref()
1625 .and_then(|v| v.as_scalar())
1626 .map(|s| s.to_string()),
1627 Some("localhost".to_string())
1628 );
1629
1630 let port = yaml.try_get_path("server.port").ok();
1631 assert_eq!(
1632 port.as_ref()
1633 .and_then(|v| v.as_scalar())
1634 .map(|s| s.to_string()),
1635 Some("8080".to_string())
1636 );
1637 }
1638
1639 #[test]
1640 fn test_get_path_deeply_nested() {
1641 use crate::yaml::Document;
1642 use std::str::FromStr;
1643
1644 let yaml = Document::from_str(
1645 "app:\n database:\n primary:\n host: db.example.com\n port: 5432\n",
1646 )
1647 .unwrap();
1648
1649 let host = yaml.try_get_path("app.database.primary.host").ok();
1650 assert_eq!(
1651 host.as_ref()
1652 .and_then(|v| v.as_scalar())
1653 .map(|s| s.to_string()),
1654 Some("db.example.com".to_string())
1655 );
1656
1657 let port = yaml.try_get_path("app.database.primary.port").ok();
1658 assert_eq!(
1659 port.as_ref()
1660 .and_then(|v| v.as_scalar())
1661 .map(|s| s.to_string()),
1662 Some("5432".to_string())
1663 );
1664 }
1665
1666 #[test]
1667 fn test_get_path_missing() {
1668 use crate::yaml::Document;
1669 use std::str::FromStr;
1670
1671 let yaml = Document::from_str("name: Alice\n").unwrap();
1672
1673 assert!(matches!(
1674 yaml.try_get_path("missing"),
1675 Err(PathError::NotFound { .. })
1676 ));
1677 assert!(matches!(
1679 yaml.try_get_path("name.nested"),
1680 Err(PathError::TypeMismatch { .. })
1681 ));
1682 assert_eq!(yaml.try_get_path(""), Err(PathError::EmptyPath));
1683 }
1684
1685 #[test]
1686 fn test_set_path_existing_key() {
1687 use crate::yaml::Document;
1688 use std::str::FromStr;
1689
1690 let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
1691
1692 yaml.try_set_path("name", "Bob").expect("set_path");
1693
1694 assert_eq!(yaml.to_string(), "name: Bob\nage: 30\n");
1695 }
1696
1697 #[test]
1698 fn test_set_path_new_key() {
1699 use crate::yaml::Document;
1700 use std::str::FromStr;
1701
1702 let yaml = Document::from_str("name: Alice\n").unwrap();
1703
1704 yaml.try_set_path("age", 30).expect("set_path");
1705
1706 assert_eq!(yaml.to_string(), "name: Alice\nage: 30\n");
1707 }
1708
1709 #[test]
1710 fn test_set_path_nested_existing() {
1711 use crate::yaml::Document;
1712 use std::str::FromStr;
1713
1714 let yaml = Document::from_str("server:\n host: localhost\n port: 8080\n").unwrap();
1715
1716 yaml.try_set_path("server.port", 9000).expect("set_path");
1717
1718 assert_eq!(
1719 yaml.to_string(),
1720 "server:\n host: localhost\n port: 9000\n"
1721 );
1722 }
1723
1724 #[test]
1725 fn test_set_path_nested_new() {
1726 use crate::yaml::Document;
1727 use std::str::FromStr;
1728
1729 let yaml = Document::from_str("server:\n host: localhost\n").unwrap();
1730
1731 yaml.try_set_path("server.port", 8080).expect("set_path");
1732
1733 assert_eq!(
1734 yaml.to_string(),
1735 "server:\n host: localhost\n port: 8080\n"
1736 );
1737 }
1738
1739 #[test]
1740 fn test_set_path_create_intermediate() {
1741 use crate::yaml::Document;
1742 use std::str::FromStr;
1743
1744 let yaml = Document::from_str("name: test\n").unwrap();
1745
1746 yaml.try_set_path("server.database.host", "localhost")
1747 .expect("set_path");
1748
1749 assert_eq!(
1750 yaml.to_string(),
1751 "name: test\nserver:\n database:\n host: localhost\n"
1752 );
1753
1754 let host = yaml.try_get_path("server.database.host").ok();
1756 assert_eq!(
1757 host.as_ref()
1758 .and_then(|v| v.as_scalar())
1759 .map(|s| s.to_string()),
1760 Some("localhost".to_string())
1761 );
1762 }
1763
1764 #[test]
1765 fn test_set_path_deeply_nested_create() {
1766 use crate::yaml::Document;
1767 use std::str::FromStr;
1768
1769 let yaml = Document::from_str("app: {}\n").unwrap();
1770
1771 yaml.try_set_path("app.database.primary.host", "db.example.com")
1772 .expect("set_path");
1773 yaml.try_set_path("app.database.primary.port", 5432)
1774 .expect("set_path");
1775
1776 assert_eq!(
1778 yaml.to_string().trim(),
1779 r#"app: {database: {primary: {host: "db.example.com", port: 5432}}}"#
1780 );
1781
1782 let host = yaml.try_get_path("app.database.primary.host").ok();
1783 assert_eq!(
1784 host.as_ref()
1785 .and_then(|v| v.as_scalar())
1786 .map(|s| s.as_string()),
1787 Some("db.example.com".to_string())
1788 );
1789
1790 let port = yaml.try_get_path("app.database.primary.port").ok();
1791 assert_eq!(port.as_ref().and_then(|v| v.to_i64()), Some(5432));
1792 }
1793
1794 #[test]
1795 fn test_remove_path_simple() {
1796 use crate::yaml::Document;
1797 use std::str::FromStr;
1798
1799 let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
1800
1801 let result = yaml.try_remove_path("age").is_ok();
1802 assert!(result);
1803
1804 assert_eq!(yaml.to_string(), "name: Alice\n");
1805 }
1806
1807 #[test]
1808 fn test_remove_path_nested() {
1809 use crate::yaml::Document;
1810 use std::str::FromStr;
1811
1812 let yaml = Document::from_str("server:\n host: localhost\n port: 8080\n").unwrap();
1813
1814 let result = yaml.try_remove_path("server.port").is_ok();
1815 assert!(result);
1816
1817 assert_eq!(yaml.to_string(), "server:\n host: localhost\n");
1818 }
1819
1820 #[test]
1821 fn test_remove_path_missing() {
1822 use crate::yaml::Document;
1823 use std::str::FromStr;
1824
1825 let yaml = Document::from_str("name: Alice\n").unwrap();
1826
1827 let result = yaml.try_remove_path("missing").is_ok();
1828 assert!(!result);
1829
1830 let result = yaml.try_remove_path("name.nested").is_ok();
1831 assert!(!result);
1832
1833 assert_eq!(yaml.to_string(), "name: Alice\n");
1835 }
1836
1837 #[test]
1838 fn test_remove_path_deeply_nested() {
1839 use crate::yaml::Document;
1840 use std::str::FromStr;
1841
1842 let yaml = Document::from_str(
1843 "app:\n database:\n primary:\n host: db.example.com\n port: 5432\n",
1844 )
1845 .unwrap();
1846
1847 let result = yaml.try_remove_path("app.database.primary.port").is_ok();
1848 assert!(result);
1849
1850 assert_eq!(
1851 yaml.to_string(),
1852 "app:\n database:\n primary:\n host: db.example.com\n"
1853 );
1854 }
1855
1856 #[test]
1857 fn test_remove_path_keeps_sibling_indentation() {
1858 use crate::yaml::Document;
1859 use std::str::FromStr;
1860
1861 let doc = Document::from_str("config:\n a: 1\n b: 2\n c: 3\n").unwrap();
1866 doc.try_remove_path("config.b").unwrap();
1867 assert_eq!(doc.to_string(), "config:\n a: 1\n c: 3\n");
1868
1869 let doc = Document::from_str("config:\n a: 1\n b: 2\n c: 3\n").unwrap();
1870 doc.try_remove_path("config.a").unwrap();
1871 assert_eq!(doc.to_string(), "config:\n b: 2\n c: 3\n");
1872
1873 let doc = Document::from_str("config:\n a: 1\n b: 2\n c: 3\n").unwrap();
1874 doc.try_remove_path("config.c").unwrap();
1875 assert_eq!(doc.to_string(), "config:\n a: 1\n b: 2\n");
1876
1877 let doc = Document::from_str("x:\n y:\n a: 1\n b: 2\n").unwrap();
1879 doc.try_remove_path("x.y.a").unwrap();
1880 assert_eq!(doc.to_string(), "x:\n y:\n b: 2\n");
1881 }
1882
1883 #[test]
1884 fn test_path_on_mapping_directly() {
1885 use crate::yaml::Document;
1886 use std::str::FromStr;
1887
1888 let yaml = Document::from_str("server:\n host: localhost\n").unwrap();
1889 let mapping = yaml.as_mapping().unwrap();
1890
1891 let host = mapping.try_get_path("server.host").ok();
1893 assert_eq!(
1894 host.as_ref()
1895 .and_then(|v| v.as_scalar())
1896 .map(|s| s.to_string()),
1897 Some("localhost".to_string())
1898 );
1899
1900 mapping.try_set_path("server.port", 8080).expect("set_path");
1902 assert_eq!(
1903 yaml.to_string(),
1904 "server:\n host: localhost\n port: 8080\n"
1905 );
1906
1907 let result = mapping.try_remove_path("server.port").is_ok();
1909 assert!(result);
1910
1911 let result_missing = mapping.try_remove_path("nonexistent.path").is_ok();
1913 assert!(!result_missing);
1914 }
1915
1916 #[test]
1917 fn test_mapping_remove_path_single_segment() {
1918 use crate::yaml::Document;
1919 use std::str::FromStr;
1920
1921 let yaml = Document::from_str("a: 1\nb: 2\n").unwrap();
1922 let mapping = yaml.as_mapping().unwrap();
1923
1924 assert!(mapping.try_remove_path("a").is_ok());
1926 assert!(mapping.try_get_path("a").is_err());
1927 assert!(mapping.try_get_path("b").is_ok());
1928
1929 assert!(mapping.try_remove_path("missing").is_err());
1931 }
1932
1933 #[test]
1934 fn test_set_path_preserves_formatting() {
1935 use crate::yaml::Document;
1936 use std::str::FromStr;
1937
1938 let yaml = Document::from_str("server:\n host: localhost # production server\n").unwrap();
1939
1940 yaml.try_set_path("server.host", "newhost")
1941 .expect("set_path");
1942
1943 assert_eq!(
1944 yaml.to_string(),
1945 "server:\n host: newhost # production server\n"
1946 );
1947 }
1948
1949 #[test]
1950 fn test_multiple_path_operations() {
1951 use crate::yaml::Document;
1952 use std::str::FromStr;
1953
1954 let yaml = Document::from_str("name: test\n").unwrap();
1955
1956 yaml.try_set_path("server.host", "localhost")
1958 .expect("set_path");
1959 yaml.try_set_path("server.port", 8080).expect("set_path");
1960 yaml.try_set_path("database.host", "db.local")
1961 .expect("set_path");
1962 yaml.try_set_path("database.port", 5432).expect("set_path");
1963
1964 assert_eq!(
1966 yaml.try_get_path("server.host")
1967 .ok()
1968 .as_ref()
1969 .and_then(|v| v.as_scalar())
1970 .map(|s| s.to_string()),
1971 Some("localhost".to_string())
1972 );
1973 assert_eq!(
1974 yaml.try_get_path("server.port")
1975 .ok()
1976 .as_ref()
1977 .and_then(|v| v.as_scalar())
1978 .map(|s| s.to_string()),
1979 Some("8080".to_string())
1980 );
1981 assert_eq!(
1982 yaml.try_get_path("database.host")
1983 .ok()
1984 .as_ref()
1985 .and_then(|v| v.as_scalar())
1986 .map(|s| s.to_string()),
1987 Some("db.local".to_string())
1988 );
1989 assert_eq!(
1990 yaml.try_get_path("database.port")
1991 .ok()
1992 .as_ref()
1993 .and_then(|v| v.as_scalar())
1994 .map(|s| s.to_string()),
1995 Some("5432".to_string())
1996 );
1997
1998 yaml.try_remove_path("server.port").expect("remove_path");
2000 yaml.try_remove_path("database.host").expect("remove_path");
2001
2002 assert!(yaml.try_get_path("server.port").is_err());
2004 assert!(yaml.try_get_path("database.host").is_err());
2005
2006 assert_eq!(
2008 yaml.try_get_path("server.host")
2009 .ok()
2010 .as_ref()
2011 .and_then(|v| v.as_scalar())
2012 .map(|s| s.to_string()),
2013 Some("localhost".to_string())
2014 );
2015 assert_eq!(
2016 yaml.try_get_path("database.port")
2017 .ok()
2018 .as_ref()
2019 .and_then(|v| v.as_scalar())
2020 .map(|s| s.to_string()),
2021 Some("5432".to_string())
2022 );
2023 }
2024
2025 #[test]
2026 fn test_set_path_creates_intermediate_sequence() {
2027 use crate::yaml::Document;
2032 use std::str::FromStr;
2033 let doc = Document::from_str("base: true\n").unwrap();
2034 doc.try_set_path("a.b[0].c", "value").expect("set_path");
2035 assert_eq!(
2036 doc.to_string(),
2037 "base: true\na:\n b:\n - {c: \"value\"}\n"
2038 );
2039 }
2040
2041 #[test]
2042 fn test_set_path_into_existing_sequence_by_index() {
2043 use crate::yaml::Document;
2044 use std::str::FromStr;
2045 let doc = Document::from_str("items:\n - a\n - b\n").unwrap();
2046 doc.try_set_path("items[1]", "B").expect("set_path");
2047 assert_eq!(doc.to_string(), "items:\n - a\n - B\n");
2048 }
2049
2050 #[test]
2051 fn test_set_path_grows_sequence_with_nulls() {
2052 use crate::yaml::Document;
2053 use std::str::FromStr;
2054 let doc = Document::from_str("items:\n - a\n").unwrap();
2055 doc.try_set_path("items[3]", "z").expect("set_path");
2056 assert_eq!(
2057 doc.to_string(),
2058 "items:\n - a\n - null\n - null\n - z\n"
2059 );
2060 }
2061
2062 #[test]
2063 fn test_set_path_index_does_not_replace_existing_mapping() {
2064 use crate::yaml::Document;
2065 use std::str::FromStr;
2066 let doc = Document::from_str("m:\n a: 1\n b: 2\n").unwrap();
2067 doc.try_set_path("m[0]", "z").expect("set_path");
2068 assert_eq!(doc.to_string(), "m:\n a: 1\n b: 2\n '0': z\n");
2069
2070 let doc = Document::from_str("m:\n \"0\":\n x: 1\n").unwrap();
2071 assert_eq!(
2072 doc.try_get_path("m.0.x")
2073 .unwrap()
2074 .as_scalar()
2075 .unwrap()
2076 .as_string(),
2077 "1"
2078 );
2079 doc.try_set_path("m.0.x", "2").expect("set_path");
2080 assert_eq!(
2081 doc.try_get_path("m.0.x")
2082 .unwrap()
2083 .as_scalar()
2084 .unwrap()
2085 .as_string(),
2086 "2"
2087 );
2088 assert!(doc.get_mapping("m").is_some());
2089
2090 let doc = Document::from_str("items:\n - a: 1\n b: 2\n").unwrap();
2091 doc.try_set_path("items[0][0]", "z").expect("set_path");
2092 assert_eq!(doc.to_string(), "items:\n - a: 1\n b: 2\n '0': z\n");
2093 }
2094
2095 #[test]
2096 fn test_set_path_key_does_not_replace_existing_sequence() {
2097 use crate::yaml::Document;
2098 use std::str::FromStr;
2099 let original = "items:\n- a\n- b\n";
2100 let doc = Document::from_str(original).unwrap();
2101 assert!(matches!(
2102 doc.try_get_path("items.foo"),
2103 Err(PathError::TypeMismatch { .. })
2104 ));
2105 assert!(matches!(
2106 doc.try_set_path("items.foo", "x"),
2107 Err(PathError::TypeMismatch { .. })
2108 ));
2109 assert_eq!(doc.to_string(), original);
2110
2111 let original = "other: &o\n a: 1\nitems: *o\n";
2114 let doc = Document::from_str(original).unwrap();
2115 assert_eq!(
2116 doc.try_set_path("items.foo", "x"),
2117 Err(PathError::AliasRefused {
2118 at: "items".to_string(),
2119 alias: "o".to_string()
2120 })
2121 );
2122 assert_eq!(doc.to_string(), original);
2123 }
2124
2125 #[test]
2126 fn test_set_path_index_does_not_replace_alias() {
2127 use crate::yaml::Document;
2128 use std::str::FromStr;
2129
2130 let original = "other: &o\n- a\n- b\nitems: *o\n";
2131 let doc = Document::from_str(original).unwrap();
2132 assert_eq!(
2133 doc.try_get_path("items[0]").unwrap().to_string(),
2134 "a".to_string()
2135 );
2136 assert_eq!(
2137 doc.try_set_path("items[0]", "x"),
2138 Err(PathError::AliasRefused {
2139 at: "items".to_string(),
2140 alias: "o".to_string()
2141 })
2142 );
2143 assert_eq!(doc.to_string(), original);
2144
2145 let doc = Document::from_str("items: !!seq [1, 2]\n").unwrap();
2148 doc.try_set_path("items[1]", "x").unwrap();
2149 assert_eq!(doc.to_string(), "items: !!seq [1, x]\n");
2150
2151 let original = "other: &o\n a: 1\ntop:\n - *o\n";
2152 let doc = Document::from_str(original).unwrap();
2153 assert_eq!(
2154 doc.try_set_path("top[0][0]", "x"),
2155 Err(PathError::AliasRefused {
2156 at: "[0]".to_string(),
2157 alias: "o".to_string()
2158 })
2159 );
2160 assert_eq!(doc.to_string(), original);
2161
2162 let doc = Document::from_str("top:\n - !custom\n a: 1\n").unwrap();
2165 doc.try_set_path("top[0][0]", "x").unwrap();
2166 assert_eq!(doc.to_string(), "top:\n - !custom\n a: 1\n '0': x\n");
2167 }
2168
2169 #[test]
2170 fn test_path_descends_into_tagged_mapping() {
2171 use crate::yaml::Document;
2172 use std::str::FromStr;
2173
2174 let doc = Document::from_str("config: !custom\n a: 1\n b: 2\n").unwrap();
2175 assert_eq!(
2176 doc.try_get_path("config.a")
2177 .unwrap()
2178 .as_scalar()
2179 .unwrap()
2180 .as_string(),
2181 "1"
2182 );
2183
2184 doc.try_set_path("config.a", 5).unwrap();
2185 doc.try_set_path("config.c", 7).unwrap();
2186 assert_eq!(doc.to_string(), "config: !custom\n a: 5\n b: 2\n c: 7\n");
2187 }
2188
2189 #[test]
2190 fn test_path_descends_into_tagged_sequence() {
2191 use crate::yaml::Document;
2192 use std::str::FromStr;
2193
2194 let doc = Document::from_str("items: !!seq [1, 2]\n").unwrap();
2195 assert_eq!(
2196 doc.try_get_path("items[0]")
2197 .unwrap()
2198 .as_scalar()
2199 .unwrap()
2200 .as_string(),
2201 "1"
2202 );
2203
2204 doc.try_set_path("items[0]", 9).unwrap();
2205 doc.try_set_path("items[2]", 7).unwrap();
2206 assert_eq!(doc.to_string(), "items: !!seq [9, 2, 7]\n");
2207 }
2208
2209 #[test]
2210 fn test_path_descends_through_nested_tags() {
2211 use crate::yaml::Document;
2212 use std::str::FromStr;
2213
2214 let doc = Document::from_str("a: !outer\n b: !inner\n c: 1\n").unwrap();
2215 doc.try_set_path("a.b.c", 2).unwrap();
2216 assert_eq!(doc.to_string(), "a: !outer\n b: !inner\n c: 2\n");
2217
2218 let doc = Document::from_str("l: !t\n - x: 1\n").unwrap();
2219 doc.try_set_path("l[0].x", 9).unwrap();
2220 assert_eq!(doc.to_string(), "l: !t\n - x: 9\n");
2221 }
2222
2223 #[test]
2224 fn test_remove_path_in_tagged_mapping_keeps_tag() {
2225 use crate::yaml::Document;
2226 use std::str::FromStr;
2227
2228 let doc = Document::from_str("config: !custom\n a: 1\n b: 2\n").unwrap();
2229 let removed = doc.try_remove_path("config.b").unwrap();
2230 assert_eq!(removed.as_scalar().unwrap().as_string(), "2");
2231 assert_eq!(doc.to_string(), "config: !custom\n a: 1\n");
2232
2233 doc.try_remove_path("config.a").unwrap();
2236 assert_eq!(doc.to_string(), "config: !custom {}\n");
2237 }
2238
2239 #[test]
2240 fn test_path_does_not_descend_into_tagged_scalar() {
2241 use crate::yaml::Document;
2242 use std::str::FromStr;
2243
2244 let original = "config: !custom hello\n";
2245 let doc = Document::from_str(original).unwrap();
2246 assert!(matches!(
2247 doc.try_get_path("config.a"),
2248 Err(PathError::TypeMismatch { .. })
2249 ));
2250 assert!(matches!(
2251 doc.try_set_path("config.a", 1),
2252 Err(PathError::TypeMismatch { .. })
2253 ));
2254 assert_eq!(doc.to_string(), original);
2255 }
2256
2257 const ALIASED: &str = "shared: &shared\n timeout: 30\nservice: *shared\n";
2258
2259 #[test]
2260 fn test_get_path_resolves_alias_by_default() {
2261 use crate::yaml::Document;
2262 use std::str::FromStr;
2263
2264 let doc = Document::from_str(ALIASED).unwrap();
2265 assert_eq!(
2266 doc.try_get_path("service.timeout").unwrap().to_string(),
2267 "30".to_string()
2268 );
2269 assert_eq!(doc.to_string(), ALIASED.to_string());
2270 }
2271
2272 #[test]
2273 fn test_get_path_refuse_policy_reports_alias() {
2274 use crate::yaml::Document;
2275 use std::str::FromStr;
2276
2277 let doc = Document::from_str(ALIASED).unwrap();
2278 assert_eq!(
2279 doc.try_get_path_with("service.timeout", AliasPolicy::Refuse),
2280 Err(PathError::AliasRefused {
2281 at: "timeout".to_string(),
2282 alias: "shared".to_string()
2283 })
2284 );
2285 }
2286
2287 #[test]
2288 fn test_get_path_undefined_alias() {
2289 use crate::yaml::Document;
2290 use std::str::FromStr;
2291
2292 let doc = Document::from_str(
2293 "service: *missing
2294",
2295 )
2296 .unwrap();
2297 assert_eq!(
2298 doc.try_get_path("service.timeout"),
2299 Err(PathError::UndefinedAlias {
2300 at: "timeout".to_string(),
2301 alias: "missing".to_string()
2302 })
2303 );
2304 }
2305
2306 #[test]
2307 fn test_set_path_refuses_alias_by_default() {
2308 use crate::yaml::Document;
2309 use std::str::FromStr;
2310
2311 let doc = Document::from_str(ALIASED).unwrap();
2312 assert_eq!(
2313 doc.try_set_path("service.timeout", 60),
2314 Err(PathError::AliasRefused {
2315 at: "service".to_string(),
2316 alias: "shared".to_string()
2317 })
2318 );
2319 assert_eq!(doc.to_string(), ALIASED.to_string());
2320 }
2321
2322 #[test]
2323 fn test_set_path_follow_writes_through_to_anchor() {
2324 use crate::yaml::Document;
2325 use std::str::FromStr;
2326
2327 let doc = Document::from_str(ALIASED).unwrap();
2328 doc.try_set_path_with("service.timeout", 60, AliasPolicy::Follow)
2329 .unwrap();
2330 assert_eq!(
2332 doc.to_string(),
2333 "shared: &shared\n timeout: 60\nservice: *shared\n".to_string()
2334 );
2335 }
2336
2337 #[test]
2338 fn test_set_path_expand_copies_without_anchor() {
2339 use crate::yaml::Document;
2340 use std::str::FromStr;
2341
2342 let doc = Document::from_str(ALIASED).unwrap();
2343 doc.try_set_path_with("service.timeout", 60, AliasPolicy::Expand)
2344 .unwrap();
2345 assert_eq!(
2348 doc.to_string(),
2349 "shared: &shared\n timeout: 30\nservice:\n timeout: 60\n".to_string()
2350 );
2351 }
2352
2353 #[test]
2354 fn test_set_path_expand_leaves_other_aliases_alone() {
2355 use crate::yaml::Document;
2356 use std::str::FromStr;
2357
2358 let src = "shared: &shared\n timeout: 30\na: *shared\nb: *shared\n";
2359 let doc = Document::from_str(src).unwrap();
2360 doc.try_set_path_with("a.timeout", 60, AliasPolicy::Expand)
2361 .unwrap();
2362 assert_eq!(
2363 doc.to_string(),
2364 "shared: &shared\n timeout: 30\na:\n timeout: 60\nb: *shared\n".to_string()
2365 );
2366 }
2367
2368 #[test]
2369 fn test_set_path_replacing_alias_outright_needs_no_policy() {
2370 use crate::yaml::Document;
2371 use std::str::FromStr;
2372
2373 let doc = Document::from_str(ALIASED).unwrap();
2376 doc.try_set_path("service", 1).unwrap();
2377 assert_eq!(
2378 doc.to_string(),
2379 "shared: &shared\n timeout: 30\nservice: 1\n".to_string()
2380 );
2381 }
2382
2383 #[test]
2384 fn test_set_path_undefined_alias_is_reported() {
2385 use crate::yaml::Document;
2386 use std::str::FromStr;
2387
2388 let src = "service: *missing\n";
2389 let doc = Document::from_str(src).unwrap();
2390 assert_eq!(
2391 doc.try_set_path_with("service.timeout", 1, AliasPolicy::Follow),
2392 Err(PathError::UndefinedAlias {
2393 at: "service".to_string(),
2394 alias: "missing".to_string()
2395 })
2396 );
2397 assert_eq!(doc.to_string(), src.to_string());
2398 }
2399
2400 #[test]
2401 fn test_remove_path_refuses_alias_by_default() {
2402 use crate::yaml::Document;
2403 use std::str::FromStr;
2404
2405 let doc = Document::from_str(ALIASED).unwrap();
2406 assert_eq!(
2407 doc.try_remove_path("service.timeout"),
2408 Err(PathError::AliasRefused {
2409 at: "timeout".to_string(),
2410 alias: "shared".to_string()
2411 })
2412 );
2413 assert_eq!(doc.to_string(), ALIASED.to_string());
2414 }
2415
2416 #[test]
2417 fn test_remove_path_follow_removes_from_anchor() {
2418 use crate::yaml::Document;
2419 use std::str::FromStr;
2420
2421 let src = "shared: &shared\n timeout: 30\n retries: 3\nservice: *shared\n";
2422 let doc = Document::from_str(src).unwrap();
2423 let removed = doc
2424 .try_remove_path_with("service.timeout", AliasPolicy::Follow)
2425 .unwrap();
2426 assert_eq!(removed.to_string(), "30".to_string());
2427 assert_eq!(
2430 doc.to_string(),
2431 "shared: &shared\n retries: 3\nservice: *shared\n".to_string()
2432 );
2433 }
2434
2435 #[test]
2436 fn test_alias_to_sequence_resolves_for_read() {
2437 use crate::yaml::Document;
2438 use std::str::FromStr;
2439
2440 let doc = Document::from_str("other: &o\n- a\n- b\nitems: *o\n").unwrap();
2441 assert_eq!(
2442 doc.try_get_path("items[1]").unwrap().to_string(),
2443 "b".to_string()
2444 );
2445 }
2446
2447 #[test]
2448 fn test_get_path_on_mapping_resolves_document_anchors() {
2449 use crate::yaml::Document;
2450 use std::str::FromStr;
2451
2452 let doc =
2456 Document::from_str("shared: &shared\n timeout: 30\nouter:\n svc: *shared\n").unwrap();
2457 let outer = doc.as_mapping().unwrap().get_mapping("outer").unwrap();
2458 assert_eq!(
2459 outer.try_get_path("svc.timeout").unwrap().to_string(),
2460 "30".to_string()
2461 );
2462 }
2463
2464 #[test]
2465 fn test_recursive_alias_terminates() {
2466 use crate::yaml::Document;
2467 use std::str::FromStr;
2468
2469 let doc = Document::from_str("a: &x\n b: *x\n").unwrap();
2472 assert!(doc.try_get_path("a.b.b.b").is_ok());
2473 }
2474}