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;
48use jsonptr::Token;
49pub use jsonptr::{
50    Pointer,
51    PointerBuf,
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        move_operation,
67        patch_ext,
68        remove_operation,
69        replace_operation,
70        test_operation,
71        AddOperation,
72        CopyOperation,
73        MoveOperation,
74        Patch,
75        PatchError,
76        PatchOperation,
77        Pointer,
78        PointerBuf,
79        RemoveOperation,
80        ReplaceOperation,
81        TestOperation,
82    };
83}
84
85// PatchMode controls what to do if the referenced element does not exist in the object.
86#[derive(Debug, Clone, Copy)]
87enum PatchMode {
88    Error,
89    Create,
90    Skip,
91}
92
93pub fn add_operation(path: PointerBuf, value: Value) -> PatchOperation {
94    PatchOperation::Add(AddOperation { path, value })
95}
96
97pub fn copy_operation(from: PointerBuf, path: PointerBuf) -> PatchOperation {
98    PatchOperation::Copy(CopyOperation { from, path })
99}
100
101pub fn move_operation(from: PointerBuf, path: PointerBuf) -> PatchOperation {
102    PatchOperation::Move(MoveOperation { from, path })
103}
104
105pub fn remove_operation(path: PointerBuf) -> PatchOperation {
106    PatchOperation::Remove(RemoveOperation { path })
107}
108
109pub fn replace_operation(path: PointerBuf, value: Value) -> PatchOperation {
110    PatchOperation::Replace(ReplaceOperation { path, value })
111}
112
113pub fn test_operation(path: PointerBuf, value: Value) -> PatchOperation {
114    PatchOperation::Test(TestOperation { path, value })
115}
116
117pub fn escape(input: &str) -> String {
118    Token::new(input).encoded().into()
119}
120
121pub fn patch_ext(obj: &mut Value, p: PatchOperation) -> Result<(), PatchError> {
122    match p {
123        PatchOperation::Add(op) => add_or_replace(obj, &op.path, &op.value, false)?,
124        PatchOperation::Remove(op) => remove(obj, &op.path)?,
125        PatchOperation::Replace(op) => add_or_replace(obj, &op.path, &op.value, true)?,
126        x => patch(obj, &[x])?,
127    }
128    Ok(())
129}
130
131fn add_or_replace(obj: &mut Value, path: &PointerBuf, value: &Value, replace: bool) -> Result<(), PatchError> {
132    let Some((subpath, tail)) = path.split_back() else {
133        return Ok(());
134    };
135
136    // "replace" requires that the path you're replacing already exist, therefore we set
137    // create_if_not_exists = !replace.  We don't want to skip missing elements.
138    let mode = if replace { PatchMode::Error } else { PatchMode::Create };
139    for v in patch_ext_helper(subpath, obj, mode)? {
140        match v {
141            Value::Object(map) => {
142                let key = tail.decoded().into();
143                if replace && !map.contains_key(&key) {
144                    return Err(PatchError::TargetDoesNotExist(path.as_str().into()));
145                }
146                map.insert(key, value.clone());
147            },
148            Value::Array(vec) => match tail.to_index()? {
149                Index::Num(idx) => {
150                    vec.get(idx).ok_or(PatchError::OutOfBounds(idx))?;
151                    if replace {
152                        vec[idx] = value.clone();
153                    } else {
154                        vec.insert(idx, value.clone());
155                    }
156                },
157                Index::Next => {
158                    vec.push(value.clone());
159                },
160            },
161            _ => {
162                return Err(PatchError::UnexpectedType(path.as_str().into()));
163            },
164        }
165    }
166
167    Ok(())
168}
169
170fn remove(obj: &mut Value, path: &PointerBuf) -> Result<(), PatchError> {
171    let Some((subpath, key)) = path.split_back() else {
172        return Ok(());
173    };
174
175    for v in patch_ext_helper(subpath, obj, PatchMode::Skip)? {
176        v.as_object_mut()
177            .ok_or(PatchError::UnexpectedType(subpath.as_str().into()))?
178            .remove(key.decoded().as_ref());
179    }
180
181    Ok(())
182}
183
184// Given JSON pointer, recursively walk through all the possible "end" values that the path
185// references; return a mutable reference so we can make modifications at those points.
186fn patch_ext_helper<'a>(
187    path: &Pointer,
188    value: &'a mut Value,
189    mode: PatchMode,
190) -> Result<Vec<&'a mut Value>, PatchError> {
191    let Some(idx) = path.as_str().find("/*") else {
192        if path.resolve(value).is_err() {
193            match mode {
194                PatchMode::Error => return Err(PatchError::TargetDoesNotExist(path.as_str().into())),
195                PatchMode::Create => {
196                    path.assign(value, json!({}))?;
197                },
198                PatchMode::Skip => return Ok(vec![]),
199            }
200        }
201        return Ok(vec![path.resolve_mut(value)?]);
202    };
203
204    // we checked the index above so unwrap is safe here
205    let (head, cons) = path.split_at(idx).unwrap();
206    let mut res = vec![];
207
208    // This is a little weird; if mode == Create, and the subpath up to this point doesn't exist,
209    // we'll create an empty array which we won't iterate over at all.  I think that's
210    // "approximately" fine and less surprising that not creating anything.
211    if head.resolve(value).is_err() {
212        match mode {
213            PatchMode::Error => return Err(PatchError::TargetDoesNotExist(path.as_str().into())),
214            PatchMode::Create => {
215                path.assign(value, json!([]))?;
216            },
217            PatchMode::Skip => return Ok(vec![]),
218        }
219    }
220    let next_array_val =
221        head.resolve_mut(value)?.as_array_mut().ok_or(PatchError::UnexpectedType(head.as_str().into()))?;
222    for v in next_array_val {
223        if let Some((_, c)) = cons.split_front() {
224            res.extend(patch_ext_helper(c, v, mode)?);
225        } else {
226            res.push(v);
227        }
228    }
229    Ok(res)
230}
231
232#[cfg(test)]
233mod tests {
234    use assertables::*;
235    use rstest::*;
236
237    use super::*;
238    use crate as json_patch_ext; // make the macros work in the tests
239
240    #[fixture]
241    fn data() -> Value {
242        json!({
243            "foo": [
244                {"baz": {"buzz": 0}},
245                {"baz": {"quzz": 1}},
246                {"baz": {"fixx": 2}},
247            ],
248        })
249    }
250
251    #[rstest]
252    fn test_patch_ext_add(mut data: Value) {
253        let path = format_ptr!("/foo/*/baz/buzz");
254        let res = patch_ext(&mut data, add_operation(path, json!(42)));
255        assert_ok!(res);
256        assert_eq!(
257            data,
258            json!({
259                "foo": [
260                    {"baz": {"buzz": 42 }},
261                    {"baz": {"quzz": 1, "buzz": 42}},
262                    {"baz": {"fixx": 2, "buzz": 42}},
263                ],
264            })
265        );
266    }
267
268    #[rstest]
269    fn test_patch_ext_add_escaped() {
270        let path = format_ptr!("/foo/bar/{}", escape("testing.sh/baz"));
271        let mut data = json!({});
272        let res = patch_ext(&mut data, add_operation(path, json!(42)));
273        assert_ok!(res);
274        assert_eq!(data, json!({"foo": {"bar": {"testing.sh/baz": 42}}}));
275    }
276
277    #[rstest]
278    fn test_patch_ext_replace(mut data: Value) {
279        let path = format_ptr!("/foo/*/baz");
280        let res = patch_ext(&mut data, replace_operation(path, json!(42)));
281        assert_ok!(res);
282        assert_eq!(
283            data,
284            json!({
285                "foo": [
286                    {"baz": 42},
287                    {"baz": 42},
288                    {"baz": 42},
289                ],
290            })
291        );
292    }
293
294    #[rstest]
295    fn test_patch_ext_replace_err(mut data: Value) {
296        let path = format_ptr!("/foo/*/baz/buzz");
297        let res = patch_ext(&mut data, replace_operation(path, json!(42)));
298        println!("{data:?}");
299        assert_err!(res);
300    }
301
302
303    #[rstest]
304    fn test_patch_ext_remove(mut data: Value) {
305        let path = format_ptr!("/foo/*/baz/quzz");
306        let res = patch_ext(&mut data, remove_operation(path));
307        assert_ok!(res);
308        assert_eq!(
309            data,
310            json!({
311                "foo": [
312                    {"baz": {"buzz": 0}},
313                    {"baz": {}},
314                    {"baz": {"fixx": 2}},
315                ],
316            })
317        );
318    }
319}