1use crate::prelude::*;
59use core::{fmt, fmt::Display, str::FromStr};
60
61#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
99pub enum Path<'a> {
100 #[default]
105 Root,
106
107 Seq {
114 parent: &'a Self,
116 index: usize,
118 },
119
120 Map {
127 parent: &'a Self,
129 key: &'a str,
131 },
132
133 Alias {
138 parent: &'a Self,
140 },
141
142 Unknown {
147 parent: &'a Self,
149 },
150}
151
152impl<'a> Path<'a> {
153 #[must_use]
170 pub fn index(&'a self, index: usize) -> Self {
171 Path::Seq {
172 parent: self,
173 index,
174 }
175 }
176
177 #[must_use]
191 pub fn key(&'a self, key: &'a str) -> Self {
192 Path::Map { parent: self, key }
193 }
194
195 #[must_use]
206 pub fn alias(&'a self) -> Self {
207 Path::Alias { parent: self }
208 }
209
210 #[must_use]
212 pub fn unknown(&'a self) -> Self {
213 Path::Unknown { parent: self }
214 }
215
216 #[must_use]
230 pub fn is_root(&self) -> bool {
231 matches!(self, Path::Root)
232 }
233
234 #[must_use]
250 pub fn parent(&self) -> Option<&Self> {
251 match self {
252 Path::Root => None,
253 Path::Seq { parent, .. }
254 | Path::Map { parent, .. }
255 | Path::Alias { parent }
256 | Path::Unknown { parent } => Some(parent),
257 }
258 }
259
260 #[must_use]
274 pub fn depth(&self) -> usize {
275 match self {
276 Path::Root => 0,
277 Path::Seq { parent, .. }
278 | Path::Map { parent, .. }
279 | Path::Alias { parent }
280 | Path::Unknown { parent } => 1 + parent.depth(),
281 }
282 }
283}
284
285impl Display for Path<'_> {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 struct Parent<'a>(&'a Path<'a>);
290
291 impl Display for Parent<'_> {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 match self.0 {
294 Path::Root => Ok(()),
295 path => write!(f, "{path}."),
296 }
297 }
298 }
299
300 struct ParentNoDot<'a>(&'a Path<'a>);
303
304 impl Display for ParentNoDot<'_> {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 match self.0 {
307 Path::Root => Ok(()),
308 path => write!(f, "{path}"),
309 }
310 }
311 }
312
313 match self {
314 Path::Root => f.write_str("."),
315 Path::Seq { parent, index } => {
316 write!(f, "{}[{}]", ParentNoDot(parent), index)
317 }
318 Path::Map { parent, key } => {
319 write!(f, "{}{}", Parent(parent), key)
320 }
321 Path::Alias { parent } => {
322 write!(f, "{}", Parent(parent))
323 }
324 Path::Unknown { parent } => {
325 write!(f, "{}?", Parent(parent))
326 }
327 }
328 }
329}
330
331#[derive(Debug, Clone, Eq, PartialEq, Hash)]
336pub enum QuerySegment {
337 Key(String),
339 Index(usize),
341 Wildcard,
343 RecursiveDescent,
345}
346
347#[derive(Debug, Clone, Eq, PartialEq, Hash)]
355pub struct QueryPath(Vec<QuerySegment>);
356
357impl QueryPath {
358 pub fn parse(input: &str) -> Result<Self, PathError> {
363 parse_query_path_checked(input).map(Self)
364 }
365
366 #[must_use]
368 pub fn segments(&self) -> &[QuerySegment] {
369 &self.0
370 }
371
372 #[must_use]
374 pub fn is_root(&self) -> bool {
375 self.0.is_empty()
376 }
377}
378
379impl FromStr for QueryPath {
380 type Err = PathError;
381
382 fn from_str(input: &str) -> Result<Self, Self::Err> {
383 Self::parse(input)
384 }
385}
386
387impl Display for QueryPath {
388 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389 let mut rendered = String::new();
390 let mut after_descent = false;
391 for segment in &self.0 {
392 match segment {
393 QuerySegment::Key(key) if is_plain_key(key) => {
394 if !rendered.is_empty() && !after_descent {
395 rendered.push('.');
396 }
397 rendered.push_str(key);
398 }
399 QuerySegment::Key(key) => rendered.push_str("e_key(key)),
400 QuerySegment::Index(index) => {
401 use core::fmt::Write as _;
402 write!(rendered, "[{index}]")?;
403 }
404 QuerySegment::Wildcard => rendered.push_str("[*]"),
405 QuerySegment::RecursiveDescent => rendered.push_str(".."),
406 }
407 after_descent = matches!(segment, QuerySegment::RecursiveDescent);
408 }
409 f.write_str(&rendered)
410 }
411}
412
413#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
415#[non_exhaustive]
416pub enum PathErrorKind {
417 EmptySegment,
419 SeparatorBeforeBracket,
421 UnterminatedBracket,
423 UnterminatedQuotedKey,
425 DanglingEscape,
427 ExpectedClosingBracket,
429 InvalidBracketSegment,
431 IndexOverflow,
433 StrayClosingBracket,
435 DanglingSeparator,
437}
438
439#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
441pub struct PathError {
442 kind: PathErrorKind,
443}
444
445impl PathError {
446 const fn new(kind: PathErrorKind) -> Self {
447 Self { kind }
448 }
449
450 #[must_use]
452 pub const fn kind(self) -> PathErrorKind {
453 self.kind
454 }
455}
456
457impl Display for PathError {
458 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459 let message = match self.kind {
460 PathErrorKind::EmptySegment => "query path contains an empty segment",
461 PathErrorKind::SeparatorBeforeBracket => {
462 "query path has a separator before a bracket segment"
463 }
464 PathErrorKind::UnterminatedBracket => "query path has an unterminated bracket",
465 PathErrorKind::UnterminatedQuotedKey => "query path has an unterminated quoted key",
466 PathErrorKind::DanglingEscape => "query path has a dangling quoted-key escape",
467 PathErrorKind::ExpectedClosingBracket => {
468 "a quoted query-path key must be followed by a closing bracket"
469 }
470 PathErrorKind::InvalidBracketSegment => {
471 "query-path bracket content must be an index or wildcard"
472 }
473 PathErrorKind::IndexOverflow => "query-path sequence index overflows usize",
474 PathErrorKind::StrayClosingBracket => "query path contains a stray closing bracket",
475 PathErrorKind::DanglingSeparator => "query path ends with a separator",
476 };
477 f.write_str(message)
478 }
479}
480
481#[cfg(feature = "std")]
482impl std::error::Error for PathError {}
483
484pub(crate) fn parse_query_path(path: &str) -> Vec<QuerySegment> {
505 parse_query_path_checked(path).unwrap_or_else(|_| {
506 vec![QuerySegment::Key("\0noyalib::invalid-query-path\0".into())]
512 })
513}
514
515fn parse_query_path_checked(path: &str) -> Result<Vec<QuerySegment>, PathError> {
516 let mut segments = Vec::new();
517 let mut current = String::new();
518 let mut chars = path.chars().peekable();
519 let mut separator_pending = false;
520
521 while let Some(c) = chars.next() {
522 match c {
523 '.' => {
524 let had_current = !current.is_empty();
525 if had_current {
526 segments.push(QuerySegment::Key(core::mem::take(&mut current)));
527 }
528 if chars.peek() == Some(&'.') {
529 if separator_pending {
530 return Err(PathError::new(PathErrorKind::EmptySegment));
531 }
532 let _ = chars.next();
533 segments.push(QuerySegment::RecursiveDescent);
534 separator_pending = false;
535 } else if (!had_current && segments.is_empty()) || separator_pending {
536 return Err(PathError::new(PathErrorKind::EmptySegment));
537 } else {
538 separator_pending = true;
539 }
540 }
541 '[' => {
542 if separator_pending {
543 return Err(PathError::new(PathErrorKind::SeparatorBeforeBracket));
544 }
545 if !current.is_empty() {
546 segments.push(QuerySegment::Key(core::mem::take(&mut current)));
547 }
548 if let Some("e @ ('"' | '\'')) = chars.peek() {
549 let _ = chars.next();
550 let key = read_quoted_key(&mut chars, quote)?;
551 if chars.next() != Some(']') {
552 return Err(PathError::new(PathErrorKind::ExpectedClosingBracket));
553 }
554 segments.push(QuerySegment::Key(key));
555 separator_pending = false;
556 continue;
557 }
558 let mut index_str = String::new();
559 let mut closed = false;
560 while let Some(&c) = chars.peek() {
561 if c == ']' {
562 let _ = chars.next();
563 closed = true;
564 break;
565 }
566 index_str.push(c);
567 let _ = chars.next();
568 }
569 if !closed {
570 return Err(PathError::new(PathErrorKind::UnterminatedBracket));
571 }
572 if index_str == "*" {
573 segments.push(QuerySegment::Wildcard);
574 } else if index_str.bytes().all(|byte| byte.is_ascii_digit())
575 && !index_str.is_empty()
576 {
577 let idx = index_str
578 .parse::<usize>()
579 .map_err(|_| PathError::new(PathErrorKind::IndexOverflow))?;
580 segments.push(QuerySegment::Index(idx));
581 } else {
582 return Err(PathError::new(PathErrorKind::InvalidBracketSegment));
583 }
584 separator_pending = false;
585 }
586 ']' => return Err(PathError::new(PathErrorKind::StrayClosingBracket)),
587 '*' => {
588 if !current.is_empty() {
589 segments.push(QuerySegment::Key(core::mem::take(&mut current)));
590 }
591 segments.push(QuerySegment::Wildcard);
592 separator_pending = false;
593 }
594 _ => {
595 current.push(c);
596 separator_pending = false;
597 }
598 }
599 }
600
601 if separator_pending {
602 return Err(PathError::new(PathErrorKind::DanglingSeparator));
603 }
604 if !current.is_empty() {
605 segments.push(QuerySegment::Key(current));
606 }
607
608 Ok(segments)
609}
610
611fn read_quoted_key(
614 chars: &mut core::iter::Peekable<core::str::Chars<'_>>,
615 quote: char,
616) -> Result<String, PathError> {
617 let mut key = String::new();
618 while let Some(c) = chars.next() {
619 match c {
620 '\\' => {
621 let escaped = chars
622 .next()
623 .ok_or_else(|| PathError::new(PathErrorKind::DanglingEscape))?;
624 key.push(escaped);
625 }
626 c if c == quote => return Ok(key),
627 c => key.push(c),
628 }
629 }
630 Err(PathError::new(PathErrorKind::UnterminatedQuotedKey))
631}
632
633fn is_plain_key(key: &str) -> bool {
636 !key.is_empty() && !key.contains(['.', '[', ']', '*'])
637}
638
639#[must_use]
654pub fn quote_key(key: &str) -> String {
655 let mut out = String::with_capacity(key.len() + 4);
656 out.push_str("[\"");
657 for c in key.chars() {
658 if matches!(c, '"' | '\\') {
659 out.push('\\');
660 }
661 out.push(c);
662 }
663 out.push_str("\"]");
664 out
665}
666
667pub fn push_key(path: &mut String, key: &str) {
682 if is_plain_key(key) {
683 if !path.is_empty() {
684 path.push('.');
685 }
686 path.push_str(key);
687 } else {
688 path.push_str("e_key(key));
689 }
690}
691
692#[must_use]
705pub fn join_keys<K: AsRef<str>>(keys: impl IntoIterator<Item = K>) -> String {
706 let mut path = String::new();
707 for key in keys {
708 push_key(&mut path, key.as_ref());
709 }
710 path
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 #[test]
718 fn test_path_root_display() {
719 let root = Path::Root;
720 assert_eq!(root.to_string(), ".");
721 }
722
723 #[test]
724 fn test_path_map_display() {
725 let root = Path::Root;
726 let key1 = Path::Map {
727 parent: &root,
728 key: "config",
729 };
730 assert_eq!(key1.to_string(), "config");
731
732 let key2 = Path::Map {
733 parent: &key1,
734 key: "server",
735 };
736 assert_eq!(key2.to_string(), "config.server");
737
738 let key3 = Path::Map {
739 parent: &key2,
740 key: "host",
741 };
742 assert_eq!(key3.to_string(), "config.server.host");
743 }
744
745 #[test]
746 fn test_path_seq_display() {
747 let root = Path::Root;
748 let items = Path::Map {
749 parent: &root,
750 key: "items",
751 };
752 let first = Path::Seq {
753 parent: &items,
754 index: 0,
755 };
756 assert_eq!(first.to_string(), "items[0]");
757
758 let second = Path::Seq {
759 parent: &items,
760 index: 1,
761 };
762 assert_eq!(second.to_string(), "items[1]");
763 }
764
765 #[test]
766 fn test_path_mixed_display() {
767 let root = Path::Root;
768 let servers = Path::Map {
769 parent: &root,
770 key: "servers",
771 };
772 let first = Path::Seq {
773 parent: &servers,
774 index: 0,
775 };
776 let host = Path::Map {
777 parent: &first,
778 key: "host",
779 };
780 assert_eq!(host.to_string(), "servers[0].host");
781 }
782
783 #[test]
784 fn test_path_unknown_display() {
785 let root = Path::Root;
786 let unknown = Path::Unknown { parent: &root };
787 assert_eq!(unknown.to_string(), "?"); let key = Path::Map {
790 parent: &root,
791 key: "test",
792 };
793 let unknown2 = Path::Unknown { parent: &key };
794 assert_eq!(unknown2.to_string(), "test.?");
795 }
796
797 #[test]
798 fn test_path_builder_methods() {
799 let root = Path::Root;
800 let config = root.key("config");
801 let items = config.key("items");
802 let first = items.index(0);
803 let name = first.key("name");
804
805 assert_eq!(name.to_string(), "config.items[0].name");
806 }
807
808 #[test]
809 fn test_path_is_root() {
810 let root = Path::Root;
811 assert!(root.is_root());
812
813 let child = root.key("test");
814 assert!(!child.is_root());
815 }
816
817 #[test]
818 fn test_path_parent() {
819 let root = Path::Root;
820 assert!(root.parent().is_none());
821
822 let child = Path::Map {
823 parent: &root,
824 key: "test",
825 };
826 assert_eq!(child.parent(), Some(&root));
827 }
828
829 #[test]
830 fn test_path_depth() {
831 let root = Path::Root;
832 assert_eq!(root.depth(), 0);
833
834 let d1 = Path::Map {
835 parent: &root,
836 key: "a",
837 };
838 assert_eq!(d1.depth(), 1);
839
840 let d2 = Path::Map {
841 parent: &d1,
842 key: "b",
843 };
844 assert_eq!(d2.depth(), 2);
845
846 let d3 = Path::Seq {
847 parent: &d2,
848 index: 0,
849 };
850 assert_eq!(d3.depth(), 3);
851 }
852
853 #[test]
854 fn test_path_default() {
855 let path: Path = Path::default();
856 assert!(path.is_root());
857 }
858
859 #[test]
860 fn test_path_equality() {
861 let root1 = Path::Root;
862 let root2 = Path::Root;
863 assert_eq!(root1, root2);
864
865 let key1 = Path::Map {
866 parent: &root1,
867 key: "test",
868 };
869 let key2 = Path::Map {
870 parent: &root2,
871 key: "test",
872 };
873 assert_eq!(key1, key2);
874
875 let key3 = Path::Map {
876 parent: &root1,
877 key: "other",
878 };
879 assert_ne!(key1, key3);
880 }
881
882 #[test]
883 fn test_path_alias() {
884 let root = Path::Root;
885 let alias = root.alias();
886 assert!(matches!(alias, Path::Alias { .. }));
887 }
888
889 #[test]
890 fn test_path_complex_real_world() {
891 let root = Path::Root;
893 let deps = root.key("dependencies");
894 let serde = deps.key("serde");
895 let features = serde.key("features");
896 let first_feature = features.index(0);
897
898 assert_eq!(first_feature.to_string(), "dependencies.serde.features[0]");
899 }
900
901 #[test]
902 fn test_path_deeply_nested() {
903 let root = Path::Root;
904 let a = root.key("a");
905 let b = a.key("b");
906 let c = b.key("c");
907 let d = c.key("d");
908 let e = d.key("e");
909
910 assert_eq!(e.to_string(), "a.b.c.d.e");
911 assert_eq!(e.depth(), 5);
912 }
913
914 #[test]
915 fn parse_query_path_handles_inline_star_after_key() {
916 let segments = parse_query_path("field*");
920 assert_eq!(segments.len(), 2);
921 assert!(matches!(&segments[0], QuerySegment::Key(s) if s == "field"));
922 assert!(matches!(&segments[1], QuerySegment::Wildcard));
923 }
924
925 #[test]
926 fn parse_query_path_rejects_unparseable_bracket_content() {
927 let segments = parse_query_path("items[abc]");
928 assert_eq!(segments.len(), 1);
929 assert!(matches!(&segments[0], QuerySegment::Key(s) if s.contains("invalid-query-path")));
930 assert!(parse_query_path_checked("items[abc]").is_err());
931 }
932
933 #[test]
934 fn parse_query_path_handles_standalone_star_segment() {
935 let segments = parse_query_path("*");
938 assert_eq!(segments.len(), 1);
939 assert!(matches!(&segments[0], QuerySegment::Wildcard));
940 }
941
942 #[test]
943 fn parse_query_path_handles_recursive_descent() {
944 let segments = parse_query_path("..name");
946 assert_eq!(segments.len(), 2);
947 assert!(matches!(&segments[0], QuerySegment::RecursiveDescent));
948 assert!(matches!(&segments[1], QuerySegment::Key(s) if s == "name"));
949 }
950
951 fn keys(segments: &[QuerySegment]) -> Vec<&str> {
952 segments
953 .iter()
954 .map(|s| match s {
955 QuerySegment::Key(k) => k.as_str(),
956 QuerySegment::Index(_) => "<index>",
957 QuerySegment::Wildcard => "<wildcard>",
958 QuerySegment::RecursiveDescent => "<descent>",
959 })
960 .collect()
961 }
962
963 #[test]
964 fn parse_query_path_reads_a_quoted_bracket_segment_as_one_key() {
965 let segments = parse_query_path(r#"labels["app.kubernetes.io/name"]"#);
966 assert_eq!(keys(&segments), ["labels", "app.kubernetes.io/name"]);
967 }
968
969 #[test]
970 fn parse_query_path_quoted_key_may_hold_any_grammar_character() {
971 for (path, key) in [
972 (r#"["*"]"#, "*"),
973 (r#"["a[0]"]"#, "a[0]"),
974 ("['a]b']", "a]b"),
975 (r#"[""]"#, ""),
976 (r#"["say \"hi\""]"#, "say \"hi\""),
977 (r#"["back\\slash"]"#, "back\\slash"),
978 (r"['it\'s']", "it's"),
979 (r#"['double "quotes"']"#, "double \"quotes\""),
980 (r#"["..x"]"#, "..x"),
981 ] {
982 let segments = parse_query_path(path);
983 assert_eq!(keys(&segments), [key], "{path}");
984 }
985 }
986
987 #[test]
988 fn parse_query_path_mixes_quoted_keys_with_the_other_segments() {
989 let segments = parse_query_path(r#"items[0]["a.b"].c[*]['d']"#);
990 assert_eq!(
991 keys(&segments),
992 ["items", "<index>", "a.b", "c", "<wildcard>", "d"]
993 );
994 assert!(matches!(segments[1], QuerySegment::Index(0)));
995 }
996
997 #[test]
998 fn parse_query_path_rejects_unterminated_quoted_key() {
999 assert!(parse_query_path_checked(r#"a["bc"#).is_err());
1000 assert!(parse_query_path_checked("a['bc").is_err());
1001 }
1002
1003 #[test]
1004 fn parse_query_path_rejects_other_partial_grammars() {
1005 for path in ["a.", ".a", "a[0", "a]", "a.[0]", r#"a["b"x]"#] {
1006 assert!(parse_query_path_checked(path).is_err(), "{path}");
1007 }
1008 }
1009
1010 #[test]
1011 fn quote_key_round_trips_every_key_through_the_parser() {
1012 for key in [
1013 "plain",
1014 "a.b",
1015 "*",
1016 "a[0]",
1017 "]",
1018 "[",
1019 "",
1020 "say \"hi\"",
1021 "back\\slash",
1022 "'",
1023 "\\\"",
1024 "app.kubernetes.io/name",
1025 "..",
1026 "[*]",
1027 "trailing\\",
1028 ] {
1029 let segments = parse_query_path("e_key(key));
1030 assert_eq!(keys(&segments), [key], "{key:?} -> {:?}", quote_key(key));
1031 }
1032 }
1033
1034 #[test]
1035 fn join_keys_quotes_only_the_keys_the_grammar_would_misread() {
1036 assert_eq!(join_keys(["server", "port"]), "server.port");
1037 assert_eq!(join_keys(["only"]), "only");
1038 assert_eq!(join_keys(Vec::<&str>::new()), "");
1039 let path = join_keys(["labels", "app.io/name", "tier", "", "x*", "y"]);
1040 assert_eq!(path, r#"labels["app.io/name"].tier[""]["x*"].y"#);
1041 assert_eq!(
1042 keys(&parse_query_path(&path)),
1043 ["labels", "app.io/name", "tier", "", "x*", "y"]
1044 );
1045 }
1046
1047 #[test]
1048 fn push_key_after_an_index_or_a_quoted_segment_needs_no_separator() {
1049 let mut path = String::from("items[0]");
1050 push_key(&mut path, "name");
1051 assert_eq!(path, "items[0].name");
1052 let mut path = quote_key("a.b");
1053 push_key(&mut path, "c");
1054 push_key(&mut path, "d]");
1055 assert_eq!(path, r#"["a.b"].c["d]"]"#);
1056 assert_eq!(keys(&parse_query_path(&path)), ["a.b", "c", "d]"]);
1057 }
1058
1059 #[test]
1060 fn query_path_reports_stable_error_categories() {
1061 for (input, kind) in [
1062 (".a", PathErrorKind::EmptySegment),
1063 ("a.[0]", PathErrorKind::SeparatorBeforeBracket),
1064 ("a[0", PathErrorKind::UnterminatedBracket),
1065 (r#"a["b"#, PathErrorKind::UnterminatedQuotedKey),
1066 (r#"a["b"x]"#, PathErrorKind::ExpectedClosingBracket),
1067 ("a[no]", PathErrorKind::InvalidBracketSegment),
1068 ("a]", PathErrorKind::StrayClosingBracket),
1069 ("a.", PathErrorKind::DanglingSeparator),
1070 ] {
1071 assert_eq!(QueryPath::parse(input).unwrap_err().kind(), kind, "{input}");
1072 }
1073
1074 let overflow = format!("items[{}]", "9".repeat(usize::BITS as usize));
1075 assert_eq!(
1076 QueryPath::parse(&overflow).unwrap_err().kind(),
1077 PathErrorKind::IndexOverflow
1078 );
1079 }
1080
1081 #[test]
1082 fn query_path_display_is_canonical_and_round_trips() {
1083 for input in [
1084 "",
1085 "server.port",
1086 "items[12].name",
1087 r#"labels["app.example/name"]"#,
1088 "items[*].name",
1089 "..name",
1090 "root..name",
1091 ] {
1092 let path = QueryPath::parse(input).unwrap();
1093 let rendered = path.to_string();
1094 assert_eq!(
1095 QueryPath::parse(&rendered).unwrap(),
1096 path,
1097 "{input} -> {rendered}"
1098 );
1099 }
1100 }
1101}