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.as_str().into()));
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.as_str().into()));
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        v.as_object_mut()
220            .ok_or(PatchError::UnexpectedType(subpath.as_str().into()))?
221            .remove(key.decoded().as_ref());
222    }
223
224    Ok(())
225}
226
227// Given JSON pointer, recursively walk through all the possible "end" values that the path
228// references; return a mutable reference so we can make modifications at those points.
229fn patch_ext_helper<'a>(
230    path: &Pointer,
231    value: &'a mut Value,
232    mode: PatchMode,
233) -> Result<Vec<&'a mut Value>, PatchError> {
234    let Some(idx) = path.as_str().find("/*") else {
235        if path.resolve(value).is_err() {
236            match mode {
237                PatchMode::Error => return Err(PatchError::TargetDoesNotExist(path.as_str().into())),
238                PatchMode::Create => {
239                    path.assign(value, json!({}))?;
240                },
241                PatchMode::Skip => return Ok(vec![]),
242            }
243        }
244        return Ok(vec![path.resolve_mut(value)?]);
245    };
246
247    // we checked the index above so unwrap is safe here
248    let (head, cons) = path.split_at(idx).unwrap();
249    let mut res = vec![];
250
251    // This is a little weird; if mode == Create, and the subpath up to this point doesn't exist,
252    // we'll create an empty array which we won't iterate over at all.  I think that's
253    // "approximately" fine and less surprising that not creating anything.
254    if head.resolve(value).is_err() {
255        match mode {
256            PatchMode::Error => return Err(PatchError::TargetDoesNotExist(path.as_str().into())),
257            PatchMode::Create => {
258                path.assign(value, json!([]))?;
259            },
260            PatchMode::Skip => return Ok(vec![]),
261        }
262    }
263
264    // Head now points at what we believe is an array; if not, it's an error.
265    let next_array_val =
266        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
269    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
273        if let Some((_, c)) = cons.split_front() {
274            res.extend(patch_ext_helper(c, v, mode)?);
275        } else {
276            panic!("cons can't be root");
277        }
278    }
279    Ok(res)
280}
281
282#[cfg(test)]
283mod tests {
284    use assertables::*;
285    use rstest::*;
286
287    use super::*;
288    use crate as json_patch_ext; // make the macros work in the tests
289
290    #[fixture]
291    fn data() -> Value {
292        json!({
293            "foo": [
294                {"baz": {"buzz": 0}},
295                {"baz": {"quzz": 1}},
296                {"baz": {"fixx": 2}},
297            ],
298        })
299    }
300
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    fn test_matches_root() {
332        let path = format_ptr!("/*");
333        let data = json!(["foo", "bar"]);
334        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
335        assert_eq!(m, vec![format_ptr!("/0"), format_ptr!("/1")]);
336    }
337
338    #[rstest]
339    #[case(format_ptr!("/*"))]
340    #[case(format_ptr!("/food"))]
341    #[case(format_ptr!("/foo/3/baz"))]
342    #[case(format_ptr!("/foo/bar/baz"))]
343    #[case(format_ptr!("/foo/0/baz/fixx"))]
344    fn test_no_match(#[case] path: PointerBuf, data: Value) {
345        let m = matches(&path, &data);
346        assert_is_empty!(m);
347    }
348
349    #[rstest]
350    fn test_patch_ext_add(mut data: Value) {
351        let path = format_ptr!("/foo/*/baz/buzz");
352        let res = patch_ext(&mut data, add_operation(path, json!(42)));
353        assert_ok!(res);
354        assert_eq!(
355            data,
356            json!({
357                "foo": [
358                    {"baz": {"buzz": 42 }},
359                    {"baz": {"quzz": 1, "buzz": 42}},
360                    {"baz": {"fixx": 2, "buzz": 42}},
361                ],
362            })
363        );
364    }
365
366    #[rstest]
367    fn test_patch_ext_add_escaped() {
368        let path = format_ptr!("/foo/bar/{}", escape("testing.sh/baz"));
369        let mut data = json!({});
370        let res = patch_ext(&mut data, add_operation(path, json!(42)));
371        assert_ok!(res);
372        assert_eq!(data, json!({"foo": {"bar": {"testing.sh/baz": 42}}}));
373    }
374
375    #[rstest]
376    fn test_patch_ext_replace(mut data: Value) {
377        let path = format_ptr!("/foo/*/baz");
378        let res = patch_ext(&mut data, replace_operation(path, json!(42)));
379        assert_ok!(res);
380        assert_eq!(
381            data,
382            json!({
383                "foo": [
384                    {"baz": 42},
385                    {"baz": 42},
386                    {"baz": 42},
387                ],
388            })
389        );
390    }
391
392    #[rstest]
393    fn test_patch_ext_replace_err(mut data: Value) {
394        let path = format_ptr!("/foo/*/baz/buzz");
395        let res = patch_ext(&mut data, replace_operation(path, json!(42)));
396        println!("{data:?}");
397        assert_err!(res);
398    }
399
400    #[rstest]
401    fn test_patch_ext_remove(mut data: Value) {
402        let path = format_ptr!("/foo/*/baz/quzz");
403        let res = patch_ext(&mut data, remove_operation(path));
404        assert_ok!(res);
405        assert_eq!(
406            data,
407            json!({
408                "foo": [
409                    {"baz": {"buzz": 0}},
410                    {"baz": {}},
411                    {"baz": {"fixx": 2}},
412                ],
413            })
414        );
415    }
416}