A better Rust ATProto crate

value query dsl

Orual d485c065 07712e06

Changed files
+860
crates
jacquard-common
src
types
+508
crates/jacquard-common/src/types/value.rs
··· 176 176 serde_ipld_dagcbor::to_vec(self) 177 177 } 178 178 179 + /// Get a value at a path within nested Data structures 180 + /// 181 + /// Path syntax: 182 + /// - `.field` or `field` - access object field 183 + /// - `[0]` - access array index 184 + /// - Combined: `embed.images[0].alt` 185 + /// 186 + /// # Example 187 + /// ```ignore 188 + /// let data: Data = ...; 189 + /// if let Some(alt_text) = data.get_at_path("embed.images[0].alt") { 190 + /// println!("Alt text: {}", alt_text.as_str().unwrap()); 191 + /// } 192 + /// ``` 193 + pub fn get_at_path(&'s self, path: &str) -> Option<&'s Data<'s>> { 194 + parse_and_traverse_path(self, path) 195 + } 196 + 197 + /// Query data with pattern matching 198 + /// 199 + /// Pattern syntax: 200 + /// - `field.nested` - exact path navigation 201 + /// - `[..]` - wildcard over collection (array elements or object values) 202 + /// - `field..nested` - scoped recursion (find nested within field, expect one) 203 + /// - `...field` - global recursion (find all occurrences anywhere) 204 + /// 205 + /// # Examples 206 + /// ```ignore 207 + /// // Exact path with wildcard 208 + /// let alts = data.query("embed.[..].alt"); 209 + /// 210 + /// // Scoped recursion 211 + /// let handle = data.query("post..handle"); // finds post.author.handle 212 + /// 213 + /// // Global recursion 214 + /// let all_cids = data.query("...cid"); // all CIDs anywhere 215 + /// ``` 216 + pub fn query(&'s self, pattern: &str) -> QueryResult<'s> { 217 + query_data(self, pattern) 218 + } 219 + 179 220 /// Parse a Data value from an IPLD value (CBOR) 180 221 pub fn from_cbor(cbor: &'s Ipld) -> Result<Self, AtDataError> { 181 222 Ok(match cbor { ··· 223 264 } 224 265 225 266 impl<'s> Array<'s> { 267 + /// Get the number of elements in the array 268 + pub fn len(&self) -> usize { 269 + self.0.len() 270 + } 271 + 272 + /// Check if the array is empty 273 + pub fn is_empty(&self) -> bool { 274 + self.0.is_empty() 275 + } 276 + 277 + /// Get an element by index 278 + pub fn get(&self, index: usize) -> Option<&Data<'s>> { 279 + self.0.get(index) 280 + } 281 + 282 + /// Get an iterator over the array elements 283 + pub fn iter(&self) -> std::slice::Iter<'_, Data<'s>> { 284 + self.0.iter() 285 + } 286 + 226 287 /// Parse an array from JSON values 227 288 pub fn from_json(json: &'s Vec<serde_json::Value>) -> Result<Self, AtDataError> { 228 289 let mut array = Vec::with_capacity(json.len()); ··· 241 302 } 242 303 } 243 304 305 + impl<'s> std::ops::Index<usize> for Array<'s> { 306 + type Output = Data<'s>; 307 + 308 + fn index(&self, index: usize) -> &Self::Output { 309 + &self.0[index] 310 + } 311 + } 312 + 244 313 /// Object/map of AT Protocol data values 245 314 #[derive(Debug, Clone, PartialEq, Eq)] 246 315 pub struct Object<'s>(pub BTreeMap<SmolStr, Data<'s>>); ··· 253 322 } 254 323 255 324 impl<'s> Object<'s> { 325 + /// Get a value by key 326 + pub fn get(&self, key: &str) -> Option<&Data<'s>> { 327 + self.0.get(key) 328 + } 329 + 330 + /// Check if a key exists in the object 331 + pub fn contains_key(&self, key: &str) -> bool { 332 + self.0.contains_key(key) 333 + } 334 + 335 + /// Get the number of key-value pairs in the object 336 + pub fn len(&self) -> usize { 337 + self.0.len() 338 + } 339 + 340 + /// Check if the object is empty 341 + pub fn is_empty(&self) -> bool { 342 + self.0.is_empty() 343 + } 344 + 345 + /// Get an iterator over the key-value pairs 346 + pub fn iter(&self) -> std::collections::btree_map::Iter<'_, SmolStr, Data<'s>> { 347 + self.0.iter() 348 + } 349 + 350 + /// Get an iterator over the keys 351 + pub fn keys(&self) -> std::collections::btree_map::Keys<'_, SmolStr, Data<'s>> { 352 + self.0.keys() 353 + } 354 + 355 + /// Get an iterator over the values 356 + pub fn values(&self) -> std::collections::btree_map::Values<'_, SmolStr, Data<'s>> { 357 + self.0.values() 358 + } 359 + 256 360 /// Parse an object from a JSON map with type inference 257 361 /// 258 362 /// Uses key names to infer the appropriate AT Protocol types for values. ··· 381 485 } 382 486 } 383 487 488 + impl<'s> std::ops::Index<&str> for Object<'s> { 489 + type Output = Data<'s>; 490 + 491 + fn index(&self, key: &str) -> &Self::Output { 492 + &self.0[key] 493 + } 494 + } 495 + 384 496 /// Level 1 deserialization of raw atproto data 385 497 /// 386 498 /// Maximally permissive with zero inference for cases where you just want to pass through the data ··· 467 579 serde_ipld_dagcbor::to_vec(self) 468 580 } 469 581 582 + /// Get a value at a path within nested RawData structures 583 + /// 584 + /// Path syntax: 585 + /// - `.field` or `field` - access object field 586 + /// - `[0]` - access array index 587 + /// - Combined: `embed.images[0].alt` 588 + /// 589 + /// # Example 590 + /// ```ignore 591 + /// let data: RawData = ...; 592 + /// if let Some(alt_text) = data.get_at_path("embed.images[0].alt") { 593 + /// println!("Alt text: {}", alt_text.as_str().unwrap()); 594 + /// } 595 + /// ``` 596 + pub fn get_at_path(&'d self, path: &str) -> Option<&'d RawData<'d>> { 597 + parse_and_traverse_raw_path(self, path) 598 + } 599 + 470 600 /// Convert a CBOR-encoded byte slice into a `RawData` value. 471 601 /// Parse a Data value from an IPLD value (CBOR) 472 602 pub fn from_cbor(cbor: &'d Ipld) -> Result<Self, AtDataError> { ··· 684 814 })?; 685 815 raw.try_into() 686 816 } 817 + 818 + /// Parse and traverse a path through nested Data structures 819 + fn parse_and_traverse_path<'s>(data: &'s Data<'s>, path: &str) -> Option<&'s Data<'s>> { 820 + let mut current = data; 821 + let mut path = path.trim_start_matches('.'); 822 + 823 + while !path.is_empty() { 824 + if path.starts_with('[') { 825 + // Array index: [N] 826 + let idx_end = path.find(']')?; 827 + let idx_str = &path[1..idx_end]; 828 + let idx: usize = idx_str.parse().ok()?; 829 + 830 + current = current.as_array()?.get(idx)?; 831 + path = &path[idx_end + 1..].trim_start_matches('.'); 832 + } else { 833 + // Field access: extract next segment (up to '.' or '[') 834 + let next_sep = path.find(&['.', '['][..]).unwrap_or(path.len()); 835 + let field = &path[..next_sep]; 836 + 837 + if field.is_empty() { 838 + break; 839 + } 840 + 841 + current = current.as_object()?.get(field)?; 842 + path = &path[next_sep..].trim_start_matches('.'); 843 + } 844 + } 845 + 846 + Some(current) 847 + } 848 + 849 + /// Parse and traverse a path through nested RawData structures 850 + fn parse_and_traverse_raw_path<'d>(data: &'d RawData<'d>, path: &str) -> Option<&'d RawData<'d>> { 851 + let mut current = data; 852 + let mut path = path.trim_start_matches('.'); 853 + 854 + while !path.is_empty() { 855 + if path.starts_with('[') { 856 + // Array index: [N] 857 + let idx_end = path.find(']')?; 858 + let idx_str = &path[1..idx_end]; 859 + let idx: usize = idx_str.parse().ok()?; 860 + 861 + current = current.as_array()?.get(idx)?; 862 + path = &path[idx_end + 1..].trim_start_matches('.'); 863 + } else { 864 + // Field access: extract next segment (up to '.' or '[') 865 + let next_sep = path.find(&['.', '['][..]).unwrap_or(path.len()); 866 + let field = &path[..next_sep]; 867 + 868 + if field.is_empty() { 869 + break; 870 + } 871 + 872 + current = current.as_object()?.get(field as &str)?; 873 + path = &path[next_sep..].trim_start_matches('.'); 874 + } 875 + } 876 + 877 + Some(current) 878 + } 879 + 880 + /// Result of a data query operation 881 + #[derive(Debug, Clone, PartialEq)] 882 + pub enum QueryResult<'s> { 883 + /// Single value expected and found 884 + Single(&'s Data<'s>), 885 + 886 + /// Multiple values from wildcard or global recursion 887 + Multiple(Vec<QueryMatch<'s>>), 888 + 889 + /// No matches found 890 + None, 891 + } 892 + 893 + impl<'s> QueryResult<'s> { 894 + /// Get single value if available 895 + pub fn single(&self) -> Option<&'s Data<'s>> { 896 + match self { 897 + QueryResult::Single(data) => Some(data), 898 + _ => None, 899 + } 900 + } 901 + 902 + /// Get multiple matches if available 903 + pub fn multiple(&self) -> Option<&[QueryMatch<'s>]> { 904 + match self { 905 + QueryResult::Multiple(matches) => Some(matches), 906 + _ => None, 907 + } 908 + } 909 + 910 + /// Get first value regardless of result type 911 + pub fn first(&self) -> Option<&'s Data<'s>> { 912 + match self { 913 + QueryResult::Single(data) => Some(data), 914 + QueryResult::Multiple(matches) => matches.first().and_then(|m| m.value), 915 + QueryResult::None => None, 916 + } 917 + } 918 + 919 + /// Check if any results were found 920 + pub fn is_empty(&self) -> bool { 921 + matches!(self, QueryResult::None) 922 + } 923 + 924 + /// Get all values as an iterator (flattens single/multiple) 925 + pub fn values(&self) -> impl Iterator<Item = &'s Data<'s>> { 926 + match self { 927 + QueryResult::Single(data) => vec![*data].into_iter(), 928 + QueryResult::Multiple(matches) => matches 929 + .iter() 930 + .filter_map(|m| m.value) 931 + .collect::<Vec<_>>() 932 + .into_iter(), 933 + QueryResult::None => vec![].into_iter(), 934 + } 935 + } 936 + } 937 + 938 + /// A single match from a query operation 939 + #[derive(Debug, Clone, PartialEq)] 940 + pub struct QueryMatch<'s> { 941 + /// Path where this value was found (e.g., "actors[0].handle") 942 + pub path: SmolStr, 943 + /// The value (None if field was missing during wildcard iteration) 944 + pub value: Option<&'s Data<'s>>, 945 + } 946 + 947 + /// Query pattern segment 948 + #[derive(Debug, Clone, PartialEq)] 949 + enum QuerySegment { 950 + /// Exact field name 951 + Field(SmolStr), 952 + /// Wildcard [..] 953 + Wildcard, 954 + /// Scoped recursion ..field 955 + ScopedRecursion(SmolStr), 956 + /// Global recursion ...field 957 + GlobalRecursion(SmolStr), 958 + } 959 + 960 + /// Parse a query pattern into segments 961 + fn parse_query_pattern(pattern: &str) -> Vec<QuerySegment> { 962 + let mut segments = Vec::new(); 963 + let mut remaining = pattern; 964 + 965 + // Skip single leading dot if present 966 + if remaining.starts_with('.') && !remaining.starts_with("..") { 967 + remaining = &remaining[1..]; 968 + } 969 + 970 + while !remaining.is_empty() { 971 + if remaining.starts_with("...") { 972 + // Global recursion 973 + let rest = &remaining[3..]; 974 + let end = rest.find(&['.', '['][..]).unwrap_or(rest.len()); 975 + let field = SmolStr::new(&rest[..end]); 976 + segments.push(QuerySegment::GlobalRecursion(field)); 977 + remaining = &rest[end..]; 978 + // Skip single dot separator 979 + if remaining.starts_with('.') && !remaining.starts_with("..") { 980 + remaining = &remaining[1..]; 981 + } 982 + } else if remaining.starts_with("..") { 983 + // Scoped recursion 984 + let rest = &remaining[2..]; 985 + let end = rest.find(&['.', '['][..]).unwrap_or(rest.len()); 986 + let field = SmolStr::new(&rest[..end]); 987 + segments.push(QuerySegment::ScopedRecursion(field)); 988 + remaining = &rest[end..]; 989 + // Skip single dot separator 990 + if remaining.starts_with('.') && !remaining.starts_with("..") { 991 + remaining = &remaining[1..]; 992 + } 993 + } else if remaining.starts_with("[..]") { 994 + // Wildcard 995 + segments.push(QuerySegment::Wildcard); 996 + remaining = &remaining[4..]; 997 + // Skip single dot separator 998 + if remaining.starts_with('.') && !remaining.starts_with("..") { 999 + remaining = &remaining[1..]; 1000 + } 1001 + } else { 1002 + // Regular field 1003 + let end = remaining.find(&['.', '['][..]).unwrap_or(remaining.len()); 1004 + let field = &remaining[..end]; 1005 + if !field.is_empty() { 1006 + segments.push(QuerySegment::Field(SmolStr::new(field))); 1007 + } 1008 + remaining = &remaining[end..]; 1009 + // Skip single dot separator 1010 + if remaining.starts_with('.') && !remaining.starts_with("..") { 1011 + remaining = &remaining[1..]; 1012 + } 1013 + } 1014 + } 1015 + 1016 + segments 1017 + } 1018 + 1019 + /// Execute a query on data 1020 + fn query_data<'s>(data: &'s Data<'s>, pattern: &str) -> QueryResult<'s> { 1021 + let segments = parse_query_pattern(pattern); 1022 + if segments.is_empty() { 1023 + return QueryResult::None; 1024 + } 1025 + 1026 + let mut results = vec![QueryMatch { 1027 + path: SmolStr::new_static(""), 1028 + value: Some(data), 1029 + }]; 1030 + 1031 + // Determine result type based on segment types before consuming segments 1032 + let has_wildcard = segments.iter().any(|s| matches!(s, QuerySegment::Wildcard)); 1033 + let has_global = segments.iter().any(|s| matches!(s, QuerySegment::GlobalRecursion(_))); 1034 + 1035 + for segment in segments { 1036 + results = execute_segment(&results, &segment); 1037 + if results.is_empty() { 1038 + return QueryResult::None; 1039 + } 1040 + } 1041 + 1042 + if has_wildcard || has_global || results.len() > 1 { 1043 + QueryResult::Multiple(results) 1044 + } else if results.len() == 1 { 1045 + if let Some(value) = results[0].value { 1046 + QueryResult::Single(value) 1047 + } else { 1048 + QueryResult::None 1049 + } 1050 + } else { 1051 + QueryResult::None 1052 + } 1053 + } 1054 + 1055 + /// Execute a single segment on current results 1056 + fn execute_segment<'s>(current: &[QueryMatch<'s>], segment: &QuerySegment) -> Vec<QueryMatch<'s>> { 1057 + let mut next = Vec::new(); 1058 + 1059 + for qm in current { 1060 + let Some(data) = qm.value else { continue }; 1061 + 1062 + match segment { 1063 + QuerySegment::Field(field) => { 1064 + if let Some(obj) = data.as_object() { 1065 + if let Some(value) = obj.get(field.as_str()) { 1066 + let new_path = append_path(&qm.path, field.as_str()); 1067 + next.push(QueryMatch { 1068 + path: new_path, 1069 + value: Some(value), 1070 + }); 1071 + } 1072 + } 1073 + } 1074 + 1075 + QuerySegment::Wildcard => match data { 1076 + Data::Array(arr) => { 1077 + for (idx, item) in arr.iter().enumerate() { 1078 + let new_path = append_path(&qm.path, &format!("[{}]", idx)); 1079 + next.push(QueryMatch { 1080 + path: new_path, 1081 + value: Some(item), 1082 + }); 1083 + } 1084 + } 1085 + Data::Object(obj) => { 1086 + for (key, value) in obj.iter() { 1087 + let new_path = append_path(&qm.path, key.as_str()); 1088 + next.push(QueryMatch { 1089 + path: new_path, 1090 + value: Some(value), 1091 + }); 1092 + } 1093 + } 1094 + _ => {} 1095 + }, 1096 + 1097 + QuerySegment::ScopedRecursion(field) => { 1098 + if let Some(found) = find_field_recursive(data, field.as_str(), &qm.path) { 1099 + next.push(found); 1100 + } 1101 + } 1102 + 1103 + QuerySegment::GlobalRecursion(field) => { 1104 + find_all_fields_recursive(data, field.as_str(), &qm.path, &mut next); 1105 + } 1106 + } 1107 + } 1108 + 1109 + next 1110 + } 1111 + 1112 + /// Recursively find first occurrence of a field (scoped recursion) 1113 + fn find_field_recursive<'s>( 1114 + data: &'s Data<'s>, 1115 + field: &str, 1116 + base_path: &SmolStr, 1117 + ) -> Option<QueryMatch<'s>> { 1118 + match data { 1119 + Data::Object(obj) => { 1120 + // Check direct children first 1121 + if let Some(value) = obj.get(field) { 1122 + let new_path = append_path(base_path, field); 1123 + return Some(QueryMatch { 1124 + path: new_path, 1125 + value: Some(value), 1126 + }); 1127 + } 1128 + 1129 + // Recurse into nested objects 1130 + for (key, value) in obj.iter() { 1131 + let new_path = append_path(base_path, key.as_str()); 1132 + if let Some(found) = find_field_recursive(value, field, &new_path) { 1133 + return Some(found); 1134 + } 1135 + } 1136 + } 1137 + Data::Array(arr) => { 1138 + for (idx, item) in arr.iter().enumerate() { 1139 + let new_path = append_path(base_path, &format!("[{}]", idx)); 1140 + if let Some(found) = find_field_recursive(item, field, &new_path) { 1141 + return Some(found); 1142 + } 1143 + } 1144 + } 1145 + _ => {} 1146 + } 1147 + 1148 + None 1149 + } 1150 + 1151 + /// Recursively find all occurrences of a field (global recursion) 1152 + fn find_all_fields_recursive<'s>( 1153 + data: &'s Data<'s>, 1154 + field: &str, 1155 + base_path: &SmolStr, 1156 + results: &mut Vec<QueryMatch<'s>>, 1157 + ) { 1158 + match data { 1159 + Data::Object(obj) => { 1160 + // Check direct children 1161 + if let Some(value) = obj.get(field) { 1162 + let new_path = append_path(base_path, field); 1163 + results.push(QueryMatch { 1164 + path: new_path, 1165 + value: Some(value), 1166 + }); 1167 + } 1168 + 1169 + // Recurse into all nested values 1170 + for (key, value) in obj.iter() { 1171 + let new_path = append_path(base_path, key.as_str()); 1172 + find_all_fields_recursive(value, field, &new_path, results); 1173 + } 1174 + } 1175 + Data::Array(arr) => { 1176 + for (idx, item) in arr.iter().enumerate() { 1177 + let new_path = append_path(base_path, &format!("[{}]", idx)); 1178 + find_all_fields_recursive(item, field, &new_path, results); 1179 + } 1180 + } 1181 + _ => {} 1182 + } 1183 + } 1184 + 1185 + /// Append a segment to a path 1186 + fn append_path(base: &SmolStr, segment: &str) -> SmolStr { 1187 + if base.is_empty() { 1188 + SmolStr::new(segment) 1189 + } else if segment.starts_with('[') { 1190 + SmolStr::new(format!("{}{}", base, segment)) 1191 + } else { 1192 + SmolStr::new(format!("{}.{}", base, segment)) 1193 + } 1194 + }
+352
crates/jacquard-common/src/types/value/tests.rs
··· 930 930 assert!(cbor_result.is_ok()); 931 931 assert!(!cbor_result.unwrap().is_empty()); 932 932 } 933 + 934 + #[test] 935 + fn test_object_methods() { 936 + let mut map = BTreeMap::new(); 937 + map.insert(SmolStr::new_static("num"), Data::Integer(42)); 938 + map.insert(SmolStr::new_static("text"), Data::String(AtprotoStr::String("hello".into()))); 939 + let obj = Object(map); 940 + 941 + // Test get 942 + assert!(obj.get("num").is_some()); 943 + assert_eq!(obj.get("num"), Some(&Data::Integer(42))); 944 + assert!(obj.get("missing").is_none()); 945 + 946 + // Test contains_key 947 + assert!(obj.contains_key("num")); 948 + assert!(!obj.contains_key("missing")); 949 + 950 + // Test len/is_empty 951 + assert_eq!(obj.len(), 2); 952 + assert!(!obj.is_empty()); 953 + 954 + let empty_obj = Object(BTreeMap::new()); 955 + assert_eq!(empty_obj.len(), 0); 956 + assert!(empty_obj.is_empty()); 957 + 958 + // Test indexing 959 + assert_eq!(&obj["num"], &Data::Integer(42)); 960 + 961 + // Test iterators 962 + assert_eq!(obj.keys().count(), 2); 963 + assert_eq!(obj.values().count(), 2); 964 + assert_eq!(obj.iter().count(), 2); 965 + } 966 + 967 + #[test] 968 + fn test_array_methods() { 969 + let arr = Array(vec![Data::Integer(1), Data::Integer(2), Data::Integer(3)]); 970 + 971 + // Test get 972 + assert_eq!(arr.get(0), Some(&Data::Integer(1))); 973 + assert_eq!(arr.get(2), Some(&Data::Integer(3))); 974 + assert!(arr.get(3).is_none()); 975 + 976 + // Test len/is_empty 977 + assert_eq!(arr.len(), 3); 978 + assert!(!arr.is_empty()); 979 + 980 + let empty_arr = Array(vec![]); 981 + assert_eq!(empty_arr.len(), 0); 982 + assert!(empty_arr.is_empty()); 983 + 984 + // Test indexing 985 + assert_eq!(&arr[1], &Data::Integer(2)); 986 + 987 + // Test iterator 988 + assert_eq!(arr.iter().count(), 3); 989 + } 990 + 991 + #[test] 992 + fn test_get_at_path_simple() { 993 + // Build nested structure: {"embed": {"alt": "test"}} 994 + let mut inner = BTreeMap::new(); 995 + inner.insert(SmolStr::new_static("alt"), Data::String(AtprotoStr::String("test".into()))); 996 + 997 + let mut outer = BTreeMap::new(); 998 + outer.insert(SmolStr::new_static("embed"), Data::Object(Object(inner))); 999 + 1000 + let data = Data::Object(Object(outer)); 1001 + 1002 + // Test simple field access 1003 + let result = data.get_at_path("embed.alt"); 1004 + assert!(result.is_some()); 1005 + assert_eq!(result.unwrap().as_str(), Some("test")); 1006 + 1007 + // Test with leading dot 1008 + let result2 = data.get_at_path(".embed.alt"); 1009 + assert!(result2.is_some()); 1010 + assert_eq!(result2.unwrap().as_str(), Some("test")); 1011 + 1012 + // Test missing path 1013 + assert!(data.get_at_path("missing.field").is_none()); 1014 + assert!(data.get_at_path("embed.missing").is_none()); 1015 + } 1016 + 1017 + #[test] 1018 + fn test_get_at_path_arrays() { 1019 + // Build: {"items": [{"name": "first"}, {"name": "second"}]} 1020 + let mut item1 = BTreeMap::new(); 1021 + item1.insert(SmolStr::new_static("name"), Data::String(AtprotoStr::String("first".into()))); 1022 + 1023 + let mut item2 = BTreeMap::new(); 1024 + item2.insert(SmolStr::new_static("name"), Data::String(AtprotoStr::String("second".into()))); 1025 + 1026 + let items = Data::Array(Array(vec![ 1027 + Data::Object(Object(item1)), 1028 + Data::Object(Object(item2)), 1029 + ])); 1030 + 1031 + let mut outer = BTreeMap::new(); 1032 + outer.insert(SmolStr::new_static("items"), items); 1033 + let data = Data::Object(Object(outer)); 1034 + 1035 + // Test array indexing 1036 + let result = data.get_at_path("items[0].name"); 1037 + assert!(result.is_some()); 1038 + assert_eq!(result.unwrap().as_str(), Some("first")); 1039 + 1040 + let result2 = data.get_at_path("items[1].name"); 1041 + assert!(result2.is_some()); 1042 + assert_eq!(result2.unwrap().as_str(), Some("second")); 1043 + 1044 + // Test out of bounds 1045 + assert!(data.get_at_path("items[2].name").is_none()); 1046 + } 1047 + 1048 + #[test] 1049 + fn test_get_at_path_complex() { 1050 + // Build: {"post": {"embed": {"images": [{"alt": "img1"}, {"alt": "img2"}]}}} 1051 + let mut img1 = BTreeMap::new(); 1052 + img1.insert(SmolStr::new_static("alt"), Data::String(AtprotoStr::String("img1".into()))); 1053 + 1054 + let mut img2 = BTreeMap::new(); 1055 + img2.insert(SmolStr::new_static("alt"), Data::String(AtprotoStr::String("img2".into()))); 1056 + 1057 + let images = Data::Array(Array(vec![ 1058 + Data::Object(Object(img1)), 1059 + Data::Object(Object(img2)), 1060 + ])); 1061 + 1062 + let mut embed_map = BTreeMap::new(); 1063 + embed_map.insert(SmolStr::new_static("images"), images); 1064 + 1065 + let mut post_map = BTreeMap::new(); 1066 + post_map.insert(SmolStr::new_static("embed"), Data::Object(Object(embed_map))); 1067 + 1068 + let mut root = BTreeMap::new(); 1069 + root.insert(SmolStr::new_static("post"), Data::Object(Object(post_map))); 1070 + 1071 + let data = Data::Object(Object(root)); 1072 + 1073 + // Test complex nested path 1074 + let result = data.get_at_path("post.embed.images[1].alt"); 1075 + assert!(result.is_some()); 1076 + assert_eq!(result.unwrap().as_str(), Some("img2")); 1077 + } 1078 + 1079 + #[test] 1080 + fn test_rawdata_get_at_path() { 1081 + // Build nested RawData structure 1082 + let mut inner = BTreeMap::new(); 1083 + inner.insert(SmolStr::new_static("value"), RawData::SignedInt(42)); 1084 + 1085 + let mut outer = BTreeMap::new(); 1086 + outer.insert(SmolStr::new_static("nested"), RawData::Object(inner)); 1087 + 1088 + let data = RawData::Object(outer); 1089 + 1090 + // Test simple field access 1091 + let result = data.get_at_path("nested.value"); 1092 + assert!(result.is_some()); 1093 + if let Some(RawData::SignedInt(n)) = result { 1094 + assert_eq!(*n, 42); 1095 + } else { 1096 + panic!("Expected SignedInt"); 1097 + } 1098 + } 1099 + 1100 + #[test] 1101 + fn test_query_exact_path() { 1102 + let mut inner = BTreeMap::new(); 1103 + inner.insert(SmolStr::new_static("handle"), Data::String(AtprotoStr::String("alice.bsky.social".into()))); 1104 + 1105 + let mut outer = BTreeMap::new(); 1106 + outer.insert(SmolStr::new_static("author"), Data::Object(Object(inner))); 1107 + 1108 + let data = Data::Object(Object(outer)); 1109 + 1110 + // Exact path should return Single 1111 + let result = data.query("author.handle"); 1112 + assert!(matches!(result, QueryResult::Single(_))); 1113 + assert_eq!(result.single().unwrap().as_str(), Some("alice.bsky.social")); 1114 + } 1115 + 1116 + #[test] 1117 + fn test_query_wildcard_array() { 1118 + // Build: {"actors": [{"handle": "alice"}, {"handle": "bob"}, {"name": "carol"}]} 1119 + let mut actor1 = BTreeMap::new(); 1120 + actor1.insert(SmolStr::new_static("handle"), Data::String(AtprotoStr::String("alice".into()))); 1121 + 1122 + let mut actor2 = BTreeMap::new(); 1123 + actor2.insert(SmolStr::new_static("handle"), Data::String(AtprotoStr::String("bob".into()))); 1124 + 1125 + let mut actor3 = BTreeMap::new(); 1126 + actor3.insert(SmolStr::new_static("name"), Data::String(AtprotoStr::String("carol".into()))); 1127 + 1128 + let actors = Data::Array(Array(vec![ 1129 + Data::Object(Object(actor1)), 1130 + Data::Object(Object(actor2)), 1131 + Data::Object(Object(actor3)), 1132 + ])); 1133 + 1134 + let mut root = BTreeMap::new(); 1135 + root.insert(SmolStr::new_static("actors"), actors); 1136 + let data = Data::Object(Object(root)); 1137 + 1138 + // Wildcard over array 1139 + let result = data.query("actors.[..]"); 1140 + assert!(matches!(result, QueryResult::Multiple(_))); 1141 + let matches = result.multiple().unwrap(); 1142 + assert_eq!(matches.len(), 3); 1143 + assert_eq!(matches[0].path.as_str(), "actors[0]"); 1144 + assert_eq!(matches[1].path.as_str(), "actors[1]"); 1145 + assert_eq!(matches[2].path.as_str(), "actors[2]"); 1146 + } 1147 + 1148 + #[test] 1149 + fn test_query_wildcard_object() { 1150 + // Build: {"embed": {"images": {...}, "video": {...}}} 1151 + let mut images = BTreeMap::new(); 1152 + images.insert(SmolStr::new_static("alt"), Data::String(AtprotoStr::String("img".into()))); 1153 + 1154 + let mut video = BTreeMap::new(); 1155 + video.insert(SmolStr::new_static("alt"), Data::String(AtprotoStr::String("vid".into()))); 1156 + 1157 + let mut embed = BTreeMap::new(); 1158 + embed.insert(SmolStr::new_static("images"), Data::Object(Object(images))); 1159 + embed.insert(SmolStr::new_static("video"), Data::Object(Object(video))); 1160 + 1161 + let mut root = BTreeMap::new(); 1162 + root.insert(SmolStr::new_static("embed"), Data::Object(Object(embed))); 1163 + let data = Data::Object(Object(root)); 1164 + 1165 + // Wildcard over object values 1166 + let result = data.query("embed.[..]"); 1167 + assert!(matches!(result, QueryResult::Multiple(_))); 1168 + let matches = result.multiple().unwrap(); 1169 + assert_eq!(matches.len(), 2); // images and video 1170 + } 1171 + 1172 + #[test] 1173 + fn test_query_scoped_recursion() { 1174 + // Build: {"post": {"author": {"profile": {"handle": "alice"}}}} 1175 + let mut handle_map = BTreeMap::new(); 1176 + handle_map.insert(SmolStr::new_static("handle"), Data::String(AtprotoStr::String("alice".into()))); 1177 + 1178 + let mut profile_map = BTreeMap::new(); 1179 + profile_map.insert(SmolStr::new_static("profile"), Data::Object(Object(handle_map))); 1180 + 1181 + let mut author_map = BTreeMap::new(); 1182 + author_map.insert(SmolStr::new_static("author"), Data::Object(Object(profile_map))); 1183 + 1184 + let mut post_map = BTreeMap::new(); 1185 + post_map.insert(SmolStr::new_static("post"), Data::Object(Object(author_map))); 1186 + 1187 + let data = Data::Object(Object(post_map)); 1188 + 1189 + // Scoped recursion: find handle within post 1190 + let result = data.query("post..handle"); 1191 + assert!(matches!(result, QueryResult::Single(_))); 1192 + assert_eq!(result.single().unwrap().as_str(), Some("alice")); 1193 + assert_eq!(result.first().unwrap().as_str(), Some("alice")); 1194 + } 1195 + 1196 + #[test] 1197 + fn test_query_global_recursion() { 1198 + // Build structure with multiple 'cid' fields at different depths 1199 + let mut inner1 = BTreeMap::new(); 1200 + inner1.insert(SmolStr::new_static("cid"), Data::String(AtprotoStr::String("cid1".into()))); 1201 + 1202 + let mut inner2 = BTreeMap::new(); 1203 + inner2.insert(SmolStr::new_static("cid"), Data::String(AtprotoStr::String("cid2".into()))); 1204 + 1205 + let mut middle = BTreeMap::new(); 1206 + middle.insert(SmolStr::new_static("post"), Data::Object(Object(inner1))); 1207 + middle.insert(SmolStr::new_static("reply"), Data::Object(Object(inner2))); 1208 + 1209 + let mut root = BTreeMap::new(); 1210 + root.insert(SmolStr::new_static("thread"), Data::Object(Object(middle))); 1211 + root.insert(SmolStr::new_static("cid"), Data::String(AtprotoStr::String("cid3".into()))); 1212 + 1213 + let data = Data::Object(Object(root)); 1214 + 1215 + // Global recursion: find all cids 1216 + let result = data.query("...cid"); 1217 + assert!(matches!(result, QueryResult::Multiple(_))); 1218 + let matches = result.multiple().unwrap(); 1219 + assert_eq!(matches.len(), 3); 1220 + 1221 + // Check values 1222 + let values: Vec<_> = result.values().map(|d| d.as_str().unwrap()).collect(); 1223 + assert!(values.contains(&"cid1")); 1224 + assert!(values.contains(&"cid2")); 1225 + assert!(values.contains(&"cid3")); 1226 + } 1227 + 1228 + #[test] 1229 + fn test_query_combined_wildcard_field() { 1230 + // Build: {"actors": [{"handle": "alice"}, {"handle": "bob"}]} 1231 + let mut actor1 = BTreeMap::new(); 1232 + actor1.insert(SmolStr::new_static("handle"), Data::String(AtprotoStr::String("alice".into()))); 1233 + 1234 + let mut actor2 = BTreeMap::new(); 1235 + actor2.insert(SmolStr::new_static("handle"), Data::String(AtprotoStr::String("bob".into()))); 1236 + 1237 + let actors = Data::Array(Array(vec![ 1238 + Data::Object(Object(actor1)), 1239 + Data::Object(Object(actor2)), 1240 + ])); 1241 + 1242 + let mut root = BTreeMap::new(); 1243 + root.insert(SmolStr::new_static("actors"), actors); 1244 + let data = Data::Object(Object(root)); 1245 + 1246 + // Wildcard + field: collect handle from each actor 1247 + let result = data.query("actors.[..].handle"); 1248 + assert!(matches!(result, QueryResult::Multiple(_))); 1249 + let matches = result.multiple().unwrap(); 1250 + assert_eq!(matches.len(), 2); 1251 + assert_eq!(matches[0].value.unwrap().as_str(), Some("alice")); 1252 + assert_eq!(matches[1].value.unwrap().as_str(), Some("bob")); 1253 + } 1254 + 1255 + #[test] 1256 + fn test_query_no_match() { 1257 + let mut map = BTreeMap::new(); 1258 + map.insert(SmolStr::new_static("foo"), Data::Integer(42)); 1259 + let data = Data::Object(Object(map)); 1260 + 1261 + // Field doesn't exist 1262 + let result = data.query("missing"); 1263 + assert!(matches!(result, QueryResult::None)); 1264 + assert!(result.is_empty()); 1265 + assert!(result.single().is_none()); 1266 + assert!(result.first().is_none()); 1267 + } 1268 + 1269 + #[test] 1270 + fn test_query_result_helpers() { 1271 + let mut map = BTreeMap::new(); 1272 + map.insert(SmolStr::new_static("value"), Data::Integer(42)); 1273 + let data = Data::Object(Object(map)); 1274 + 1275 + let result = data.query("value"); 1276 + 1277 + // Test helper methods 1278 + assert!(!result.is_empty()); 1279 + assert!(result.single().is_some()); 1280 + assert_eq!(result.first().unwrap().as_integer(), Some(42)); 1281 + 1282 + let values: Vec<_> = result.values().collect(); 1283 + assert_eq!(values.len(), 1); 1284 + }