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    println!("{cons}");
147    for (i, v) in next_array_val.iter().enumerate() {
148        // /1 is a valid pointer so the unwrap below is fine
149        let idx_str = format!("/{i}");
150        let idx_path = PointerBuf::parse(&idx_str).unwrap();
151
152        // The cons pointer either looks like /* or /*/something, so we need to split_front
153        // to get the array marker out, and either return the current path if there's nothing
154        // else, or recurse and concatenate the subpath(s) to the head
155        if let Some((_, c)) = cons.split_front() {
156            let subpaths = matches(c, v);
157            res.extend(subpaths.iter().map(|(p, v)| (head.concat(&idx_path.concat(p)), *v)));
158        } else {
159            panic!("cons can't be root");
160        }
161    }
162    res
163}
164
165pub fn patch_ext(obj: &mut Value, p: PatchOperation) -> Result<(), PatchError> {
166    match p {
167        PatchOperation::Add(op) => add_or_replace(obj, &op.path, &op.value, false)?,
168        PatchOperation::Remove(op) => remove(obj, &op.path)?,
169        PatchOperation::Replace(op) => add_or_replace(obj, &op.path, &op.value, true)?,
170        x => patch(obj, &[x])?,
171    }
172    Ok(())
173}
174
175fn add_or_replace(obj: &mut Value, path: &PointerBuf, value: &Value, replace: bool) -> Result<(), PatchError> {
176    let Some((subpath, tail)) = path.split_back() else {
177        return Ok(());
178    };
179
180    // "replace" requires that the path you're replacing already exist, therefore we set
181    // create_if_not_exists = !replace.  We don't want to skip missing elements.
182    let mode = if replace { PatchMode::Error } else { PatchMode::Create };
183    for v in patch_ext_helper(subpath, obj, mode)? {
184        match v {
185            Value::Object(map) => {
186                let key = tail.decoded().into();
187                if replace && !map.contains_key(&key) {
188                    return Err(PatchError::TargetDoesNotExist(path.as_str().into()));
189                }
190                map.insert(key, value.clone());
191            },
192            Value::Array(vec) => match tail.to_index()? {
193                Index::Num(idx) => {
194                    vec.get(idx).ok_or(PatchError::OutOfBounds(idx))?;
195                    if replace {
196                        vec[idx] = value.clone();
197                    } else {
198                        vec.insert(idx, value.clone());
199                    }
200                },
201                Index::Next => {
202                    vec.push(value.clone());
203                },
204            },
205            _ => {
206                return Err(PatchError::UnexpectedType(path.as_str().into()));
207            },
208        }
209    }
210
211    Ok(())
212}
213
214fn remove(obj: &mut Value, path: &PointerBuf) -> Result<(), PatchError> {
215    let Some((subpath, key)) = path.split_back() else {
216        return Ok(());
217    };
218
219    for v in patch_ext_helper(subpath, obj, PatchMode::Skip)? {
220        v.as_object_mut()
221            .ok_or(PatchError::UnexpectedType(subpath.as_str().into()))?
222            .remove(key.decoded().as_ref());
223    }
224
225    Ok(())
226}
227
228// Given JSON pointer, recursively walk through all the possible "end" values that the path
229// references; return a mutable reference so we can make modifications at those points.
230fn patch_ext_helper<'a>(
231    path: &Pointer,
232    value: &'a mut Value,
233    mode: PatchMode,
234) -> Result<Vec<&'a mut Value>, PatchError> {
235    let Some(idx) = path.as_str().find("/*") else {
236        if path.resolve(value).is_err() {
237            match mode {
238                PatchMode::Error => return Err(PatchError::TargetDoesNotExist(path.as_str().into())),
239                PatchMode::Create => {
240                    path.assign(value, json!({}))?;
241                },
242                PatchMode::Skip => return Ok(vec![]),
243            }
244        }
245        return Ok(vec![path.resolve_mut(value)?]);
246    };
247
248    // we checked the index above so unwrap is safe here
249    let (head, cons) = path.split_at(idx).unwrap();
250    let mut res = vec![];
251
252    // This is a little weird; if mode == Create, and the subpath up to this point doesn't exist,
253    // we'll create an empty array which we won't iterate over at all.  I think that's
254    // "approximately" fine and less surprising that not creating anything.
255    if head.resolve(value).is_err() {
256        match mode {
257            PatchMode::Error => return Err(PatchError::TargetDoesNotExist(path.as_str().into())),
258            PatchMode::Create => {
259                path.assign(value, json!([]))?;
260            },
261            PatchMode::Skip => return Ok(vec![]),
262        }
263    }
264
265    // Head now points at what we believe is an array; if not, it's an error.
266    let next_array_val =
267        head.resolve_mut(value)?.as_array_mut().ok_or(PatchError::UnexpectedType(head.as_str().into()))?;
268
269    // Iterate over all the array values and recurse, returning all found values
270    for v in next_array_val {
271        // The cons pointer either looks like /* or /*/something, so we need to split_front
272        // to get the array marker out, and either return the current value if there's nothing
273        // else, or recurse and return all the found values
274        if let Some((_, c)) = cons.split_front() {
275            res.extend(patch_ext_helper(c, v, mode)?);
276        } else {
277            panic!("cons can't be root");
278        }
279    }
280    Ok(res)
281}
282
283#[cfg(test)]
284mod tests {
285    use assertables::*;
286    use rstest::*;
287
288    use super::*;
289    use crate as json_patch_ext; // make the macros work in the tests
290
291    #[fixture]
292    fn data() -> Value {
293        json!({
294            "foo": [
295                {"baz": {"buzz": 0}},
296                {"baz": {"quzz": 1}},
297                {"baz": {"fixx": 2}},
298            ],
299        })
300    }
301
302    #[rstest]
303    fn test_matches_1(data: Value) {
304        let path = format_ptr!("/foo");
305        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
306        assert_eq!(m, vec![format_ptr!("/foo")]);
307    }
308
309    #[rstest]
310    fn test_matches_2(data: Value) {
311        let path = format_ptr!("/foo/*/baz");
312        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
313        assert_eq!(m, vec![format_ptr!("/foo/0/baz"), format_ptr!("/foo/1/baz"), format_ptr!("/foo/2/baz")]);
314    }
315
316    #[rstest]
317    fn test_matches_3(data: Value) {
318        let path = format_ptr!("/foo/*");
319        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
320        assert_eq!(m, vec![format_ptr!("/foo/0"), format_ptr!("/foo/1"), format_ptr!("/foo/2")]);
321    }
322
323    #[rstest]
324    #[case(format_ptr!("/foo/*/baz/fixx"))]
325    #[case(format_ptr!("/foo/2/baz/fixx"))]
326    fn test_matches_4(#[case] path: PointerBuf, data: Value) {
327        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
328        assert_eq!(m, vec![format_ptr!("/foo/2/baz/fixx")]);
329    }
330
331    #[rstest]
332    fn test_matches_root() {
333        let path = format_ptr!("/*");
334        let data = json!(["foo", "bar"]);
335        let m: Vec<_> = matches(&path, &data).iter().map(|(p, _)| p.clone()).collect();
336        assert_eq!(m, vec![format_ptr!("/0"), format_ptr!("/1")]);
337    }
338
339    #[rstest]
340    #[case(format_ptr!("/*"))]
341    #[case(format_ptr!("/food"))]
342    #[case(format_ptr!("/foo/3/baz"))]
343    #[case(format_ptr!("/foo/bar/baz"))]
344    #[case(format_ptr!("/foo/0/baz/fixx"))]
345    fn test_no_match(#[case] path: PointerBuf, data: Value) {
346        let m = matches(&path, &data);
347        assert_is_empty!(m);
348    }
349
350    #[rstest]
351    fn test_patch_ext_add(mut data: Value) {
352        let path = format_ptr!("/foo/*/baz/buzz");
353        let res = patch_ext(&mut data, add_operation(path, json!(42)));
354        assert_ok!(res);
355        assert_eq!(
356            data,
357            json!({
358                "foo": [
359                    {"baz": {"buzz": 42 }},
360                    {"baz": {"quzz": 1, "buzz": 42}},
361                    {"baz": {"fixx": 2, "buzz": 42}},
362                ],
363            })
364        );
365    }
366
367    #[rstest]
368    fn test_patch_ext_add_escaped() {
369        let path = format_ptr!("/foo/bar/{}", escape("testing.sh/baz"));
370        let mut data = json!({});
371        let res = patch_ext(&mut data, add_operation(path, json!(42)));
372        assert_ok!(res);
373        assert_eq!(data, json!({"foo": {"bar": {"testing.sh/baz": 42}}}));
374    }
375
376    #[rstest]
377    fn test_patch_ext_replace(mut data: Value) {
378        let path = format_ptr!("/foo/*/baz");
379        let res = patch_ext(&mut data, replace_operation(path, json!(42)));
380        assert_ok!(res);
381        assert_eq!(
382            data,
383            json!({
384                "foo": [
385                    {"baz": 42},
386                    {"baz": 42},
387                    {"baz": 42},
388                ],
389            })
390        );
391    }
392
393    #[rstest]
394    fn test_patch_ext_replace_err(mut data: Value) {
395        let path = format_ptr!("/foo/*/baz/buzz");
396        let res = patch_ext(&mut data, replace_operation(path, json!(42)));
397        println!("{data:?}");
398        assert_err!(res);
399    }
400
401    #[rstest]
402    fn test_patch_ext_remove(mut data: Value) {
403        let path = format_ptr!("/foo/*/baz/quzz");
404        let res = patch_ext(&mut data, remove_operation(path));
405        assert_ok!(res);
406        assert_eq!(
407            data,
408            json!({
409                "foo": [
410                    {"baz": {"buzz": 0}},
411                    {"baz": {}},
412                    {"baz": {"fixx": 2}},
413                ],
414            })
415        );
416    }
417}