Skip to content

Commit 4d2d236

Browse files
committed
add matches function
1 parent 2bb6366 commit 4d2d236

File tree

2 files changed

+92
-2
lines changed

2 files changed

+92
-2
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib.rs

+91-1
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ pub use json_patch::{
4545
};
4646
#[doc(no_inline)]
4747
use jsonptr::index::Index;
48-
use jsonptr::Token;
4948
pub use jsonptr::{
5049
Pointer,
5150
PointerBuf,
51+
Token,
5252
};
5353
use serde_json::{
5454
json,
@@ -63,6 +63,7 @@ pub mod prelude {
6363
copy_operation,
6464
escape,
6565
format_ptr,
66+
matches,
6667
move_operation,
6768
patch_ext,
6869
remove_operation,
@@ -79,6 +80,7 @@ pub mod prelude {
7980
RemoveOperation,
8081
ReplaceOperation,
8182
TestOperation,
83+
Token,
8284
};
8385
}
8486

@@ -118,6 +120,47 @@ pub fn escape(input: &str) -> String {
118120
Token::new(input).encoded().into()
119121
}
120122

123+
pub fn matches<'a>(path: &Pointer, value: &'a Value) -> Vec<(PointerBuf, &'a Value)> {
124+
let Some(idx) = path.as_str().find("/*") else {
125+
// Base case -- no stars;
126+
// If we can't resolve, there's no match to be found
127+
if let Ok(v) = path.resolve(value) {
128+
return vec![(path.to_buf(), v)];
129+
} else {
130+
return vec![];
131+
}
132+
};
133+
134+
// we checked the index above so unwrap is safe here
135+
let (head, cons) = path.split_at(idx).unwrap();
136+
let mut res = vec![];
137+
138+
// If we can't resolve the head, or it's not an array, no match found
139+
let Ok(head_val) = head.resolve(value) else {
140+
return vec![];
141+
};
142+
let Some(next_array_val) = head_val.as_array() else {
143+
return vec![];
144+
};
145+
146+
for (i, v) in next_array_val.iter().enumerate() {
147+
// /1 is a valid pointer so the unwrap below is fine
148+
let idx_str = format!("/{i}");
149+
let idx_path = PointerBuf::parse(&idx_str).unwrap();
150+
151+
// The cons pointer either looks like /* or /*/something, so we need to split_front
152+
// to get the array marker out, and either return the current path if there's nothing
153+
// else, or recurse and concatenate the subpath(s) to the head
154+
if let Some((_, c)) = cons.split_front() {
155+
let subpaths = matches(c, v);
156+
res.extend(subpaths.iter().map(|(p, v)| (head.concat(&idx_path.concat(p)), *v)));
157+
} else {
158+
res.push((head.concat(&idx_path), v));
159+
}
160+
}
161+
res
162+
}
163+
121164
pub fn patch_ext(obj: &mut Value, p: PatchOperation) -> Result<(), PatchError> {
122165
match p {
123166
PatchOperation::Add(op) => add_or_replace(obj, &op.path, &op.value, false)?,
@@ -217,9 +260,16 @@ fn patch_ext_helper<'a>(
217260
PatchMode::Skip => return Ok(vec![]),
218261
}
219262
}
263+
264+
// Head now points at what we believe is an array; if not, it's an error.
220265
let next_array_val =
221266
head.resolve_mut(value)?.as_array_mut().ok_or(PatchError::UnexpectedType(head.as_str().into()))?;
267+
268+
// Iterate over all the array values and recurse, returning all found values
222269
for v in next_array_val {
270+
// The cons pointer either looks like /* or /*/something, so we need to split_front
271+
// to get the array marker out, and either return the current value if there's nothing
272+
// else, or recurse and return all the found values
223273
if let Some((_, c)) = cons.split_front() {
224274
res.extend(patch_ext_helper(c, v, mode)?);
225275
} else {
@@ -248,6 +298,46 @@ mod tests {
248298
})
249299
}
250300

301+
#[rstest]
302+
fn test_matches_1(data: Value) {
303+
let path = format_ptr!("/foo");
304+
let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
305+
assert_eq!(m, vec![format_ptr!("/foo")]);
306+
}
307+
308+
#[rstest]
309+
fn test_matches_2(data: Value) {
310+
let path = format_ptr!("/foo/*/baz");
311+
let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
312+
assert_eq!(m, vec![format_ptr!("/foo/0/baz"), format_ptr!("/foo/1/baz"), format_ptr!("/foo/2/baz")]);
313+
}
314+
315+
#[rstest]
316+
fn test_matches_3(data: Value) {
317+
let path = format_ptr!("/foo/*");
318+
let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
319+
assert_eq!(m, vec![format_ptr!("/foo/0"), format_ptr!("/foo/1"), format_ptr!("/foo/2")]);
320+
}
321+
322+
#[rstest]
323+
#[case(format_ptr!("/foo/*/baz/fixx"))]
324+
#[case(format_ptr!("/foo/2/baz/fixx"))]
325+
fn test_matches_4(#[case] path: PointerBuf, data: Value) {
326+
let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
327+
assert_eq!(m, vec![format_ptr!("/foo/2/baz/fixx")]);
328+
}
329+
330+
#[rstest]
331+
#[case(format_ptr!("/*"))]
332+
#[case(format_ptr!("/food"))]
333+
#[case(format_ptr!("/foo/3/baz"))]
334+
#[case(format_ptr!("/foo/bar/baz"))]
335+
#[case(format_ptr!("/foo/0/baz/fixx"))]
336+
fn test_no_match(#[case] path: PointerBuf, data: Value) {
337+
let m = matches(&path, &data);
338+
assert_is_empty!(m);
339+
}
340+
251341
#[rstest]
252342
fn test_patch_ext_add(mut data: Value) {
253343
let path = format_ptr!("/foo/*/baz/buzz");

0 commit comments

Comments
 (0)