diff --git a/README.md b/README.md index a02907c..a4b63a4 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ First, get the latest version of the library using the following command: $ go get github.com/wI2L/jsondiff@latest ``` -:warning: Requires Go1.14+, due to the usage of the package [`hash/maphash`](https://golang.org/pkg/hash/maphash/). +:warning: Requires Go1.18+, due to the usage of the package [`hash/maphash`](https://golang.org/pkg/hash/maphash/), and the `any` keyword (predeclared type alias for the empty interface). ### Example use cases @@ -272,6 +272,8 @@ The patch is similar to the following: As you can see, the `remove` and `replace` operations are preceded with a `test` operation which assert/verify the `value` of the previous `path`. On the other hand, the `add` operation can be reverted to a remove operation directly and doesn't need to be preceded by a `test`. +[Run this example](https://pkg.go.dev/github.com/wI2L/jsondiff#example-Invertible). + Finally, as a side example, if we were to use the `Rationalize()` option in the context of the previous example, the output would be shorter, but the generated patch would still remain invertible: ```json @@ -303,6 +305,56 @@ The root arrays of each document are not equal because the values differ at each For such situations, you can use the `Equivalent()` option to instruct the diff generator to skip the generation of operations that would otherwise be added to the patch to represent the differences between the two arrays. +#### Ignores + +:construction: *This option is experimental and might be revised in the future.* +
+
+ +The `Ignores()` option allows to exclude one or more JSON fields/values from the *generated diff*. The fields must be identified using the JSON Pointer (RFC6901) string syntax. + +The option accepts a variadic list of JSON Pointers, which all individually represent a value in the source document. However, if the value does not exist in the source document, the value will be considered to be in the target document, which allows to *ignore* `add` operations. + +For example, let's generate the diff between those two JSON documents: + +```json +{ + "A": "bar", + "B": "baz", + "C": "foo" +} +``` + +```json +{ + "A": "rab", + "B": "baz", + "D": "foo" +} +``` + +Without the `Ignores()` option, the output patch is the following: + +```json +[ + { "op": "replace", "path": "/A", "value": "rab" }, + { "op": "remove", "path": "/C" }, + { "op": "add", "path": "/D", "value": "foo" } +] +``` + +Using the option with the following pointers list, we can ignore some of the fields that were updated, added or removed: + +```go +jsondiff.Ignores("/A", "/B", "/C") +``` + +The resulting patch is empty, because all changes and ignored. + +[Run this example](https://pkg.go.dev/github.com/wI2L/jsondiff#example-Ignores). + +> See the actual [testcases](testdata/tests/options/ignore.json) for more examples. + ## Benchmarks Performance is not the primary target of the package, instead it strives for correctness. A simple benchmark that compare the performance of available options is provided to give a rough estimate of the cost of each option. You can find the JSON documents used by this benchmark in the directory [testdata/benchs](testdata/benchs). diff --git a/differ.go b/differ.go index 9747eec..3f18882 100644 --- a/differ.go +++ b/differ.go @@ -15,11 +15,17 @@ type Differ struct { opts options } +type jsonNode struct { + ptr pointer + val interface{} +} + type options struct { factorize bool rationalize bool invertible bool equivalent bool + ignoredPtrs map[pointer]struct{} } // Patch returns the list of JSON patch operations @@ -62,6 +68,9 @@ func (d *Differ) diff(ptr pointer, src, tgt interface{}) { if src == nil && tgt == nil { return } + if _, ok := d.opts.ignoredPtrs[ptr]; ok { + return + } if !areComparable(src, tgt) { if ptr.isRoot() { // If incomparable values are located at the root @@ -196,9 +205,15 @@ func (d *Differ) compareObjects(ptr pointer, src, tgt map[string]interface{}) { case inOld && inNew: d.diff(ptr.appendKey(k), src[k], tgt[k]) case inOld && !inNew: - d.remove(ptr.appendKey(k), src[k]) + p := ptr.appendKey(k) + if _, ok := d.opts.ignoredPtrs[p]; !ok { + d.remove(p, src[k]) + } case !inOld && inNew: - d.add(ptr.appendKey(k), tgt[k]) + p := ptr.appendKey(k) + if _, ok := d.opts.ignoredPtrs[p]; !ok { + d.add(p, tgt[k]) + } } } } @@ -213,7 +228,9 @@ func (d *Differ) compareArrays(ptr pointer, src, tgt []interface{}) { // from the destination and the removal index // is always equal to the original array length. for i := size; i < len(src); i++ { - d.remove(ptr.appendIndex(size), src[i]) + if _, ok := d.opts.ignoredPtrs[ptr.appendIndex(i)]; !ok { + d.remove(ptr.appendIndex(size), src[i]) + } } if d.opts.equivalent && d.unorderedDeepEqualSlice(src, tgt) { goto next @@ -228,7 +245,9 @@ next: // than the source, entries are appended to the // destination. for i := size; i < len(tgt); i++ { - d.add(ptr.appendKey("-"), tgt[i]) + if _, ok := d.opts.ignoredPtrs[ptr.appendIndex(i)]; !ok { + d.add(ptr.appendKey("-"), tgt[i]) + } } } @@ -244,7 +263,7 @@ func (d *Differ) unorderedDeepEqualSlice(src, tgt []interface{}) bool { } for _, v := range tgt { k := d.hasher.digest(v) - // If the digest hash if not in the Compare, + // If the digest hash is not in the compare, // return early. if _, ok := diff[k]; !ok { return false diff --git a/differ_test.go b/differ_test.go index 3d3779d..b02559e 100644 --- a/differ_test.go +++ b/differ_test.go @@ -2,7 +2,8 @@ package jsondiff import ( "encoding/json" - "io/ioutil" + "fmt" + "os" "path/filepath" "reflect" "strings" @@ -12,12 +13,16 @@ import ( var testNameReplacer = strings.NewReplacer(",", "", "(", "", ")", "") type testcase struct { - Name string `json:"name"` - Before interface{} `json:"before"` - After interface{} `json:"after"` - Patch []Operation `json:"patch"` + Name string `json:"name"` + Before interface{} `json:"before"` + After interface{} `json:"after"` + Patch Patch `json:"patch"` + IncompletePatch Patch `json:"incomplete_patch"` + Ignores []string `json:"ignores"` } +type patchGetter func(tc *testcase) Patch + func TestArrayCases(t *testing.T) { runCasesFromFile(t, "testdata/tests/array.json") } func TestObjectCases(t *testing.T) { runCasesFromFile(t, "testdata/tests/object.json") } func TestRootCases(t *testing.T) { runCasesFromFile(t, "testdata/tests/root.json") } @@ -33,6 +38,7 @@ func TestOptions(t *testing.T) { {"testdata/tests/options/factorization.json", makeopts(Factorize())}, {"testdata/tests/options/rationalization.json", makeopts(Rationalize())}, {"testdata/tests/options/equivalence.json", makeopts(Equivalent())}, + {"testdata/tests/options/ignore.json", makeopts()}, {"testdata/tests/options/all.json", makeopts(Factorize(), Rationalize(), Invertible(), Equivalent())}, } { var ( @@ -47,7 +53,7 @@ func TestOptions(t *testing.T) { } func runCasesFromFile(t *testing.T, filename string, opts ...Option) { - b, err := ioutil.ReadFile(filename) + b, err := os.ReadFile(filename) if err != nil { t.Fatal(err) } @@ -63,42 +69,60 @@ func runTestCases(t *testing.T, cases []testcase, opts ...Option) { name := testNameReplacer.Replace(tc.Name) t.Run(name, func(t *testing.T) { - beforeBytes, err := json.Marshal(tc.Before) - if err != nil { - t.Error(err) - } - d := Differ{ - targetBytes: beforeBytes, - } - d.applyOpts(opts...) - d.Compare(tc.Before, tc.After) + runTestCase(t, tc, func(tc *testcase) Patch { + return tc.Patch + }, opts...) + }) + if len(tc.Ignores) != 0 { + name = fmt.Sprintf("%s_with_ignore", name) + xopts := append(opts, Ignores(tc.Ignores...)) - if d.patch != nil { - t.Logf("\n%s", d.patch) - } - if len(d.patch) != len(tc.Patch) { - t.Errorf("got %d patches, want %d", len(d.patch), len(tc.Patch)) - return + t.Run(name, func(t *testing.T) { + runTestCase(t, tc, func(tc *testcase) Patch { + return tc.IncompletePatch + }, xopts...) + }) + } + } +} + +func runTestCase(t *testing.T, tc testcase, pc patchGetter, opts ...Option) { + beforeBytes, err := json.Marshal(tc.Before) + if err != nil { + t.Error(err) + } + d := Differ{ + targetBytes: beforeBytes, + } + d.applyOpts(opts...) + d.Compare(tc.Before, tc.After) + + wantPatch := pc(&tc) + + if d.patch != nil { + t.Logf("\n%s", d.patch) + } + if len(d.patch) != len(wantPatch) { + t.Errorf("got %d patches, want %d", len(d.patch), len(wantPatch)) + return + } + for i, op := range d.patch { + want := wantPatch[i] + if g, w := op.Type, want.Type; g != w { + t.Errorf("op #%d mismatch: op: got %q, want %q", i, g, w) + } + if g, w := op.Path.String(), want.Path.String(); g != w { + t.Errorf("op #%d mismatch: path: got %q, want %q", i, g, w) + } + switch want.Type { + case OperationCopy, OperationMove: + if g, w := op.From.String(), want.From.String(); g != w { + t.Errorf("op #%d mismatch: from: got %q, want %q", i, g, w) } - for i, op := range d.patch { - want := tc.Patch[i] - if g, w := op.Type, want.Type; g != w { - t.Errorf("op #%d mismatch: op: got %q, want %q", i, g, w) - } - if g, w := op.Path.String(), want.Path.String(); g != w { - t.Errorf("op #%d mismatch: path: got %q, want %q", i, g, w) - } - switch want.Type { - case OperationCopy, OperationMove: - if g, w := op.From.String(), want.From.String(); g != w { - t.Errorf("op #%d mismatch: from: got %q, want %q", i, g, w) - } - case OperationAdd, OperationReplace: - if !reflect.DeepEqual(op.Value, want.Value) { - t.Errorf("op #%d mismatch: value: unequal", i) - } - } + case OperationAdd, OperationReplace: + if !reflect.DeepEqual(op.Value, want.Value) { + t.Errorf("op #%d mismatch: value: unequal", i) } - }) + } } } diff --git a/equal.go b/equal.go index 49c38cf..6828cb5 100644 --- a/equal.go +++ b/equal.go @@ -1,7 +1,6 @@ package jsondiff import ( - "fmt" "reflect" ) @@ -13,7 +12,7 @@ func areComparable(i1, i2 interface{}) bool { } // typeSwitchKind returns the reflect.Kind of -// the interface i using a type switch statement. +// the interface using a type switch statement. func typeSwitchKind(i interface{}) reflect.Kind { switch i.(type) { case string: @@ -29,7 +28,11 @@ func typeSwitchKind(i interface{}) reflect.Kind { case map[string]interface{}: return reflect.Map default: - panic(fmt.Sprintf("invalid json type %T", i)) + // reflect.Invalid is the zero-value of the + // reflect.Kind type and does not represent + // the actual kind of the value, it is used + // to signal an unsupported type. + return reflect.Invalid } } diff --git a/equal_test.go b/equal_test.go index 57079c2..260860c 100644 --- a/equal_test.go +++ b/equal_test.go @@ -26,3 +26,62 @@ func BenchmarkGetType(b *testing.B) { } }) } + +func Test_typeSwitchKind(t *testing.T) { + for _, tt := range []struct { + val any + valid bool + kind reflect.Kind + }{ + { + "foo", + true, + reflect.String, + }, + { + false, + true, + reflect.Bool, + }, + { + float32(3.14), + false, + reflect.Invalid, + }, + { + nil, + true, + reflect.Ptr, + }, + { + &struct{}{}, + false, + reflect.Invalid, + }, + { + 3.14, + true, + reflect.Float64, + }, + { + func() {}, + false, + reflect.Invalid, + }, + { + []interface{}{}, + true, + reflect.Slice, + }, + { + map[string]interface{}{}, + true, + reflect.Map, + }, + } { + k := typeSwitchKind(tt.val) + if k != tt.kind { + t.Errorf("got %s, want %s", k, tt.kind) + } + } +} diff --git a/example_test.go b/example_test.go index fb2b649..4f3bc38 100644 --- a/example_test.go +++ b/example_test.go @@ -3,8 +3,8 @@ package jsondiff_test import ( "encoding/json" "fmt" - "io/ioutil" "log" + "os" "github.com/wI2L/jsondiff" ) @@ -120,7 +120,7 @@ func ExampleCompareJSON() { Age int `json:"age"` Phones []Phone `json:"phoneNumbers"` } - source, err := ioutil.ReadFile("testdata/examples/john.json") + source, err := os.ReadFile("testdata/examples/john.json") if err != nil { log.Fatal(err) } @@ -191,3 +191,21 @@ func ExampleFactorize() { // {"op":"copy","from":"/a","path":"/c"} // {"op":"move","from":"/b","path":"/d"} } + +func ExampleIgnores() { + source := `{"A":"bar","B":"baz","C":"foo"}` + target := `{"A":"rab","B":"baz","D":"foo"}` + + patch, err := jsondiff.CompareJSONOpts( + []byte(source), + []byte(target), + jsondiff.Ignores("/A", "/C", "/D"), + ) + if err != nil { + log.Fatal(err) + } + for _, op := range patch { + fmt.Printf("%s\n", op) + } + // Output: +} diff --git a/go.mod b/go.mod index 105f1d5..1b6ca93 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/wI2L/jsondiff -go 1.17 +go 1.18 -require github.com/tidwall/gjson v1.14.3 +require github.com/tidwall/gjson v1.14.4 require ( github.com/tidwall/match v1.1.1 // indirect diff --git a/go.sum b/go.sum index 95c208e..57ae69a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= -github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= diff --git a/option.go b/option.go index 0d5fcb0..b62ef9d 100644 --- a/option.go +++ b/option.go @@ -14,7 +14,7 @@ func Rationalize() Option { } // Equivalent disables the generation of operations for -// arrays of equal length and content that are not ordered. +// arrays of equal length and unordered/equal elements. func Equivalent() Option { return func(o *Differ) { o.opts.equivalent = true } } @@ -29,3 +29,15 @@ func Equivalent() Option { func Invertible() Option { return func(o *Differ) { o.opts.invertible = true } } + +// Ignores defines the list of values that are ignored +// by the diff generation, represented as a list of JSON +// Pointer strings (RFC 6901). +func Ignores(ptrs ...string) Option { + return func(o *Differ) { + o.opts.ignoredPtrs = make(map[pointer]struct{}, len(ptrs)) + for _, ptr := range ptrs { + o.opts.ignoredPtrs[pointer(ptr)] = struct{}{} + } + } +} diff --git a/pointer.go b/pointer.go index 007d92f..f451fb2 100644 --- a/pointer.go +++ b/pointer.go @@ -1,11 +1,16 @@ package jsondiff import ( + "errors" + "fmt" "strconv" "strings" ) -const jsonPointerSep = "/" +const ( + ptrSeparator = "/" + emptyPtr = pointer("") +) var ( // rfc6901Replacer is a replacer used to escape JSON @@ -20,16 +25,9 @@ var ( dotPathReplacer = strings.NewReplacer(".", "\\.", "/", ".") ) -type jsonNode struct { - ptr pointer - val interface{} -} - // pointer represents a RFC6901 JSON Pointer. type pointer string -const emptyPtr = pointer("") - // String implements the fmt.Stringer interface. func (p pointer) String() string { return string(p) @@ -45,13 +43,69 @@ func (p pointer) toJSONPath() string { } func (p pointer) appendKey(key string) pointer { - return pointer(string(p) + jsonPointerSep + rfc6901Replacer.Replace(key)) + return pointer(string(p) + ptrSeparator + rfc6901Replacer.Replace(key)) } func (p pointer) appendIndex(idx int) pointer { - return pointer(string(p) + jsonPointerSep + strconv.Itoa(idx)) + return pointer(string(p) + ptrSeparator + strconv.Itoa(idx)) } func (p pointer) isRoot() bool { return len(p) == 0 } + +var ( + errLeadingSlash = errors.New("no leading slash") + errIncompleteEscapeSequence = errors.New("incomplete escape sequence") + errInvalidEscapeSequence = errors.New("invalid escape sequence") +) + +func parsePointer(s string) ([]string, error) { + if s == "" { + return nil, nil + } + a := []rune(s) + + if len(a) > 0 && a[0] != '/' { + return nil, errLeadingSlash + } + var tokens []string + + ls := 0 + for i, r := range a { + if r == '/' { + if i != 0 { + tokens = append(tokens, string(a[ls+1:i])) + } + if i == len(a)-1 { + // Last char is a '/', next fragment is an empty string. + tokens = append(tokens, "") + break + } + ls = i + } else if r == '~' { + if i == len(a)-1 { + return nil, errIncompleteEscapeSequence + } + if a[i+1] != '0' && a[i+1] != '1' { + return nil, errInvalidEscapeSequence + } + } else { + if !isUnescaped(r) { + return nil, fmt.Errorf("invalid rune %q", r) + } + if i == len(a)-1 { + // End of string, accumulate from last separator. + tokens = append(tokens, string(a[ls+1:])) + } + } + } + return tokens, nil +} + +func isUnescaped(r rune) bool { + // Unescaped range is defined as: + // %x00-2E / %x30-7D / %x7F-10FFFF + // https://datatracker.ietf.org/doc/html/rfc6901#section-3 + return r >= 0x00 && r <= 0x2E || r >= 0x30 && r <= 0x7D || r >= 0x7F && r <= 0x10FFFF +} diff --git a/pointer_test.go b/pointer_test.go new file mode 100644 index 0000000..36e52d5 --- /dev/null +++ b/pointer_test.go @@ -0,0 +1,168 @@ +package jsondiff + +import ( + "reflect" + "testing" +) + +func Test_parsePointer(t *testing.T) { + for _, tt := range []struct { + ptr string + valid bool + err error + ntok int + tokens []string + }{ + // RFC Section 5. + // https://tools.ietf.org/html/rfc6901#section-5 + { + "", + true, + nil, + 0, + nil, + }, + { + "/foo", + true, + nil, + 1, + []string{"foo"}, + }, + { + "/foo/0", + true, + nil, + 2, + []string{"foo", "0"}, + }, + { + "/", + true, + nil, + 1, + []string{""}, + }, + { + "/a~1b", + true, + nil, + 1, + []string{"a~1b"}, + }, + { + "/c%d", + true, + nil, + 1, + []string{"c%d"}, + }, + { + "/e^f", + true, + nil, + 1, + []string{"e^f"}, + }, + { + "/g|h", + true, + nil, + 1, + []string{"g|h"}, + }, + { + "/i\\j", + true, + nil, + 1, + []string{"i\\j"}, + }, + { + "/k\"l", + true, + nil, + 1, + []string{"k\"l"}, + }, + { + "/ ", + true, + nil, + 1, + []string{" "}, + }, + { + "/m~0n", + true, + nil, + 1, + []string{"m~0n"}, + }, + // Custom tests. + // Simple. + { + "/a/b/c", + true, + nil, + 3, + []string{"a", "b", "c"}, + }, + { + "/a/0/b", + true, + nil, + 3, + []string{"a", "0", "b"}, + }, + // Complex. + { + "/a/b/", + true, + nil, + 3, + []string{"a", "b", ""}, + }, + // Error cases. + { + "a/b/c", + false, + errLeadingSlash, + 0, + nil, + }, + { + "/a/~", + false, + errIncompleteEscapeSequence, + 0, + nil, + }, + { + "/a/b/~3", + false, + errInvalidEscapeSequence, + 0, + nil, + }, + } { + tokens, err := parsePointer(tt.ptr) + if tt.valid && err != nil { + t.Errorf("expected valid pointer, got error: %q", err) + } + if !tt.valid { + if err == nil { + t.Errorf("expected error, got none") + } else if err != tt.err { + t.Errorf("error mismtahc, got %q, want %q", err, tt.err) + } + } + if l := len(tokens); l != tt.ntok { + t.Errorf("got %d tokens, want %d: %q", l, tt.ntok, tt.ptr) + } else { + if !reflect.DeepEqual(tokens, tt.tokens) { + t.Errorf("tokens mismatch, got %v, want %v", tokens, tt.tokens) + } + } + } +} diff --git a/testdata/tests/options/ignore.json b/testdata/tests/options/ignore.json new file mode 100644 index 0000000..cb752eb --- /dev/null +++ b/testdata/tests/options/ignore.json @@ -0,0 +1,179 @@ +[{ + "name": "ignore updated elements of a array", + "before": [ + "a", "b", "c", "d", "e" + ], + "after": [ + "a", "x", "c", "y", "z" + ], + "ignores": [ + "/1", + "/3" + ], + "patch": [ + { "op": "replace", "path": "/1", "value": "x" }, + { "op": "replace", "path": "/3", "value": "y" }, + { "op": "replace", "path": "/4", "value": "z" } + ], + "incomplete_patch": [ + { "op": "replace", "path": "/4", "value": "z" } + ] +}, { + "name": "ignore added keys of a array", + "before": [ + "a", "b", "c" + ], + "after": [ + "a", "b", "c", "d", "e" + ], + "ignores": [ + "/3" + ], + "patch": [ + { "op": "add", "path": "/-", "value": "d" }, + { "op": "add", "path": "/-", "value": "e" } + ], + "incomplete_patch": [ + { "op": "add", "path": "/-", "value": "e" } + ] +}, { + "name": "ignore removed keys of a array", + "before": [ + "a", "b", "c", "d", "e" + ], + "after": [ + "a", "b", "c" + ], + "ignores": [ + "/4" + ], + "patch": [ + { "op": "remove", "path": "/3" }, + { "op": "remove", "path": "/3" } + ], + "incomplete_patch": [ + { "op": "remove", "path": "/3" } + ] +},{ + "name": "ignore updated keys of an object", + "before": { + "a": "AAA", + "b": "BBB", + "c": "CCC" + }, + "after": { + "a": "BBB", + "b": "AAA", + "c": "DDD" + }, + "ignores": [ + "/a" + ], + "patch": [ + { "op": "replace", "path": "/a", "value": "BBB" }, + { "op": "replace", "path": "/b", "value": "AAA" }, + { "op": "replace", "path": "/c", "value": "DDD" } + ], + "incomplete_patch": [ + { "op": "replace", "path": "/b", "value": "AAA" }, + { "op": "replace", "path": "/c", "value": "DDD" } + ] +}, { + "name": "ignore added keys of an object", + "before": { + "a": "AAA", + "b": "BBB", + "c": "CCC" + }, + "after": { + "a": "AAA", + "b": "BBB", + "c": "CCC", + "d": "DDD", + "e": "EEE" + }, + "ignores": [ + "/d" + ], + "patch": [ + { "op": "add", "path": "/d", "value": "DDD" }, + { "op": "add", "path": "/e", "value": "EEE" } + ], + "incomplete_patch": [ + { "op": "add", "path": "/e", "value": "EEE" } + ] +},{ + "name": "ignore removed keys of an object", + "before": { + "a": "AAA", + "b": "BBB", + "c": "CCC", + "d": "DDD", + "e": "EEE" + }, + "after": { + "b": "BBB", + "d": "DDD", + "e": "EEE" + }, + "ignores": [ + "/a" + ], + "patch": [ + { "op": "remove", "path": "/a" }, + { "op": "remove", "path": "/c" } + ], + "incomplete_patch": [ + { "op": "remove", "path": "/c" } + ] +}, { + "name": "ignore embedded array index in objects", + "before": { + "a": { + "b": { + "c": [ + "x", "y", "z" + ] + } + } + }, + "after": { + "a": { + "b": { + "c": [ + "x", "Y", "z" + ] + } + } + }, + "ignores": [ + "/a/b/c/1" + ], + "patch": [ + { "op": "replace", "path": "/a/b/c/1", "value": "Y" } + ], + "incomplete_patch": null +}, { + "name": "ignore embedded object keys in array", + "before": [ + { + "a": { + "b": "c" + } + } + ], + "after": [ + { + "a": { + "b": "d" + } + } + ], + "ignores": [ + "/0/a/b" + ], + "patch": [ + { "op": "replace", "path": "/0/a/b", "value": "d" } + ], + "incomplete_patch": null +}] \ No newline at end of file