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