json_patch_ext/
lib.rs

1//! This module provides some unofficial "extensions" to the [jsonpatch](https://jsonpatch.com)
2//! format for describing changes to a JSON document.  In particular, it adds the `*` operator as a
3//! valid token for arrays in a JSON document.  It means: apply this change to all elements of this
4//! array.  For example, consider the following document:
5//!
6//! ```json
7//! {
8//!   "foo": {
9//!     "bar": [
10//!       {"baz": 1},
11//!       {"baz": 2},
12//!       {"baz": 3},
13//!     ]
14//!   }
15//! }
16//! ```
17//!
18//! The pathspec `/foo/bar/*/baz` would reference the `baz` field of all three array entries in the
19//! `bar` array.  It is an error to use `*` to reference a field that is not an array.  It is an
20//! error to use `*` at the end of a path, e.g., `/foo/*`.
21//!
22//! Additionally, this crate will auto-create parent paths for the AddOperation only, e.g., the
23//! result of applying `AddOperation{ path: "/foo/bar", value: 1 }` to the empty document will be
24//!
25//! ```json
26//! { "foo": {"bar": 1}}
27//! ```
28
29mod errors;
30mod macros;
31
32use json_patch::patch;
33// mark these as re-exports in the generated docs (maybe related to
34// https://github.com/rust-lang/rust/issues/131180?)
35#[doc(no_inline)]
36pub use json_patch::{
37    AddOperation,
38    CopyOperation,
39    MoveOperation,
40    Patch,
41    PatchOperation,
42    RemoveOperation,
43    ReplaceOperation,
44    TestOperation,
45};
46#[doc(no_inline)]
47use jsonptr::index::Index;
48pub use jsonptr::{
49    Pointer,
50    PointerBuf,
51    Token,
52};
53use serde_json::{
54    json,
55    Value,
56};
57
58pub use crate::errors::PatchError;
59
60pub mod prelude {
61    pub use super::{
62        add_operation,
63        copy_operation,
64        escape,
65        format_ptr,
66        matches,
67        move_operation,
68        patch_ext,
69        remove_operation,
70        replace_operation,
71        test_operation,
72        AddOperation,
73        CopyOperation,
74        MoveOperation,
75        Patch,
76        PatchError,
77        PatchOperation,
78        Pointer,
79        PointerBuf,
80        RemoveOperation,
81        ReplaceOperation,
82        TestOperation,
83        Token,
84    };
85}
86
87// PatchMode controls what to do if the referenced element does not exist in the object.
88#[derive(Debug, Clone, Copy)]
89enum PatchMode {
90    Error,
91    Create,
92    Skip,
93}
94
95pub fn add_operation(path: PointerBuf, value: Value) -> PatchOperation {
96    PatchOperation::Add(AddOperation { path, value })
97}
98
99pub fn copy_operation(from: PointerBuf, path: PointerBuf) -> PatchOperation {
100    PatchOperation::Copy(CopyOperation { from, path })
101}
102
103pub fn move_operation(from: PointerBuf, path: PointerBuf) -> PatchOperation {
104    PatchOperation::Move(MoveOperation { from, path })
105}
106
107pub fn remove_operation(path: PointerBuf) -> PatchOperation {
108    PatchOperation::Remove(RemoveOperation { path })
109}
110
111pub fn replace_operation(path: PointerBuf, value: Value) -> PatchOperation {
112    PatchOperation::Replace(ReplaceOperation { path, value })
113}
114
115pub fn test_operation(path: PointerBuf, value: Value) -> PatchOperation {
116    PatchOperation::Test(TestOperation { path, value })
117}
118
119pub fn escape(input: &str) -> String {
120    Token::new(input).encoded().into()
121}
122
123pub 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            panic!("cons can't be root");
159        }
160    }
161    res
162}
163
164pub fn patch_ext(obj: &mut Value, p: PatchOperation) -> Result<(), PatchError> {
165    match p {
166        PatchOperation::Add(op) => add_or_replace(obj, &op.path, &op.value, false)?,
167        PatchOperation::Remove(op) => remove(obj, &op.path)?,
168        PatchOperation::Replace(op) => add_or_replace(obj, &op.path, &op.value, true)?,
169        x => patch(obj, &[x])?,
170    }
171    Ok(())
172}
173
174fn add_or_replace(obj: &mut Value, path: &PointerBuf, value: &Value, replace: bool) -> Result<(), PatchError> {
175    let Some((subpath, tail)) = path.split_back() else {
176        return Ok(());
177    };
178
179    // "replace" requires that the path you're replacing already exist, therefore we set
180    // create_if_not_exists = !replace.  We don't want to skip missing elements.
181    let mode = if replace { PatchMode::Error } else { PatchMode::Create };
182    for v in patch_ext_helper(subpath, obj, mode)? {
183        match v {
184            Value::Object(map) => {
185                let key = tail.decoded().into();
186                if replace && !map.contains_key(&key) {
187                    return Err(PatchError::TargetDoesNotExist(path.to_string()));
188                }
189                map.insert(key, value.clone());
190            },
191            Value::Array(vec) => match tail.to_index()? {
192                Index::Num(idx) => {
193                    vec.get(idx).ok_or(PatchError::OutOfBounds(idx))?;
194                    if replace {
195                        vec[idx] = value.clone();
196                    } else {
197                        vec.insert(idx, value.clone());
198                    }
199                },
200                Index::Next => {
201                    vec.push(value.clone());
202                },
203            },
204            _ => {
205                return Err(PatchError::UnexpectedType(path.to_string()));
206            },
207        }
208    }
209
210    Ok(())
211}
212
213fn remove(obj: &mut Value, path: &PointerBuf) -> Result<(), PatchError> {
214    let Some((subpath, key)) = path.split_back() else {
215        return Ok(());
216    };
217
218    for v in patch_ext_helper(subpath, obj, PatchMode::Skip)? {
219        match v {
220            Value::Object(map) => {
221                map.remove(key.decoded().as_ref());
222            },
223            Value::Array(vec) => {
224                if let Index::Num(idx) = key.to_index()? {
225                    vec.get(idx).ok_or(PatchError::OutOfBounds(idx))?;
226                    vec.remove(idx);
227                } else {
228                    return Err(PatchError::UnexpectedType(key.to_string()));
229                }
230            },
231            _ => {
232                return Err(PatchError::UnexpectedType(path.to_string()));
233            },
234        }
235    }
236
237    Ok(())
238}
239
240// Given JSON pointer, recursively walk through all the possible "end" values that the path
241// references; return a mutable reference so we can make modifications at those points.
242fn patch_ext_helper<'a>(
243    path: &Pointer,
244    value: &'a mut Value,
245    mode: PatchMode,
246) -> Result<Vec<&'a mut Value>, PatchError> {
247    let Some(idx) = path.as_str().find("/*") else {
248        if path.resolve(value).is_err() {
249            match mode {
250                PatchMode::Error => return Err(PatchError::TargetDoesNotExist(path.as_str().into())),
251                PatchMode::Create => {
252                    path.assign(value, json!({}))?;
253                },
254                PatchMode::Skip => return Ok(vec![]),
255            }
256        }
257        return Ok(vec![path.resolve_mut(value)?]);
258    };
259
260    // we checked the index above so unwrap is safe here
261    let (head, cons) = path.split_at(idx).unwrap();
262    let mut res = vec![];
263
264    // This is a little weird; if mode == Create, and the subpath up to this point doesn't exist,
265    // we'll create an empty array which we won't iterate over at all.  I think that's
266    // "approximately" fine and less surprising that not creating anything.
267    if head.resolve(value).is_err() {
268        match mode {
269            PatchMode::Error => return Err(PatchError::TargetDoesNotExist(path.as_str().into())),
270            PatchMode::Create => {
271                path.assign(value, json!([]))?;
272            },
273            PatchMode::Skip => return Ok(vec![]),
274        }
275    }
276
277    // Head now points at what we believe is an array; if not, it's an error.
278    let next_array_val =
279        head.resolve_mut(value)?.as_array_mut().ok_or(PatchError::UnexpectedType(head.as_str().into()))?;
280
281    // Iterate over all the array values and recurse, returning all found values
282    for v in next_array_val {
283        // The cons pointer either looks like /* or /*/something, so we need to split_front
284        // to get the array marker out, and either return the current value if there's nothing
285        // else, or recurse and return all the found values
286        if let Some((_, c)) = cons.split_front() {
287            res.extend(patch_ext_helper(c, v, mode)?);
288        } else {
289            panic!("cons can't be root");
290        }
291    }
292    Ok(res)
293}
294
295#[cfg(test)]
296mod tests {
297    use assertables::*;
298    use rstest::*;
299
300    use super::*;
301    use crate as json_patch_ext; // make the macros work in the tests
302
303    #[fixture]
304    fn data() -> Value {
305        json!({
306            "foo": [
307                {"baz": {"buzz": 0}},
308                {"baz": {"quzz": 1}},
309                {"baz": {"fixx": 2}},
310            ],
311        })
312    }
313
314    #[rstest]
315    fn test_matches_1(data: Value) {
316        let path = format_ptr!("/foo");
317        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
318        assert_eq!(m, vec![format_ptr!("/foo")]);
319    }
320
321    #[rstest]
322    fn test_matches_2(data: Value) {
323        let path = format_ptr!("/foo/*/baz");
324        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
325        assert_eq!(m, vec![format_ptr!("/foo/0/baz"), format_ptr!("/foo/1/baz"), format_ptr!("/foo/2/baz")]);
326    }
327
328    #[rstest]
329    fn test_matches_3(data: Value) {
330        let path = format_ptr!("/foo/*");
331        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
332        assert_eq!(m, vec![format_ptr!("/foo/0"), format_ptr!("/foo/1"), format_ptr!("/foo/2")]);
333    }
334
335    #[rstest]
336    #[case(format_ptr!("/foo/*/baz/fixx"))]
337    #[case(format_ptr!("/foo/2/baz/fixx"))]
338    fn test_matches_4(#[case] path: PointerBuf, data: Value) {
339        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
340        assert_eq!(m, vec![format_ptr!("/foo/2/baz/fixx")]);
341    }
342
343    #[rstest]
344    fn test_matches_root() {
345        let path = format_ptr!("/*");
346        let data = json!(["foo", "bar"]);
347        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
348        assert_eq!(m, vec![format_ptr!("/0"), format_ptr!("/1")]);
349    }
350
351    #[rstest]
352    #[case(format_ptr!("/*"))]
353    #[case(format_ptr!("/food"))]
354    #[case(format_ptr!("/foo/3/baz"))]
355    #[case(format_ptr!("/foo/bar/baz"))]
356    #[case(format_ptr!("/foo/0/baz/fixx"))]
357    fn test_no_match(#[case] path: PointerBuf, data: Value) {
358        let m = matches(&path, &data);
359        assert_is_empty!(m);
360    }
361
362    #[rstest]
363    fn test_patch_ext_add(mut data: Value) {
364        let path = format_ptr!("/foo/*/baz/buzz");
365        let res = patch_ext(&mut data, add_operation(path, json!(42)));
366        assert_ok!(res);
367        assert_eq!(
368            data,
369            json!({
370                "foo": [
371                    {"baz": {"buzz": 42 }},
372                    {"baz": {"quzz": 1, "buzz": 42}},
373                    {"baz": {"fixx": 2, "buzz": 42}},
374                ],
375            })
376        );
377    }
378
379    #[rstest]
380    fn test_patch_ext_add_vec1(mut data: Value) {
381        let path = format_ptr!("/foo/1");
382        let res = patch_ext(&mut data, add_operation(path, json!(42)));
383        assert_ok!(res);
384        assert_eq!(
385            data,
386            json!({
387                "foo": [
388                    {"baz": {"buzz": 0}},
389                    42,
390                    {"baz": {"quzz": 1}},
391                    {"baz": {"fixx": 2}},
392                ],
393            })
394        );
395    }
396
397    #[rstest]
398    fn test_patch_ext_add_vec2(mut data: Value) {
399        let path = format_ptr!("/foo/-");
400        let res = patch_ext(&mut data, add_operation(path, json!(42)));
401        assert_ok!(res);
402        assert_eq!(
403            data,
404            json!({
405                "foo": [
406                    {"baz": {"buzz": 0}},
407                    {"baz": {"quzz": 1}},
408                    {"baz": {"fixx": 2}},
409                    42,
410                ],
411            })
412        );
413    }
414
415    #[rstest]
416    fn test_patch_ext_add_vec_err(mut data: Value) {
417        let path = format_ptr!("/foo/a");
418        let res = patch_ext(&mut data, add_operation(path, json!(42)));
419        assert_err!(res);
420    }
421
422    #[rstest]
423    fn test_patch_ext_add_escaped() {
424        let path = format_ptr!("/foo/bar/{}", escape("testing.sh/baz"));
425        let mut data = json!({});
426        let res = patch_ext(&mut data, add_operation(path, json!(42)));
427        assert_ok!(res);
428        assert_eq!(data, json!({"foo": {"bar": {"testing.sh/baz": 42}}}));
429    }
430
431    #[rstest]
432    fn test_patch_ext_replace(mut data: Value) {
433        let path = format_ptr!("/foo/*/baz");
434        let res = patch_ext(&mut data, replace_operation(path, json!(42)));
435        assert_ok!(res);
436        assert_eq!(
437            data,
438            json!({
439                "foo": [
440                    {"baz": 42},
441                    {"baz": 42},
442                    {"baz": 42},
443                ],
444            })
445        );
446    }
447
448    #[rstest]
449    fn test_patch_ext_replace_vec1(mut data: Value) {
450        let path = format_ptr!("/foo/1");
451        let res = patch_ext(&mut data, replace_operation(path, json!(42)));
452        assert_ok!(res);
453        assert_eq!(
454            data,
455            json!({
456                "foo": [
457                    {"baz": {"buzz": 0}},
458                    42,
459                    {"baz": {"fixx": 2}},
460                ],
461            })
462        );
463    }
464
465    #[rstest]
466    fn test_patch_ext_replace_vec2(mut data: Value) {
467        let path = format_ptr!("/foo/-");
468        let res = patch_ext(&mut data, replace_operation(path, json!(42)));
469        assert_ok!(res);
470        assert_eq!(
471            data,
472            json!({
473                "foo": [
474                    {"baz": {"buzz": 0}},
475                    {"baz": {"quzz": 1}},
476                    {"baz": {"fixx": 2}},
477                    42,
478                ],
479            })
480        );
481    }
482
483    #[rstest]
484    fn test_patch_ext_replace_err(mut data: Value) {
485        let path = format_ptr!("/foo/*/baz/buzz");
486        let res = patch_ext(&mut data, replace_operation(path, json!(42)));
487        assert_err!(res);
488    }
489
490    #[rstest]
491    fn test_patch_ext_remove(mut data: Value) {
492        let path = format_ptr!("/foo/*/baz/quzz");
493        let res = patch_ext(&mut data, remove_operation(path));
494        assert_ok!(res);
495        assert_eq!(
496            data,
497            json!({
498                "foo": [
499                    {"baz": {"buzz": 0}},
500                    {"baz": {}},
501                    {"baz": {"fixx": 2}},
502                ],
503            })
504        );
505    }
506
507    #[rstest]
508    fn test_patch_ext_remove_vec(mut data: Value) {
509        let path = format_ptr!("/foo/1");
510        let res = patch_ext(&mut data, remove_operation(path));
511        assert_ok!(res);
512        assert_eq!(
513            data,
514            json!({
515                "foo": [
516                    {"baz": {"buzz": 0}},
517                    {"baz": {"fixx": 2}},
518                ],
519            })
520        );
521    }
522
523    #[rstest]
524    fn test_patch_ext_remove_vec_err(mut data: Value) {
525        let path = format_ptr!("/foo/-");
526        let res = patch_ext(&mut data, remove_operation(path));
527        assert_err!(res);
528    }
529}