1use crate::builder::MappingBuilder;
29use crate::yaml::Mapping;
30
31pub trait YamlPath {
41 fn get_path(&self, path: &str) -> Option<crate::as_yaml::YamlNode>;
62
63 fn set_path(&self, path: &str, value: impl crate::AsYaml);
81
82 fn remove_path(&self, path: &str) -> bool;
103}
104
105#[derive(Debug, Clone, PartialEq)]
107pub enum PathSegment {
108 Key(String),
110 Index(usize),
112}
113
114pub fn parse_path(path: &str) -> Vec<PathSegment> {
147 if path.is_empty() {
148 return vec![];
149 }
150
151 let mut segments = Vec::new();
152 let mut current = String::new();
153 let mut chars = path.chars().peekable();
154 let mut escaped = false;
155
156 while let Some(ch) = chars.next() {
157 if escaped {
158 current.push(ch);
160 escaped = false;
161 continue;
162 }
163
164 match ch {
165 '\\' => {
166 escaped = true;
168 }
169 '.' => {
170 if !current.is_empty() {
172 if let Ok(index) = current.parse::<usize>() {
174 segments.push(PathSegment::Index(index));
175 } else {
176 segments.push(PathSegment::Key(current.clone()));
177 }
178 current.clear();
179 }
180 }
181 '[' => {
182 if !current.is_empty() {
184 segments.push(PathSegment::Key(current.clone()));
185 current.clear();
186 }
187
188 let mut index_str = String::new();
190 while let Some(&next_ch) = chars.peek() {
191 if next_ch == ']' {
192 chars.next(); break;
194 }
195 index_str.push(chars.next().unwrap());
196 }
197
198 if let Ok(index) = index_str.parse::<usize>() {
200 segments.push(PathSegment::Index(index));
201 }
202 }
203 _ => {
204 current.push(ch);
205 }
206 }
207 }
208
209 if !current.is_empty() {
211 if let Ok(index) = current.parse::<usize>() {
212 segments.push(PathSegment::Index(index));
213 } else {
214 segments.push(PathSegment::Key(current));
215 }
216 }
217
218 segments
219}
220
221fn navigate_path(
225 mut current: crate::as_yaml::YamlNode,
226 segments: &[PathSegment],
227) -> Option<crate::as_yaml::YamlNode> {
228 for segment in segments {
229 match segment {
230 PathSegment::Key(key) => {
231 let mapping = current.as_mapping()?;
233 current = mapping.get(key)?;
234 }
235 PathSegment::Index(index) => {
236 let sequence = current.as_sequence()?;
238 current = sequence.get(*index)?;
239 }
240 }
241 }
242
243 Some(current)
244}
245
246impl YamlPath for crate::yaml::Document {
248 fn get_path(&self, path: &str) -> Option<crate::as_yaml::YamlNode> {
249 let segments = parse_path(path);
250 if segments.is_empty() {
251 return None;
252 }
253
254 let root = if let Some(m) = self.as_mapping() {
256 crate::as_yaml::YamlNode::Mapping(m)
257 } else if let Some(s) = self.as_sequence() {
258 crate::as_yaml::YamlNode::Sequence(s)
259 } else if let Some(sc) = self.as_scalar() {
260 crate::as_yaml::YamlNode::Scalar(sc)
261 } else {
262 return None;
263 };
264
265 navigate_path(root, &segments)
267 }
268
269 fn set_path(&self, path: &str, value: impl crate::AsYaml) {
270 let segments = parse_path(path);
271 if segments.is_empty() {
272 return;
273 }
274
275 let mapping = match self.as_mapping() {
277 Some(m) => m,
278 None => return,
279 };
280
281 set_path_impl(&mapping, &segments, value);
282 }
283
284 fn remove_path(&self, path: &str) -> bool {
285 let segments = parse_path(path);
286 if segments.is_empty() {
287 return false;
288 }
289
290 let root = if let Some(m) = self.as_mapping() {
292 crate::as_yaml::YamlNode::Mapping(m)
293 } else if let Some(s) = self.as_sequence() {
294 crate::as_yaml::YamlNode::Sequence(s)
295 } else {
296 return false;
297 };
298
299 remove_path_impl(root, &segments)
300 }
301}
302
303fn set_path_impl<V: crate::AsYaml>(mapping: &Mapping, segments: &[PathSegment], value: V) {
307 set_path_on_mapping(mapping, segments, value);
308}
309
310fn remove_path_impl(root: crate::as_yaml::YamlNode, segments: &[PathSegment]) -> bool {
314 if segments.is_empty() {
315 return false;
316 }
317
318 if segments.len() == 1 {
319 match &segments[0] {
321 PathSegment::Key(key) => {
322 if let Some(mapping) = root.as_mapping() {
323 return mapping.remove(key.as_str()).is_some();
324 }
325 }
326 PathSegment::Index(_) => {
327 return false;
330 }
331 }
332 return false;
333 }
334
335 match &segments[0] {
337 PathSegment::Key(key) => {
338 if let Some(mapping) = root.as_mapping() {
339 if let Some(nested) = mapping.get(key.as_str()) {
340 return remove_path_impl(nested, &segments[1..]);
341 }
342 }
343 }
344 PathSegment::Index(index) => {
345 if let Some(sequence) = root.as_sequence() {
346 if let Some(nested) = sequence.get(*index) {
347 return remove_path_impl(nested, &segments[1..]);
348 }
349 }
350 }
351 }
352
353 false
354}
355
356impl YamlPath for Mapping {
358 fn get_path(&self, path: &str) -> Option<crate::as_yaml::YamlNode> {
359 let segments = parse_path(path);
360 if segments.is_empty() {
361 return None;
362 }
363
364 let first_key = match &segments[0] {
366 PathSegment::Key(key) => key.as_str(),
367 PathSegment::Index(_) => return None, };
369
370 if segments.len() == 1 {
371 return self.get(first_key);
372 }
373
374 let current = self.get(first_key)?;
376 navigate_path(current, &segments[1..])
377 }
378
379 fn set_path(&self, path: &str, value: impl crate::AsYaml) {
380 let segments = parse_path(path);
381 if segments.is_empty() {
382 return;
383 }
384
385 set_path_on_mapping(self, &segments, value);
386 }
387
388 fn remove_path(&self, path: &str) -> bool {
389 let segments = parse_path(path);
390 if segments.is_empty() {
391 return false;
392 }
393
394 remove_path_from_mapping(self, &segments)
395 }
396}
397
398fn set_path_on_mapping<V: crate::AsYaml>(mapping: &Mapping, segments: &[PathSegment], value: V) {
402 if segments.is_empty() {
403 return;
404 }
405
406 let first_key = match &segments[0] {
408 PathSegment::Key(key) => key.as_str(),
409 PathSegment::Index(_) => return, };
411
412 if segments.len() == 1 {
413 mapping.set(first_key, value);
415 return;
416 }
417
418 if let Some(nested) = mapping.get_mapping(first_key) {
420 set_path_on_mapping(&nested, &segments[1..], value);
422 } else {
423 let empty_mapping = MappingBuilder::new()
425 .build_document()
426 .as_mapping()
427 .expect("MappingBuilder always produces a mapping");
428 mapping.set(first_key, &empty_mapping);
429
430 if let Some(nested) = mapping.get_mapping(first_key) {
432 set_path_on_mapping(&nested, &segments[1..], value);
433 }
434 }
435}
436
437fn remove_path_from_mapping(mapping: &Mapping, segments: &[PathSegment]) -> bool {
441 if segments.is_empty() {
442 return false;
443 }
444
445 let first_key = match &segments[0] {
447 PathSegment::Key(key) => key.as_str(),
448 PathSegment::Index(_) => return false, };
450
451 if segments.len() == 1 {
452 return mapping.remove(first_key).is_some();
454 }
455
456 if let Some(nested) = mapping.get_mapping(first_key) {
458 remove_path_from_mapping(&nested, &segments[1..])
459 } else {
460 false }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_parse_path_basic() {
470 assert_eq!(parse_path(""), Vec::<PathSegment>::new());
471 assert_eq!(parse_path("key"), vec![PathSegment::Key("key".to_string())]);
472 assert_eq!(
473 parse_path("a.b"),
474 vec![
475 PathSegment::Key("a".to_string()),
476 PathSegment::Key("b".to_string())
477 ]
478 );
479 assert_eq!(
480 parse_path("a.b.c.d"),
481 vec![
482 PathSegment::Key("a".to_string()),
483 PathSegment::Key("b".to_string()),
484 PathSegment::Key("c".to_string()),
485 PathSegment::Key("d".to_string())
486 ]
487 );
488 }
489
490 #[test]
491 fn test_parse_path_with_array_indices() {
492 assert_eq!(
493 parse_path("items[0]"),
494 vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
495 );
496 assert_eq!(
497 parse_path("items[0].name"),
498 vec![
499 PathSegment::Key("items".to_string()),
500 PathSegment::Index(0),
501 PathSegment::Key("name".to_string())
502 ]
503 );
504 assert_eq!(
505 parse_path("data.items[5].value"),
506 vec![
507 PathSegment::Key("data".to_string()),
508 PathSegment::Key("items".to_string()),
509 PathSegment::Index(5),
510 PathSegment::Key("value".to_string())
511 ]
512 );
513 }
514
515 #[test]
516 fn test_parse_path_with_numeric_indices() {
517 assert_eq!(
518 parse_path("items.0"),
519 vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
520 );
521 assert_eq!(
522 parse_path("items.0.name"),
523 vec![
524 PathSegment::Key("items".to_string()),
525 PathSegment::Index(0),
526 PathSegment::Key("name".to_string())
527 ]
528 );
529 }
530
531 #[test]
532 fn test_parse_path_with_escaping() {
533 assert_eq!(
534 parse_path("key\\.with\\.dots"),
535 vec![PathSegment::Key("key.with.dots".to_string())]
536 );
537 assert_eq!(
538 parse_path("a.key\\.with\\.dots.b"),
539 vec![
540 PathSegment::Key("a".to_string()),
541 PathSegment::Key("key.with.dots".to_string()),
542 PathSegment::Key("b".to_string())
543 ]
544 );
545 }
546
547 #[test]
548 fn test_get_path_with_array_index() {
549 use crate::yaml::Document;
550 use std::str::FromStr;
551
552 let yaml = r#"
553items:
554 - name: first
555 value: 1
556 - name: second
557 value: 2
558"#;
559 let doc = Document::from_str(yaml).unwrap();
560
561 let name = doc.get_path("items[0].name");
563 assert_eq!(
564 name.as_ref()
565 .and_then(|v| v.as_scalar())
566 .map(|s| s.as_string()),
567 Some("first".to_string())
568 );
569
570 let value = doc.get_path("items[1].value");
571 assert_eq!(
572 value
573 .as_ref()
574 .and_then(|v| v.as_scalar())
575 .map(|s| s.as_string()),
576 Some("2".to_string())
577 );
578 }
579
580 #[test]
581 fn test_get_path_with_numeric_index() {
582 use crate::yaml::Document;
583 use std::str::FromStr;
584
585 let yaml = r#"
586items:
587 - name: first
588 value: 1
589 - name: second
590 value: 2
591"#;
592 let doc = Document::from_str(yaml).unwrap();
593
594 let name = doc.get_path("items.0.name");
596 assert_eq!(
597 name.as_ref()
598 .and_then(|v| v.as_scalar())
599 .map(|s| s.as_string()),
600 Some("first".to_string())
601 );
602
603 let value = doc.get_path("items.1.value");
604 assert_eq!(
605 value
606 .as_ref()
607 .and_then(|v| v.as_scalar())
608 .map(|s| s.as_string()),
609 Some("2".to_string())
610 );
611 }
612
613 #[test]
614 fn test_get_path_with_escaping() {
615 use crate::yaml::Document;
616
617 let doc = Document::new();
618 doc.set("key.with.dots", "test value");
619
620 assert!(doc.get_path("key.with.dots").is_none());
622
623 let value = doc.get_path("key\\.with\\.dots");
625 assert_eq!(
626 value
627 .as_ref()
628 .and_then(|v| v.as_scalar())
629 .map(|s| s.as_string()),
630 Some("test value".to_string())
631 );
632 }
633
634 #[test]
635 fn test_get_path_array_only() {
636 use crate::yaml::Document;
637 use std::str::FromStr;
638
639 let yaml = r#"
640- first
641- second
642- third
643"#;
644 let doc = Document::from_str(yaml).unwrap();
645
646 let item = doc.get_path("0");
648 assert_eq!(
649 item.as_ref()
650 .and_then(|v| v.as_scalar())
651 .map(|s| s.as_string()),
652 Some("first".to_string())
653 );
654
655 let item = doc.get_path("2");
656 assert_eq!(
657 item.as_ref()
658 .and_then(|v| v.as_scalar())
659 .map(|s| s.as_string()),
660 Some("third".to_string())
661 );
662 }
663
664 #[test]
665 fn test_remove_path_with_array_index() {
666 use crate::yaml::Document;
667 use std::str::FromStr;
668
669 let yaml = r#"
670items:
671 - name: first
672 nested:
673 key: value
674"#;
675 let doc = Document::from_str(yaml).unwrap();
676
677 assert!(doc.remove_path("items[0].nested.key"));
679 assert!(doc.get_path("items[0].nested.key").is_none());
680
681 assert!(doc.get_path("items[0].nested").is_some());
683 }
684
685 #[test]
686 fn test_mapping_get_path_with_indices() {
687 use crate::yaml::Document;
688 use std::str::FromStr;
689
690 let yaml = r#"
691config:
692 servers:
693 - host: server1.com
694 port: 8080
695 - host: server2.com
696 port: 9090
697"#;
698 let doc = Document::from_str(yaml).unwrap();
699 let mapping = doc.as_mapping().unwrap();
700
701 let host = mapping.get_path("config.servers[0].host");
703 assert_eq!(
704 host.as_ref()
705 .and_then(|v| v.as_scalar())
706 .map(|s| s.as_string()),
707 Some("server1.com".to_string())
708 );
709
710 let port = mapping.get_path("config.servers.1.port");
711 assert_eq!(
712 port.as_ref()
713 .and_then(|v| v.as_scalar())
714 .map(|s| s.as_string()),
715 Some("9090".to_string())
716 );
717 }
718
719 #[test]
720 fn test_get_path_simple() {
721 use crate::yaml::Document;
722 use std::str::FromStr;
723
724 let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
725
726 let name = yaml.get_path("name");
727 assert_eq!(
728 name.as_ref()
729 .and_then(|v| v.as_scalar())
730 .map(|s| s.to_string()),
731 Some("Alice".to_string())
732 );
733
734 let age = yaml.get_path("age");
735 assert_eq!(
736 age.as_ref()
737 .and_then(|v| v.as_scalar())
738 .map(|s| s.to_string()),
739 Some("30".to_string())
740 );
741 }
742
743 #[test]
744 fn test_get_path_nested() {
745 use crate::yaml::Document;
746 use std::str::FromStr;
747
748 let yaml = Document::from_str("server:\n host: localhost\n port: 8080\n").unwrap();
749
750 let host = yaml.get_path("server.host");
751 assert_eq!(
752 host.as_ref()
753 .and_then(|v| v.as_scalar())
754 .map(|s| s.to_string()),
755 Some("localhost".to_string())
756 );
757
758 let port = yaml.get_path("server.port");
759 assert_eq!(
760 port.as_ref()
761 .and_then(|v| v.as_scalar())
762 .map(|s| s.to_string()),
763 Some("8080".to_string())
764 );
765 }
766
767 #[test]
768 fn test_get_path_deeply_nested() {
769 use crate::yaml::Document;
770 use std::str::FromStr;
771
772 let yaml = Document::from_str(
773 "app:\n database:\n primary:\n host: db.example.com\n port: 5432\n",
774 )
775 .unwrap();
776
777 let host = yaml.get_path("app.database.primary.host");
778 assert_eq!(
779 host.as_ref()
780 .and_then(|v| v.as_scalar())
781 .map(|s| s.to_string()),
782 Some("db.example.com".to_string())
783 );
784
785 let port = yaml.get_path("app.database.primary.port");
786 assert_eq!(
787 port.as_ref()
788 .and_then(|v| v.as_scalar())
789 .map(|s| s.to_string()),
790 Some("5432".to_string())
791 );
792 }
793
794 #[test]
795 fn test_get_path_missing() {
796 use crate::yaml::Document;
797 use std::str::FromStr;
798
799 let yaml = Document::from_str("name: Alice\n").unwrap();
800
801 assert_eq!(yaml.get_path("missing"), None);
802 assert_eq!(yaml.get_path("name.nested"), None);
803 assert_eq!(yaml.get_path(""), None);
804 }
805
806 #[test]
807 fn test_set_path_existing_key() {
808 use crate::yaml::Document;
809 use std::str::FromStr;
810
811 let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
812
813 yaml.set_path("name", "Bob");
814
815 assert_eq!(yaml.to_string(), "name: Bob\nage: 30\n");
816 }
817
818 #[test]
819 fn test_set_path_new_key() {
820 use crate::yaml::Document;
821 use std::str::FromStr;
822
823 let yaml = Document::from_str("name: Alice\n").unwrap();
824
825 yaml.set_path("age", 30);
826
827 assert_eq!(yaml.to_string(), "name: Alice\nage: 30\n");
828 }
829
830 #[test]
831 fn test_set_path_nested_existing() {
832 use crate::yaml::Document;
833 use std::str::FromStr;
834
835 let yaml = Document::from_str("server:\n host: localhost\n port: 8080\n").unwrap();
836
837 yaml.set_path("server.port", 9000);
838
839 assert_eq!(
840 yaml.to_string(),
841 "server:\n host: localhost\n port: 9000\n"
842 );
843 }
844
845 #[test]
846 fn test_set_path_nested_new() {
847 use crate::yaml::Document;
848 use std::str::FromStr;
849
850 let yaml = Document::from_str("server:\n host: localhost\n").unwrap();
851
852 yaml.set_path("server.port", 8080);
853
854 assert_eq!(yaml.to_string(), "server:\n host: localhost\nport: 8080\n");
855 }
856
857 #[test]
858 fn test_set_path_create_intermediate() {
859 use crate::yaml::Document;
860 use std::str::FromStr;
861
862 let yaml = Document::from_str("name: test\n").unwrap();
863
864 yaml.set_path("server.database.host", "localhost");
865
866 assert_eq!(
867 yaml.to_string(),
868 "name: test\nserver:\ndatabase:\nhost: localhost\n\n\n"
869 );
870
871 let host = yaml.get_path("server.database.host");
873 assert_eq!(
874 host.as_ref()
875 .and_then(|v| v.as_scalar())
876 .map(|s| s.to_string()),
877 Some("localhost".to_string())
878 );
879 }
880
881 #[test]
882 fn test_set_path_deeply_nested_create() {
883 use crate::yaml::Document;
884 use std::str::FromStr;
885
886 let yaml = Document::from_str("app: {}\n").unwrap();
887
888 yaml.set_path("app.database.primary.host", "db.example.com");
889 yaml.set_path("app.database.primary.port", 5432);
890
891 let host = yaml.get_path("app.database.primary.host");
892 assert_eq!(
893 host.as_ref()
894 .and_then(|v| v.as_scalar())
895 .map(|s| s.to_string()),
896 Some("db.example.com".to_string())
897 );
898
899 let port = yaml.get_path("app.database.primary.port");
900 assert_eq!(
901 port.as_ref()
902 .and_then(|v| v.as_scalar())
903 .map(|s| s.to_string()),
904 Some("5432".to_string())
905 );
906 }
907
908 #[test]
909 fn test_remove_path_simple() {
910 use crate::yaml::Document;
911 use std::str::FromStr;
912
913 let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
914
915 let result = yaml.remove_path("age");
916 assert!(result);
917
918 assert_eq!(yaml.to_string(), "name: Alice");
919 }
920
921 #[test]
922 fn test_remove_path_nested() {
923 use crate::yaml::Document;
924 use std::str::FromStr;
925
926 let yaml = Document::from_str("server:\n host: localhost\n port: 8080\n").unwrap();
927
928 let result = yaml.remove_path("server.port");
929 assert!(result);
930
931 assert_eq!(yaml.to_string(), "server:\n host: localhost ");
932 }
933
934 #[test]
935 fn test_remove_path_missing() {
936 use crate::yaml::Document;
937 use std::str::FromStr;
938
939 let yaml = Document::from_str("name: Alice\n").unwrap();
940
941 let result = yaml.remove_path("missing");
942 assert!(!result);
943
944 let result = yaml.remove_path("name.nested");
945 assert!(!result);
946
947 assert_eq!(yaml.to_string(), "name: Alice\n");
949 }
950
951 #[test]
952 fn test_remove_path_deeply_nested() {
953 use crate::yaml::Document;
954 use std::str::FromStr;
955
956 let yaml = Document::from_str(
957 "app:\n database:\n primary:\n host: db.example.com\n port: 5432\n",
958 )
959 .unwrap();
960
961 let result = yaml.remove_path("app.database.primary.port");
962 assert!(result);
963
964 assert_eq!(
965 yaml.to_string(),
966 "app:\n database:\n primary:\n host: db.example.com "
967 );
968 }
969
970 #[test]
971 fn test_path_on_mapping_directly() {
972 use crate::yaml::Document;
973 use std::str::FromStr;
974
975 let yaml = Document::from_str("server:\n host: localhost\n").unwrap();
976 let mapping = yaml.as_mapping().unwrap();
977
978 let host = mapping.get_path("server.host");
980 assert_eq!(
981 host.as_ref()
982 .and_then(|v| v.as_scalar())
983 .map(|s| s.to_string()),
984 Some("localhost".to_string())
985 );
986
987 mapping.set_path("server.port", 8080);
989 assert_eq!(yaml.to_string(), "server:\n host: localhost\nport: 8080\n");
990
991 let result = mapping.remove_path("server.port");
993 assert!(result);
994
995 let result_missing = mapping.remove_path("nonexistent.path");
997 assert!(!result_missing);
998 }
999
1000 #[test]
1001 fn test_set_path_preserves_formatting() {
1002 use crate::yaml::Document;
1003 use std::str::FromStr;
1004
1005 let yaml = Document::from_str("server:\n host: localhost # production server\n").unwrap();
1006
1007 yaml.set_path("server.host", "newhost");
1008
1009 assert_eq!(
1010 yaml.to_string(),
1011 "server:\n host: newhost # production server\n"
1012 );
1013 }
1014
1015 #[test]
1016 fn test_multiple_path_operations() {
1017 use crate::yaml::Document;
1018 use std::str::FromStr;
1019
1020 let yaml = Document::from_str("name: test\n").unwrap();
1021
1022 yaml.set_path("server.host", "localhost");
1024 yaml.set_path("server.port", 8080);
1025 yaml.set_path("database.host", "db.local");
1026 yaml.set_path("database.port", 5432);
1027
1028 assert_eq!(
1030 yaml.get_path("server.host")
1031 .as_ref()
1032 .and_then(|v| v.as_scalar())
1033 .map(|s| s.to_string()),
1034 Some("localhost".to_string())
1035 );
1036 assert_eq!(
1037 yaml.get_path("server.port")
1038 .as_ref()
1039 .and_then(|v| v.as_scalar())
1040 .map(|s| s.to_string()),
1041 Some("8080".to_string())
1042 );
1043 assert_eq!(
1044 yaml.get_path("database.host")
1045 .as_ref()
1046 .and_then(|v| v.as_scalar())
1047 .map(|s| s.to_string()),
1048 Some("db.local".to_string())
1049 );
1050 assert_eq!(
1051 yaml.get_path("database.port")
1052 .as_ref()
1053 .and_then(|v| v.as_scalar())
1054 .map(|s| s.to_string()),
1055 Some("5432".to_string())
1056 );
1057
1058 yaml.remove_path("server.port");
1060 yaml.remove_path("database.host");
1061
1062 assert_eq!(yaml.get_path("server.port"), None);
1064 assert_eq!(yaml.get_path("database.host"), None);
1065
1066 assert_eq!(
1068 yaml.get_path("server.host")
1069 .as_ref()
1070 .and_then(|v| v.as_scalar())
1071 .map(|s| s.to_string()),
1072 Some("localhost".to_string())
1073 );
1074 assert_eq!(
1075 yaml.get_path("database.port")
1076 .as_ref()
1077 .and_then(|v| v.as_scalar())
1078 .map(|s| s.to_string()),
1079 Some("5432".to_string())
1080 );
1081 }
1082}