+508
crates/jacquard-common/src/types/value.rs
+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
+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
+
}