From 9d7a8df7850ae5ac649c794b9186c73e7cd4c370 Mon Sep 17 00:00:00 2001 From: Sam Ward Date: Wed, 29 May 2019 17:11:39 -0700 Subject: [PATCH 01/23] Expose Operation and methods, fix lint errors --- patch.go | 108 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 31 deletions(-) diff --git a/patch.go b/patch.go index c9cf590..38aff8e 100644 --- a/patch.go +++ b/patch.go @@ -3,6 +3,7 @@ package jsonpatch import ( "bytes" "encoding/json" + "errors" "fmt" "strconv" "strings" @@ -18,10 +19,10 @@ var ( // SupportNegativeIndices decides whether to support non-standard practice of // allowing negative indices to mean indices starting at the end of an array. // Default to true. - SupportNegativeIndices bool = true + SupportNegativeIndices = true // AccumulatedCopySizeLimit limits the total size increase in bytes caused by // "copy" operations in a patch. - AccumulatedCopySizeLimit int64 = 0 + AccumulatedCopySizeLimit int64 ) type lazyNode struct { @@ -31,10 +32,11 @@ type lazyNode struct { which int } -type operation map[string]*json.RawMessage +// Operation is a single JSON-Patch step, such as a single 'add' operation. +type Operation map[string]*json.RawMessage -// Patch is an ordered collection of operations. -type Patch []operation +// Patch is an ordered collection of Operations. +type Patch []Operation type partialDoc map[string]*lazyNode type partialArray []*lazyNode @@ -227,7 +229,8 @@ func (n *lazyNode) equal(o *lazyNode) bool { return true } -func (o operation) kind() string { +// Kind reads the "op" field of the Operation. +func (o Operation) Kind() string { if obj, ok := o["op"]; ok && obj != nil { var op string @@ -243,39 +246,41 @@ func (o operation) kind() string { return "unknown" } -func (o operation) path() string { +// Path reads the "path" field of the Operation. +func (o Operation) Path() (string, error) { if obj, ok := o["path"]; ok && obj != nil { var op string err := json.Unmarshal(*obj, &op) if err != nil { - return "unknown" + return "unknown", err } - return op + return op, nil } - return "unknown" + return "unknown", errors.New("operation missing path field") } -func (o operation) from() string { +// From reads the "from" field of the Operation. +func (o Operation) From() (string, error) { if obj, ok := o["from"]; ok && obj != nil { var op string err := json.Unmarshal(*obj, &op) if err != nil { - return "unknown" + return "unknown", err } - return op + return op, nil } - return "unknown" + return "unknown", errors.New("operation missing from field") } -func (o operation) value() *lazyNode { +func (o Operation) value() *lazyNode { if obj, ok := o["value"]; ok { return newLazyNode(obj) } @@ -283,6 +288,23 @@ func (o operation) value() *lazyNode { return nil } +// ValueInterface decodes the operation value into an interface. +func (o Operation) ValueInterface() (interface{}, error) { + if obj, ok := o["value"]; ok && obj != nil { + var v interface{} + + err := json.Unmarshal(*obj, &v) + + if err != nil { + return nil, err + } + + return v, nil + } + + return nil, errors.New("operation missing value field") +} + func isArray(buf []byte) bool { Loop: for _, c := range buf { @@ -462,8 +484,11 @@ func (d *partialArray) remove(key string) error { } -func (p Patch) add(doc *container, op operation) error { - path := op.path() +func (p Patch) add(doc *container, op Operation) error { + path, err := op.Path() + if err != nil { + return fmt.Errorf("jsonpatch add operation failed to decode path: %s", err) + } con, key := findObject(doc, path) @@ -474,8 +499,11 @@ func (p Patch) add(doc *container, op operation) error { return con.add(key, op.value()) } -func (p Patch) remove(doc *container, op operation) error { - path := op.path() +func (p Patch) remove(doc *container, op Operation) error { + path, err := op.Path() + if err != nil { + return fmt.Errorf("jsonpatch remove operation failed to decode path: %s", err) + } con, key := findObject(doc, path) @@ -486,8 +514,11 @@ func (p Patch) remove(doc *container, op operation) error { return con.remove(key) } -func (p Patch) replace(doc *container, op operation) error { - path := op.path() +func (p Patch) replace(doc *container, op Operation) error { + path, err := op.Path() + if err != nil { + return fmt.Errorf("jsonpatch replace operation failed to decode path: %s", err) + } con, key := findObject(doc, path) @@ -503,8 +534,11 @@ func (p Patch) replace(doc *container, op operation) error { return con.set(key, op.value()) } -func (p Patch) move(doc *container, op operation) error { - from := op.from() +func (p Patch) move(doc *container, op Operation) error { + from, err := op.From() + if err != nil { + return fmt.Errorf("jsonpatch _ operation failed to decode from: %s", err) + } con, key := findObject(doc, from) @@ -522,7 +556,10 @@ func (p Patch) move(doc *container, op operation) error { return err } - path := op.path() + path, err := op.Path() + if err != nil { + return fmt.Errorf("jsonpatch _ operation failed to decode path: %s", err) + } con, key = findObject(doc, path) @@ -533,8 +570,11 @@ func (p Patch) move(doc *container, op operation) error { return con.add(key, val) } -func (p Patch) test(doc *container, op operation) error { - path := op.path() +func (p Patch) test(doc *container, op Operation) error { + path, err := op.Path() + if err != nil { + return fmt.Errorf("jsonpatch _ operation failed to decode path: %s", err) + } con, key := findObject(doc, path) @@ -564,8 +604,11 @@ func (p Patch) test(doc *container, op operation) error { return fmt.Errorf("Testing value %s failed", path) } -func (p Patch) copy(doc *container, op operation, accumulatedCopySize *int64) error { - from := op.from() +func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64) error { + from, err := op.From() + if err != nil { + return fmt.Errorf("jsonpatch _ operation failed to decode from: %s", err) + } con, key := findObject(doc, from) @@ -578,7 +621,10 @@ func (p Patch) copy(doc *container, op operation, accumulatedCopySize *int64) er return err } - path := op.path() + path, err := op.Path() + if err != nil { + return fmt.Errorf("jsonpatch _ operation failed to decode path: %s", err) + } con, key = findObject(doc, path) @@ -651,7 +697,7 @@ func (p Patch) ApplyIndent(doc []byte, indent string) ([]byte, error) { var accumulatedCopySize int64 for _, op := range p { - switch op.kind() { + switch op.Kind() { case "add": err = p.add(&pd, op) case "remove": @@ -665,7 +711,7 @@ func (p Patch) ApplyIndent(doc []byte, indent string) ([]byte, error) { case "copy": err = p.copy(&pd, op, &accumulatedCopySize) default: - err = fmt.Errorf("Unexpected kind: %s", op.kind()) + err = fmt.Errorf("Unexpected kind: %s", op.Kind()) } if err != nil { From 250db642d4b83e9a56efd4ea28854f5b4debe07b Mon Sep 17 00:00:00 2001 From: Sam Ward Date: Wed, 29 May 2019 17:19:17 -0700 Subject: [PATCH 02/23] Update error messages --- patch.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/patch.go b/patch.go index 38aff8e..cc4f66e 100644 --- a/patch.go +++ b/patch.go @@ -537,7 +537,7 @@ func (p Patch) replace(doc *container, op Operation) error { func (p Patch) move(doc *container, op Operation) error { from, err := op.From() if err != nil { - return fmt.Errorf("jsonpatch _ operation failed to decode from: %s", err) + return fmt.Errorf("jsonpatch move operation failed to decode from: %s", err) } con, key := findObject(doc, from) @@ -558,7 +558,7 @@ func (p Patch) move(doc *container, op Operation) error { path, err := op.Path() if err != nil { - return fmt.Errorf("jsonpatch _ operation failed to decode path: %s", err) + return fmt.Errorf("jsonpatch move operation failed to decode path: %s", err) } con, key = findObject(doc, path) @@ -573,7 +573,7 @@ func (p Patch) move(doc *container, op Operation) error { func (p Patch) test(doc *container, op Operation) error { path, err := op.Path() if err != nil { - return fmt.Errorf("jsonpatch _ operation failed to decode path: %s", err) + return fmt.Errorf("jsonpatch test operation failed to decode path: %s", err) } con, key := findObject(doc, path) @@ -607,7 +607,7 @@ func (p Patch) test(doc *container, op Operation) error { func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64) error { from, err := op.From() if err != nil { - return fmt.Errorf("jsonpatch _ operation failed to decode from: %s", err) + return fmt.Errorf("jsonpatch copy operation failed to decode from: %s", err) } con, key := findObject(doc, from) @@ -623,7 +623,7 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64) er path, err := op.Path() if err != nil { - return fmt.Errorf("jsonpatch _ operation failed to decode path: %s", err) + return fmt.Errorf("jsonpatch copy operation failed to decode path: %s", err) } con, key = findObject(doc, path) From 72d00a977af0798c589689fff0a6d06e1651129c Mon Sep 17 00:00:00 2001 From: Sam Ward Date: Mon, 3 Jun 2019 17:32:21 -0700 Subject: [PATCH 03/23] Revert govet fixes --- patch.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patch.go b/patch.go index cc4f66e..2714a1e 100644 --- a/patch.go +++ b/patch.go @@ -19,10 +19,10 @@ var ( // SupportNegativeIndices decides whether to support non-standard practice of // allowing negative indices to mean indices starting at the end of an array. // Default to true. - SupportNegativeIndices = true + SupportNegativeIndices bool = true // AccumulatedCopySizeLimit limits the total size increase in bytes caused by // "copy" operations in a patch. - AccumulatedCopySizeLimit int64 + AccumulatedCopySizeLimit int64 = 0 ) type lazyNode struct { From 026c730a0dcc5d11f93f1cf1cc65b01247ea7b6f Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Mon, 10 Jun 2019 16:46:25 -0700 Subject: [PATCH 04/23] Cleanup errors to help understand what failed. Fixes #75 --- patch.go | 124 ++++++++++++++++++++++++++++++++------------------ patch_test.go | 2 +- 2 files changed, 80 insertions(+), 46 deletions(-) diff --git a/patch.go b/patch.go index 2714a1e..1b5f95e 100644 --- a/patch.go +++ b/patch.go @@ -3,10 +3,11 @@ package jsonpatch import ( "bytes" "encoding/json" - "errors" "fmt" "strconv" "strings" + + "github.com/pkg/errors" ) const ( @@ -25,6 +26,14 @@ var ( AccumulatedCopySizeLimit int64 = 0 ) +var ( + ErrTestFailed = errors.New("test failed") + ErrMissing = errors.New("missing value") + ErrUnknownType = errors.New("unknown object type") + ErrInvalid = errors.New("invalid state detected") + ErrInvalidIndex = errors.New("invalid index referenced") +) + type lazyNode struct { raw *json.RawMessage doc partialDoc @@ -61,7 +70,7 @@ func (n *lazyNode) MarshalJSON() ([]byte, error) { case eAry: return json.Marshal(n.ary) default: - return nil, fmt.Errorf("Unknown type") + return nil, ErrUnknownType } } @@ -93,7 +102,7 @@ func (n *lazyNode) intoDoc() (*partialDoc, error) { } if n.raw == nil { - return nil, fmt.Errorf("Unable to unmarshal nil pointer as partial document") + return nil, ErrInvalid } err := json.Unmarshal(*n.raw, &n.doc) @@ -112,7 +121,7 @@ func (n *lazyNode) intoAry() (*partialArray, error) { } if n.raw == nil { - return nil, fmt.Errorf("Unable to unmarshal nil pointer as partial array") + return nil, ErrInvalid } err := json.Unmarshal(*n.raw, &n.ary) @@ -260,7 +269,7 @@ func (o Operation) Path() (string, error) { return op, nil } - return "unknown", errors.New("operation missing path field") + return "unknown", errors.Wrapf(ErrMissing, "operation missing path field") } // From reads the "from" field of the Operation. @@ -277,7 +286,7 @@ func (o Operation) From() (string, error) { return op, nil } - return "unknown", errors.New("operation missing from field") + return "unknown", errors.Wrapf(ErrMissing, "operation, missing from field") } func (o Operation) value() *lazyNode { @@ -302,7 +311,7 @@ func (o Operation) ValueInterface() (interface{}, error) { return v, nil } - return nil, errors.New("operation missing value field") + return nil, errors.Wrapf(ErrMissing, "operation, missing value field") } func isArray(buf []byte) bool { @@ -381,7 +390,7 @@ func (d *partialDoc) get(key string) (*lazyNode, error) { func (d *partialDoc) remove(key string) error { _, ok := (*d)[key] if !ok { - return fmt.Errorf("Unable to remove nonexistent key: %s", key) + return errors.Wrapf(ErrMissing, "Unable to remove nonexistent key: %s", key) } delete(*d, key) @@ -407,7 +416,7 @@ func (d *partialArray) add(key string, val *lazyNode) error { idx, err := strconv.Atoi(key) if err != nil { - return err + return errors.Wrapf(err, "value was not a proper array index: '%s'", key) } sz := len(*d) + 1 @@ -417,12 +426,12 @@ func (d *partialArray) add(key string, val *lazyNode) error { cur := *d if idx >= len(ary) { - return fmt.Errorf("Unable to access invalid index: %d", idx) + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } if SupportNegativeIndices { if idx < -len(ary) { - return fmt.Errorf("Unable to access invalid index: %d", idx) + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } if idx < 0 { @@ -446,7 +455,7 @@ func (d *partialArray) get(key string) (*lazyNode, error) { } if idx >= len(*d) { - return nil, fmt.Errorf("Unable to access invalid index: %d", idx) + return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } return (*d)[idx], nil @@ -461,12 +470,12 @@ func (d *partialArray) remove(key string) error { cur := *d if idx >= len(cur) { - return fmt.Errorf("Unable to access invalid index: %d", idx) + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } if SupportNegativeIndices { if idx < -len(cur) { - return fmt.Errorf("Unable to access invalid index: %d", idx) + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } if idx < 0 { @@ -487,161 +496,186 @@ func (d *partialArray) remove(key string) error { func (p Patch) add(doc *container, op Operation) error { path, err := op.Path() if err != nil { - return fmt.Errorf("jsonpatch add operation failed to decode path: %s", err) + return errors.Wrapf(ErrMissing, "add operation failed to decode path") } con, key := findObject(doc, path) if con == nil { - return fmt.Errorf("jsonpatch add operation does not apply: doc is missing path: \"%s\"", path) + return errors.Wrapf(ErrMissing, "add operation does not apply: doc is missing path: \"%s\"", path) } - return con.add(key, op.value()) + err = con.add(key, op.value()) + if err != nil { + return errors.Wrapf(err, "error in add for path: '%s'", path) + } + + return nil } func (p Patch) remove(doc *container, op Operation) error { path, err := op.Path() if err != nil { - return fmt.Errorf("jsonpatch remove operation failed to decode path: %s", err) + return errors.Wrapf(ErrMissing, "remove operation failed to decode path") } con, key := findObject(doc, path) if con == nil { - return fmt.Errorf("jsonpatch remove operation does not apply: doc is missing path: \"%s\"", path) + return errors.Wrapf(ErrMissing, "remove operation does not apply: doc is missing path: \"%s\"", path) } - return con.remove(key) + err = con.remove(key) + if err != nil { + return errors.Wrapf(err, "error in remove for path: '%s'", path) + } + + return nil } func (p Patch) replace(doc *container, op Operation) error { path, err := op.Path() if err != nil { - return fmt.Errorf("jsonpatch replace operation failed to decode path: %s", err) + return errors.Wrapf(err, "replace operation failed to decode path") } con, key := findObject(doc, path) if con == nil { - return fmt.Errorf("jsonpatch replace operation does not apply: doc is missing path: %s", path) + return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing path: %s", path) } _, ok := con.get(key) if ok != nil { - return fmt.Errorf("jsonpatch replace operation does not apply: doc is missing key: %s", path) + return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing key: %s", path) + } + + err = con.set(key, op.value()) + if err != nil { + return errors.Wrapf(err, "error in remove for path: '%s'", path) } - return con.set(key, op.value()) + return nil } func (p Patch) move(doc *container, op Operation) error { from, err := op.From() if err != nil { - return fmt.Errorf("jsonpatch move operation failed to decode from: %s", err) + return errors.Wrapf(err, "move operation failed to decode from") } con, key := findObject(doc, from) if con == nil { - return fmt.Errorf("jsonpatch move operation does not apply: doc is missing from path: %s", from) + return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing from path: %s", from) } val, err := con.get(key) if err != nil { - return err + return errors.Wrapf(err, "error in move for path: '%s'", key) } err = con.remove(key) if err != nil { - return err + return errors.Wrapf(err, "error in move for path: '%s'", key) } path, err := op.Path() if err != nil { - return fmt.Errorf("jsonpatch move operation failed to decode path: %s", err) + return errors.Wrapf(err, "move operation failed to decode path") } con, key = findObject(doc, path) if con == nil { - return fmt.Errorf("jsonpatch move operation does not apply: doc is missing destination path: %s", path) + return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing destination path: %s", path) + } + + err = con.add(key, val) + if err != nil { + return errors.Wrapf(err, "error in move for path: '%s'", path) } - return con.add(key, val) + return nil } func (p Patch) test(doc *container, op Operation) error { path, err := op.Path() if err != nil { - return fmt.Errorf("jsonpatch test operation failed to decode path: %s", err) + return errors.Wrapf(err, "test operation failed to decode path") } con, key := findObject(doc, path) if con == nil { - return fmt.Errorf("jsonpatch test operation does not apply: is missing path: %s", path) + return errors.Wrapf(ErrMissing, "test operation does not apply: is missing path: %s", path) } val, err := con.get(key) - if err != nil { - return err + return errors.Wrapf(err, "error in test for path: '%s'", path) } if val == nil { if op.value().raw == nil { return nil } - return fmt.Errorf("Testing value %s failed", path) + return errors.Wrapf(ErrTestFailed, "testing value %s failed", path) } else if op.value() == nil { - return fmt.Errorf("Testing value %s failed", path) + return errors.Wrapf(ErrTestFailed, "testing value %s failed", path) } if val.equal(op.value()) { return nil } - return fmt.Errorf("Testing value %s failed", path) + return errors.Wrapf(ErrTestFailed, "testing value %s failed", path) } func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64) error { from, err := op.From() if err != nil { - return fmt.Errorf("jsonpatch copy operation failed to decode from: %s", err) + return errors.Wrapf(err, "copy operation failed to decode from") } con, key := findObject(doc, from) if con == nil { - return fmt.Errorf("jsonpatch copy operation does not apply: doc is missing from path: %s", from) + return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing from path: %s", from) } val, err := con.get(key) if err != nil { - return err + return errors.Wrapf(err, "error in copy for from: '%s'", from) } path, err := op.Path() if err != nil { - return fmt.Errorf("jsonpatch copy operation failed to decode path: %s", err) + return errors.Wrapf(ErrMissing, "copy operation failed to decode path") } con, key = findObject(doc, path) if con == nil { - return fmt.Errorf("jsonpatch copy operation does not apply: doc is missing destination path: %s", path) + return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing destination path: %s", path) } valCopy, sz, err := deepCopy(val) if err != nil { - return err + return errors.Wrapf(err, "error while performing deep copy") } + (*accumulatedCopySize) += int64(sz) if AccumulatedCopySizeLimit > 0 && *accumulatedCopySize > AccumulatedCopySizeLimit { return NewAccumulatedCopySizeError(AccumulatedCopySizeLimit, *accumulatedCopySize) } - return con.add(key, valCopy) + err = con.add(key, valCopy) + if err != nil { + return errors.Wrapf(err, "error while adding value during copy") + } + + return nil } // Equal indicates if 2 JSON documents have the same structural equality. diff --git a/patch_test.go b/patch_test.go index 96bcef4..5db2d97 100644 --- a/patch_test.go +++ b/patch_test.go @@ -470,7 +470,7 @@ func TestAllTest(t *testing.T) { } else if !c.result && err == nil { t.Errorf("Testing passed when it should have faild: %s", err) } else if !c.result { - expected := fmt.Sprintf("Testing value %s failed", c.failedPath) + expected := fmt.Sprintf("testing value %s failed: test failed", c.failedPath) if err.Error() != expected { t.Errorf("Testing failed as expected but invalid message: expected [%s], got [%s]", expected, err) } From cb8f3b5f9db3c390f2cdf6e67f7e6ea3b339a197 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Sat, 3 Aug 2019 11:54:03 -0700 Subject: [PATCH 05/23] Add bits to better be a go module --- go.mod | 5 +++++ go.sum | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a858cab --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/evanphx/json-patch + +go 1.12 + +require github.com/pkg/errors v0.8.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f29ab35 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From 978ce82e2d41ef90322aafde4e68643f2eebc1b1 Mon Sep 17 00:00:00 2001 From: Han Kang Date: Thu, 15 Aug 2019 11:33:39 -0700 Subject: [PATCH 06/23] check lengths of maps and recurse over only one if it is necessary --- merge.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/merge.go b/merge.go index 6806c4c..5dc6d34 100644 --- a/merge.go +++ b/merge.go @@ -307,10 +307,8 @@ func matchesValue(av, bv interface{}) bool { return true case map[string]interface{}: bt := bv.(map[string]interface{}) - for key := range at { - if !matchesValue(at[key], bt[key]) { - return false - } + if len(bt) != len(at) { + return false } for key := range bt { if !matchesValue(at[key], bt[key]) { From 93727dee61ef9e3266906ff6aead1142d575fac2 Mon Sep 17 00:00:00 2001 From: Han Kang Date: Thu, 15 Aug 2019 15:53:01 -0700 Subject: [PATCH 07/23] add benchmark tests for matchesValue --- merge_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/merge_test.go b/merge_test.go index 994b1b5..c87b078 100644 --- a/merge_test.go +++ b/merge_test.go @@ -1,6 +1,7 @@ package jsonpatch import ( + "fmt" "strings" "testing" ) @@ -447,6 +448,45 @@ func TestCreateMergePatchComplexAddAll(t *testing.T) { } } +// createNestedMap created a series of nested map objects such that the number of +// objects is roughly 2^n (precisely, 2^(n+1)-1). +func createNestedMap(m map[string]interface{}, depth int, objectCount *int) { + if depth == 0 { + return + } + for i := 0; i< 2;i++ { + nested := map[string]interface{}{} + *objectCount += 1 + createNestedMap(nested, depth-1, objectCount) + m[fmt.Sprintf("key-%v", i)] = nested + } +} + +func benchmarkMatchesValueWithDeeplyNestedFields(depth int, b *testing.B) { + a := map[string]interface{}{} + objCount := 1 + createNestedMap(a, depth, &objCount) + b.ResetTimer() + b.Run(fmt.Sprintf("objectCount=%v", objCount), func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !matchesValue(a, a) { + b.Errorf("Should be equal") + } + } + }) +} + +func BenchmarkMatchesValue1(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(1, b) } +func BenchmarkMatchesValue2(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(2, b) } +func BenchmarkMatchesValue3(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(3, b) } +func BenchmarkMatchesValue4(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(4, b) } +func BenchmarkMatchesValue5(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(5, b) } +func BenchmarkMatchesValue6(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(6, b) } +func BenchmarkMatchesValue7(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(7, b) } +func BenchmarkMatchesValue8(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(8, b) } +func BenchmarkMatchesValue9(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(9, b) } +func BenchmarkMatchesValue10(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(10, b) } + func TestCreateMergePatchComplexRemoveAll(t *testing.T) { doc := `{"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4], "nested": {"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4]} }` exp := `{"a":null,"f":null,"hello":null,"i":null,"n":null,"nested":null,"pi":null,"t":null}` From 6c9bd73542a047157f1ec00be155eec2893a3673 Mon Sep 17 00:00:00 2001 From: Eric Paris Date: Wed, 2 Oct 2019 12:36:16 -0400 Subject: [PATCH 08/23] Fix panic when SupportNegativeIndices is false If SupportNegativeIndices is false then a negative indicie will cause a panic. This regression was introduced in: fcd53eccb4f88d7e54b742358017d72c9b34ae44 Before that change we always checked the validity of the negative index. --- patch.go | 20 ++++++++--------- patch_test.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/patch.go b/patch.go index 1b5f95e..8f75549 100644 --- a/patch.go +++ b/patch.go @@ -429,14 +429,14 @@ func (d *partialArray) add(key string, val *lazyNode) error { return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } - if SupportNegativeIndices { - if idx < -len(ary) { + if idx < 0 { + if !SupportNegativeIndices { return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } - - if idx < 0 { - idx += len(ary) + if idx < -len(ary) { + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } + idx += len(ary) } copy(ary[0:idx], cur[0:idx]) @@ -473,14 +473,14 @@ func (d *partialArray) remove(key string) error { return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } - if SupportNegativeIndices { - if idx < -len(cur) { + if idx < 0 { + if !SupportNegativeIndices { return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } - - if idx < 0 { - idx += len(cur) + if idx < -len(cur) { + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) } + idx += len(cur) } ary := make([]*lazyNode, len(cur)-1) diff --git a/patch_test.go b/patch_test.go index 5db2d97..3a45150 100644 --- a/patch_test.go +++ b/patch_test.go @@ -477,3 +477,65 @@ func TestAllTest(t *testing.T) { } } } + +func TestAdd(t *testing.T) { + testCases := []struct { + name string + key string + val lazyNode + arr partialArray + rejectNegativeIndicies bool + err string + }{ + { + name: "should work", + key: "0", + val: lazyNode{}, + arr: partialArray{}, + }, + { + name: "index too large", + key: "1", + val: lazyNode{}, + arr: partialArray{}, + err: "Unable to access invalid index: 1: invalid index referenced", + }, + { + name: "negative should work", + key: "-1", + val: lazyNode{}, + arr: partialArray{}, + }, + { + name: "negative too small", + key: "-2", + val: lazyNode{}, + arr: partialArray{}, + err: "Unable to access invalid index: -2: invalid index referenced", + }, + { + name: "negative but negative disabled", + key: "-1", + val: lazyNode{}, + arr: partialArray{}, + rejectNegativeIndicies: true, + err: "Unable to access invalid index: -1: invalid index referenced", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + SupportNegativeIndices = !tc.rejectNegativeIndicies + key := tc.key + arr := &tc.arr + val := &tc.val + err := arr.add(key, val) + if err == nil && tc.err != "" { + t.Errorf("Expected error but got none! %v", tc.err) + } else if err != nil && tc.err == "" { + t.Errorf("Did not expect error but go: %v", err) + } else if err != nil && err.Error() != tc.err { + t.Errorf("Expected error %v but got error %v", tc.err, err) + } + }) + } +} From a5df74aab0efb032ca893c048330abcad0b78e6f Mon Sep 17 00:00:00 2001 From: Brett Buddin Date: Wed, 7 Aug 2019 17:12:57 -0400 Subject: [PATCH 09/23] Conform to RFC6902 replacement semantics. A "replace" patch operation referencing a key that does not exist in the target object are currently being accepted; ultimately becoming "add" operations on the target object. This is incorrect per the specification: From https://tools.ietf.org/html/rfc6902#section-4.3 section about "replace": The target location MUST exist for the operation to be successful. This corrects the behavior by returning an error in this situation. --- patch.go | 10 +++++++--- patch_test.go | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/patch.go b/patch.go index 8f75549..c105632 100644 --- a/patch.go +++ b/patch.go @@ -384,13 +384,17 @@ func (d *partialDoc) add(key string, val *lazyNode) error { } func (d *partialDoc) get(key string) (*lazyNode, error) { - return (*d)[key], nil + v, ok := (*d)[key] + if !ok { + return v, errors.Wrapf(ErrMissing, "unable to get nonexistent key: %s", key) + } + return v, nil } func (d *partialDoc) remove(key string) error { _, ok := (*d)[key] if !ok { - return errors.Wrapf(ErrMissing, "Unable to remove nonexistent key: %s", key) + return errors.Wrapf(ErrMissing, "unable to remove nonexistent key: %s", key) } delete(*d, key) @@ -612,7 +616,7 @@ func (p Patch) test(doc *container, op Operation) error { } val, err := con.get(key) - if err != nil { + if err != nil && errors.Cause(err) != ErrMissing { return errors.Wrapf(err, "error in test for path: '%s'", path) } diff --git a/patch_test.go b/patch_test.go index 3a45150..30f6176 100644 --- a/patch_test.go +++ b/patch_test.go @@ -332,6 +332,10 @@ var BadCases = []BadCase{ `{ "foo": [ "all", "grass", "cows", "eat" ] }`, `[ { "op": "move", "from": "/foo/1", "path": "/foo/4" } ]`, }, + { + `{ "baz": "qux" }`, + `[ { "op": "replace", "path": "/foo", "value": "bar" } ]`, + }, } // This is not thread safe, so we cannot run patch tests in parallel. From caa7f266ac971d84049d2a618644979d26114ac8 Mon Sep 17 00:00:00 2001 From: Brett Buddin Date: Mon, 4 Nov 2019 13:16:59 -0500 Subject: [PATCH 10/23] Test for copying non-existent key. --- patch_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/patch_test.go b/patch_test.go index 30f6176..66e5bd3 100644 --- a/patch_test.go +++ b/patch_test.go @@ -336,6 +336,11 @@ var BadCases = []BadCase{ `{ "baz": "qux" }`, `[ { "op": "replace", "path": "/foo", "value": "bar" } ]`, }, + // Can't copy from non-existent "from" key. + { + `{ "foo": "bar"}`, + `[{"op": "copy", "path": "/qux", "from": "/baz"}]`, + }, } // This is not thread safe, so we cannot run patch tests in parallel. From c235ce89e1e02a76eac0be921e6e8fe12d9c6120 Mon Sep 17 00:00:00 2001 From: Brett Buddin Date: Mon, 4 Nov 2019 13:17:14 -0500 Subject: [PATCH 11/23] Test for testing non-existent and null-value keys. --- patch_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/patch_test.go b/patch_test.go index 66e5bd3..69851bc 100644 --- a/patch_test.go +++ b/patch_test.go @@ -468,6 +468,18 @@ var TestCases = []TestCase{ false, "/foo", }, + { + `{ "foo": "bar" }`, + `[ { "op": "test", "path": "/baz", "value": "bar" } ]`, + false, + "/baz", + }, + { + `{ "foo": "bar" }`, + `[ { "op": "test", "path": "/baz", "value": null } ]`, + true, + "/baz", + }, } func TestAllTest(t *testing.T) { From 266303115221f6a79e03731280f3676b22168d2c Mon Sep 17 00:00:00 2001 From: Brett Buddin Date: Mon, 4 Nov 2019 13:20:41 -0500 Subject: [PATCH 12/23] Test for copying null-value key. --- patch_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/patch_test.go b/patch_test.go index 69851bc..8e20cf3 100644 --- a/patch_test.go +++ b/patch_test.go @@ -186,6 +186,11 @@ var Cases = []Case{ `[{"op": "copy", "path": "/foo/0", "from": "/foo"}]`, `{ "foo": [["bar"], "bar"]}`, }, + { + `{ "foo": null}`, + `[{"op": "copy", "path": "/bar", "from": "/foo"}]`, + `{ "foo": null, "bar": null}`, + }, { `{ "foo": ["bar","qux","baz"]}`, `[ { "op": "remove", "path": "/foo/-2"}]`, From d6e02f2e89ae278b8d33584ea8bf4e0d635a091d Mon Sep 17 00:00:00 2001 From: Igor Gubernat Date: Fri, 8 Nov 2019 17:34:34 +0200 Subject: [PATCH 13/23] Equality comparison bug fix --- patch.go | 4 ++++ patch_test.go | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/patch.go b/patch.go index 8f75549..1d7126b 100644 --- a/patch.go +++ b/patch.go @@ -202,6 +202,10 @@ func (n *lazyNode) equal(o *lazyNode) bool { return false } + if len(n.doc) != len(o.doc) { + return false + } + for k, v := range n.doc { ov, ok := o.doc[k] diff --git a/patch_test.go b/patch_test.go index 3a45150..b70b96c 100644 --- a/patch_test.go +++ b/patch_test.go @@ -514,12 +514,12 @@ func TestAdd(t *testing.T) { err: "Unable to access invalid index: -2: invalid index referenced", }, { - name: "negative but negative disabled", - key: "-1", - val: lazyNode{}, - arr: partialArray{}, + name: "negative but negative disabled", + key: "-1", + val: lazyNode{}, + arr: partialArray{}, rejectNegativeIndicies: true, - err: "Unable to access invalid index: -1: invalid index referenced", + err: "Unable to access invalid index: -1: invalid index referenced", }, } for _, tc := range testCases { @@ -539,3 +539,30 @@ func TestAdd(t *testing.T) { }) } } + +type EqualityCase struct { + a, b string + equal bool +} + +var EqualityCases = []EqualityCase{ + EqualityCase{ + `{"foo": "bar"}`, + `{"foo": "bar", "baz": "qux"}`, + false, + }, +} + +func TestEquality(t *testing.T) { + for _, tc := range EqualityCases { + got := Equal([]byte(tc.a), []byte(tc.b)) + if got != tc.equal { + t.Errorf("Expected Equal(%s, %s) to return %t, but got %t", tc.a, tc.b, tc.equal, got) + } + + got = Equal([]byte(tc.b), []byte(tc.a)) + if got != tc.equal { + t.Errorf("Expected Equal(%s, %s) to return %t, but got %t", tc.b, tc.a, tc.equal, got) + } + } +} From 09fbc9aecd72ec357081a8a5c8f5f565d65c034d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A5rd=20Aase?= Date: Fri, 22 Nov 2019 09:54:54 +0100 Subject: [PATCH 14/23] Fixed output of example Made the output text in the examples match what the code does --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9c7f87f..79bbe8a 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ func main() { } if !jsonpatch.Equal(original, different) { - fmt.Println(`"original" is _not_ structurally equal to "similar"`) + fmt.Println(`"original" is _not_ structurally equal to "different"`) } } ``` @@ -173,7 +173,7 @@ When ran, you get the following output: ```bash $ go run main.go "original" is structurally equal to "similar" -"original" is _not_ structurally equal to "similar" +"original" is _not_ structurally equal to "different" ``` ## Combine merge patches From a80ebee1a5303d8061590a7f4382519d4e72ad44 Mon Sep 17 00:00:00 2001 From: Arnaud Rebillout Date: Tue, 17 Dec 2019 17:15:10 +0700 Subject: [PATCH 15/23] Typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c7f87f..0eee74c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # JSON-Patch -`jsonpatch` is a library which provides functionallity for both applying +`jsonpatch` is a library which provides functionality for both applying [RFC6902 JSON patches](http://tools.ietf.org/html/rfc6902) against documents, as well as for calculating & applying [RFC7396 JSON merge patches](https://tools.ietf.org/html/rfc7396). From b02acf2def786060f3dbba6469149eb3d41d2881 Mon Sep 17 00:00:00 2001 From: jackhafner Date: Tue, 24 Mar 2020 19:13:14 -0700 Subject: [PATCH 16/23] Fix Issue #88, add more tests for the Equals() function. --- patch.go | 4 +++ patch_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/patch.go b/patch.go index 0e5034b..e736759 100644 --- a/patch.go +++ b/patch.go @@ -213,6 +213,10 @@ func (n *lazyNode) equal(o *lazyNode) bool { return false } + if (v == nil) != (ov == nil) { + return false + } + if v == nil && ov == nil { continue } diff --git a/patch_test.go b/patch_test.go index 60c044f..2f2566a 100644 --- a/patch_test.go +++ b/patch_test.go @@ -567,28 +567,86 @@ func TestAdd(t *testing.T) { } type EqualityCase struct { + name string a, b string equal bool } var EqualityCases = []EqualityCase{ - EqualityCase{ + { + "ExtraKeyFalse", `{"foo": "bar"}`, `{"foo": "bar", "baz": "qux"}`, false, }, + { + "StripWhitespaceTrue", + `{ + "foo": "bar", + "baz": "qux" + }`, + `{"foo": "bar", "baz": "qux"}`, + true, + }, + { + "KeysOutOfOrderTrue", + `{ + "baz": "qux", + "foo": "bar" + }`, + `{"foo": "bar", "baz": "qux"}`, + true, + }, + { + "ComparingNullFalse", + `{"foo": null}`, + `{"foo": "bar"}`, + false, + }, + { + "ComparingNullTrue", + `{"foo": null}`, + `{"foo": null}`, + true, + }, + { + "ArrayOutOfOrderFalse", + `["foo", "bar", "baz"]`, + `["bar", "baz", "foo"]`, + false, + }, + { + "ArrayTrue", + `["foo", "bar", "baz"]`, + `["foo", "bar", "baz"]`, + true, + }, + { + "NonStringTypesTrue", + `{"int": 6, "bool": true, "float": 7.0, "string": "the_string", "null": null}`, + `{"int": 6, "bool": true, "float": 7.0, "string": "the_string", "null": null}`, + true, + }, + { + "NestedNullFalse", + `{"foo": ["an", "array"], "bar": {"an": "object"}}`, + `{"foo": null, "bar": null}`, + false, + }, } func TestEquality(t *testing.T) { for _, tc := range EqualityCases { - got := Equal([]byte(tc.a), []byte(tc.b)) - if got != tc.equal { - t.Errorf("Expected Equal(%s, %s) to return %t, but got %t", tc.a, tc.b, tc.equal, got) - } + t.Run(tc.name, func(t *testing.T) { + got := Equal([]byte(tc.a), []byte(tc.b)) + if got != tc.equal { + t.Errorf("Expected Equal(%s, %s) to return %t, but got %t", tc.a, tc.b, tc.equal, got) + } - got = Equal([]byte(tc.b), []byte(tc.a)) - if got != tc.equal { - t.Errorf("Expected Equal(%s, %s) to return %t, but got %t", tc.b, tc.a, tc.equal, got) - } + got = Equal([]byte(tc.b), []byte(tc.a)) + if got != tc.equal { + t.Errorf("Expected Equal(%s, %s) to return %t, but got %t", tc.b, tc.a, tc.equal, got) + } + }) } } From 2d02e91ed03708f86b3ce8523c194c13bae54785 Mon Sep 17 00:00:00 2001 From: jackhafner Date: Tue, 24 Mar 2020 19:17:37 -0700 Subject: [PATCH 17/23] added some more Equals() tests --- patch_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/patch_test.go b/patch_test.go index 2f2566a..5cd519d 100644 --- a/patch_test.go +++ b/patch_test.go @@ -633,6 +633,30 @@ var EqualityCases = []EqualityCase{ `{"foo": null, "bar": null}`, false, }, + { + "NullCompareStringFalse", + `"foo"`, + `null`, + false, + }, + { + "NullCompareIntFalse", + `6`, + `null`, + false, + }, + { + "NullCompareFloatFalse", + `6.01`, + `null`, + false, + }, + { + "NullCompareBoolFalse", + `false`, + `null`, + false, + }, } func TestEquality(t *testing.T) { From 44ca1a372a397698271c8e83793f61c92a468718 Mon Sep 17 00:00:00 2001 From: dataturd Date: Wed, 25 Mar 2020 18:14:14 -0700 Subject: [PATCH 18/23] fix little typo in README too --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1ae569..f161b5f 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ When ran, you get the following output: ```bash $ go run main.go patch document: {"height":null,"name":"Jane"} -updated tina doc: {"age":28,"name":"Jane"} +updated alternative doc: {"age":28,"name":"Jane"} ``` ## Create and apply a JSON Patch From 239740074c83cce06f64c4300d73e1b25dddb0e6 Mon Sep 17 00:00:00 2001 From: Benjamin Elder Date: Thu, 26 Mar 2020 14:36:39 -0700 Subject: [PATCH 19/23] copy go sources into v5 --- v5/cmd/json-patch/file_flag.go | 39 ++ v5/cmd/json-patch/main.go | 56 +++ v5/errors.go | 38 ++ v5/go.mod | 5 + v5/go.sum | 2 + v5/merge.go | 381 ++++++++++++++++ v5/merge_test.go | 625 ++++++++++++++++++++++++++ v5/patch.go | 788 +++++++++++++++++++++++++++++++++ v5/patch_test.go | 676 ++++++++++++++++++++++++++++ 9 files changed, 2610 insertions(+) create mode 100644 v5/cmd/json-patch/file_flag.go create mode 100644 v5/cmd/json-patch/main.go create mode 100644 v5/errors.go create mode 100644 v5/go.mod create mode 100644 v5/go.sum create mode 100644 v5/merge.go create mode 100644 v5/merge_test.go create mode 100644 v5/patch.go create mode 100644 v5/patch_test.go diff --git a/v5/cmd/json-patch/file_flag.go b/v5/cmd/json-patch/file_flag.go new file mode 100644 index 0000000..fe69d6e --- /dev/null +++ b/v5/cmd/json-patch/file_flag.go @@ -0,0 +1,39 @@ +package main + +// Borrowed from Concourse: https://github.com/concourse/atc/blob/master/atccmd/file_flag.go + +import ( + "fmt" + "os" + "path/filepath" +) + +// FileFlag is a flag for passing a path to a file on disk. The file is +// expected to be a file, not a directory, that actually exists. +type FileFlag string + +// UnmarshalFlag implements go-flag's Unmarshaler interface +func (f *FileFlag) UnmarshalFlag(value string) error { + stat, err := os.Stat(value) + if err != nil { + return err + } + + if stat.IsDir() { + return fmt.Errorf("path '%s' is a directory, not a file", value) + } + + abs, err := filepath.Abs(value) + if err != nil { + return err + } + + *f = FileFlag(abs) + + return nil +} + +// Path is the path to the file +func (f FileFlag) Path() string { + return string(f) +} diff --git a/v5/cmd/json-patch/main.go b/v5/cmd/json-patch/main.go new file mode 100644 index 0000000..2be1826 --- /dev/null +++ b/v5/cmd/json-patch/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + + jsonpatch "github.com/evanphx/json-patch" + flags "github.com/jessevdk/go-flags" +) + +type opts struct { + PatchFilePaths []FileFlag `long:"patch-file" short:"p" value-name:"PATH" description:"Path to file with one or more operations"` +} + +func main() { + var o opts + _, err := flags.Parse(&o) + if err != nil { + log.Fatalf("error: %s\n", err) + } + + patches := make([]jsonpatch.Patch, len(o.PatchFilePaths)) + + for i, patchFilePath := range o.PatchFilePaths { + var bs []byte + bs, err = ioutil.ReadFile(patchFilePath.Path()) + if err != nil { + log.Fatalf("error reading patch file: %s", err) + } + + var patch jsonpatch.Patch + patch, err = jsonpatch.DecodePatch(bs) + if err != nil { + log.Fatalf("error decoding patch file: %s", err) + } + + patches[i] = patch + } + + doc, err := ioutil.ReadAll(os.Stdin) + if err != nil { + log.Fatalf("error reading from stdin: %s", err) + } + + mdoc := doc + for _, patch := range patches { + mdoc, err = patch.Apply(mdoc) + if err != nil { + log.Fatalf("error applying patch: %s", err) + } + } + + fmt.Printf("%s", mdoc) +} diff --git a/v5/errors.go b/v5/errors.go new file mode 100644 index 0000000..75304b4 --- /dev/null +++ b/v5/errors.go @@ -0,0 +1,38 @@ +package jsonpatch + +import "fmt" + +// AccumulatedCopySizeError is an error type returned when the accumulated size +// increase caused by copy operations in a patch operation has exceeded the +// limit. +type AccumulatedCopySizeError struct { + limit int64 + accumulated int64 +} + +// NewAccumulatedCopySizeError returns an AccumulatedCopySizeError. +func NewAccumulatedCopySizeError(l, a int64) *AccumulatedCopySizeError { + return &AccumulatedCopySizeError{limit: l, accumulated: a} +} + +// Error implements the error interface. +func (a *AccumulatedCopySizeError) Error() string { + return fmt.Sprintf("Unable to complete the copy, the accumulated size increase of copy is %d, exceeding the limit %d", a.accumulated, a.limit) +} + +// ArraySizeError is an error type returned when the array size has exceeded +// the limit. +type ArraySizeError struct { + limit int + size int +} + +// NewArraySizeError returns an ArraySizeError. +func NewArraySizeError(l, s int) *ArraySizeError { + return &ArraySizeError{limit: l, size: s} +} + +// Error implements the error interface. +func (a *ArraySizeError) Error() string { + return fmt.Sprintf("Unable to create array of size %d, limit is %d", a.size, a.limit) +} diff --git a/v5/go.mod b/v5/go.mod new file mode 100644 index 0000000..a858cab --- /dev/null +++ b/v5/go.mod @@ -0,0 +1,5 @@ +module github.com/evanphx/json-patch + +go 1.12 + +require github.com/pkg/errors v0.8.1 diff --git a/v5/go.sum b/v5/go.sum new file mode 100644 index 0000000..f29ab35 --- /dev/null +++ b/v5/go.sum @@ -0,0 +1,2 @@ +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/v5/merge.go b/v5/merge.go new file mode 100644 index 0000000..5dc6d34 --- /dev/null +++ b/v5/merge.go @@ -0,0 +1,381 @@ +package jsonpatch + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" +) + +func merge(cur, patch *lazyNode, mergeMerge bool) *lazyNode { + curDoc, err := cur.intoDoc() + + if err != nil { + pruneNulls(patch) + return patch + } + + patchDoc, err := patch.intoDoc() + + if err != nil { + return patch + } + + mergeDocs(curDoc, patchDoc, mergeMerge) + + return cur +} + +func mergeDocs(doc, patch *partialDoc, mergeMerge bool) { + for k, v := range *patch { + if v == nil { + if mergeMerge { + (*doc)[k] = nil + } else { + delete(*doc, k) + } + } else { + cur, ok := (*doc)[k] + + if !ok || cur == nil { + pruneNulls(v) + (*doc)[k] = v + } else { + (*doc)[k] = merge(cur, v, mergeMerge) + } + } + } +} + +func pruneNulls(n *lazyNode) { + sub, err := n.intoDoc() + + if err == nil { + pruneDocNulls(sub) + } else { + ary, err := n.intoAry() + + if err == nil { + pruneAryNulls(ary) + } + } +} + +func pruneDocNulls(doc *partialDoc) *partialDoc { + for k, v := range *doc { + if v == nil { + delete(*doc, k) + } else { + pruneNulls(v) + } + } + + return doc +} + +func pruneAryNulls(ary *partialArray) *partialArray { + newAry := []*lazyNode{} + + for _, v := range *ary { + if v != nil { + pruneNulls(v) + newAry = append(newAry, v) + } + } + + *ary = newAry + + return ary +} + +var errBadJSONDoc = fmt.Errorf("Invalid JSON Document") +var errBadJSONPatch = fmt.Errorf("Invalid JSON Patch") +var errBadMergeTypes = fmt.Errorf("Mismatched JSON Documents") + +// MergeMergePatches merges two merge patches together, such that +// applying this resulting merged merge patch to a document yields the same +// as merging each merge patch to the document in succession. +func MergeMergePatches(patch1Data, patch2Data []byte) ([]byte, error) { + return doMergePatch(patch1Data, patch2Data, true) +} + +// MergePatch merges the patchData into the docData. +func MergePatch(docData, patchData []byte) ([]byte, error) { + return doMergePatch(docData, patchData, false) +} + +func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) { + doc := &partialDoc{} + + docErr := json.Unmarshal(docData, doc) + + patch := &partialDoc{} + + patchErr := json.Unmarshal(patchData, patch) + + if _, ok := docErr.(*json.SyntaxError); ok { + return nil, errBadJSONDoc + } + + if _, ok := patchErr.(*json.SyntaxError); ok { + return nil, errBadJSONPatch + } + + if docErr == nil && *doc == nil { + return nil, errBadJSONDoc + } + + if patchErr == nil && *patch == nil { + return nil, errBadJSONPatch + } + + if docErr != nil || patchErr != nil { + // Not an error, just not a doc, so we turn straight into the patch + if patchErr == nil { + if mergeMerge { + doc = patch + } else { + doc = pruneDocNulls(patch) + } + } else { + patchAry := &partialArray{} + patchErr = json.Unmarshal(patchData, patchAry) + + if patchErr != nil { + return nil, errBadJSONPatch + } + + pruneAryNulls(patchAry) + + out, patchErr := json.Marshal(patchAry) + + if patchErr != nil { + return nil, errBadJSONPatch + } + + return out, nil + } + } else { + mergeDocs(doc, patch, mergeMerge) + } + + return json.Marshal(doc) +} + +// resemblesJSONArray indicates whether the byte-slice "appears" to be +// a JSON array or not. +// False-positives are possible, as this function does not check the internal +// structure of the array. It only checks that the outer syntax is present and +// correct. +func resemblesJSONArray(input []byte) bool { + input = bytes.TrimSpace(input) + + hasPrefix := bytes.HasPrefix(input, []byte("[")) + hasSuffix := bytes.HasSuffix(input, []byte("]")) + + return hasPrefix && hasSuffix +} + +// CreateMergePatch will return a merge patch document capable of converting +// the original document(s) to the modified document(s). +// The parameters can be bytes of either two JSON Documents, or two arrays of +// JSON documents. +// The merge patch returned follows the specification defined at http://tools.ietf.org/html/draft-ietf-appsawg-json-merge-patch-07 +func CreateMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) { + originalResemblesArray := resemblesJSONArray(originalJSON) + modifiedResemblesArray := resemblesJSONArray(modifiedJSON) + + // Do both byte-slices seem like JSON arrays? + if originalResemblesArray && modifiedResemblesArray { + return createArrayMergePatch(originalJSON, modifiedJSON) + } + + // Are both byte-slices are not arrays? Then they are likely JSON objects... + if !originalResemblesArray && !modifiedResemblesArray { + return createObjectMergePatch(originalJSON, modifiedJSON) + } + + // None of the above? Then return an error because of mismatched types. + return nil, errBadMergeTypes +} + +// createObjectMergePatch will return a merge-patch document capable of +// converting the original document to the modified document. +func createObjectMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) { + originalDoc := map[string]interface{}{} + modifiedDoc := map[string]interface{}{} + + err := json.Unmarshal(originalJSON, &originalDoc) + if err != nil { + return nil, errBadJSONDoc + } + + err = json.Unmarshal(modifiedJSON, &modifiedDoc) + if err != nil { + return nil, errBadJSONDoc + } + + dest, err := getDiff(originalDoc, modifiedDoc) + if err != nil { + return nil, err + } + + return json.Marshal(dest) +} + +// createArrayMergePatch will return an array of merge-patch documents capable +// of converting the original document to the modified document for each +// pair of JSON documents provided in the arrays. +// Arrays of mismatched sizes will result in an error. +func createArrayMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) { + originalDocs := []json.RawMessage{} + modifiedDocs := []json.RawMessage{} + + err := json.Unmarshal(originalJSON, &originalDocs) + if err != nil { + return nil, errBadJSONDoc + } + + err = json.Unmarshal(modifiedJSON, &modifiedDocs) + if err != nil { + return nil, errBadJSONDoc + } + + total := len(originalDocs) + if len(modifiedDocs) != total { + return nil, errBadJSONDoc + } + + result := []json.RawMessage{} + for i := 0; i < len(originalDocs); i++ { + original := originalDocs[i] + modified := modifiedDocs[i] + + patch, err := createObjectMergePatch(original, modified) + if err != nil { + return nil, err + } + + result = append(result, json.RawMessage(patch)) + } + + return json.Marshal(result) +} + +// Returns true if the array matches (must be json types). +// As is idiomatic for go, an empty array is not the same as a nil array. +func matchesArray(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + if (a == nil && b != nil) || (a != nil && b == nil) { + return false + } + for i := range a { + if !matchesValue(a[i], b[i]) { + return false + } + } + return true +} + +// Returns true if the values matches (must be json types) +// The types of the values must match, otherwise it will always return false +// If two map[string]interface{} are given, all elements must match. +func matchesValue(av, bv interface{}) bool { + if reflect.TypeOf(av) != reflect.TypeOf(bv) { + return false + } + switch at := av.(type) { + case string: + bt := bv.(string) + if bt == at { + return true + } + case float64: + bt := bv.(float64) + if bt == at { + return true + } + case bool: + bt := bv.(bool) + if bt == at { + return true + } + case nil: + // Both nil, fine. + return true + case map[string]interface{}: + bt := bv.(map[string]interface{}) + if len(bt) != len(at) { + return false + } + for key := range bt { + if !matchesValue(at[key], bt[key]) { + return false + } + } + return true + case []interface{}: + bt := bv.([]interface{}) + return matchesArray(at, bt) + } + return false +} + +// getDiff returns the (recursive) difference between a and b as a map[string]interface{}. +func getDiff(a, b map[string]interface{}) (map[string]interface{}, error) { + into := map[string]interface{}{} + for key, bv := range b { + av, ok := a[key] + // value was added + if !ok { + into[key] = bv + continue + } + // If types have changed, replace completely + if reflect.TypeOf(av) != reflect.TypeOf(bv) { + into[key] = bv + continue + } + // Types are the same, compare values + switch at := av.(type) { + case map[string]interface{}: + bt := bv.(map[string]interface{}) + dst := make(map[string]interface{}, len(bt)) + dst, err := getDiff(at, bt) + if err != nil { + return nil, err + } + if len(dst) > 0 { + into[key] = dst + } + case string, float64, bool: + if !matchesValue(av, bv) { + into[key] = bv + } + case []interface{}: + bt := bv.([]interface{}) + if !matchesArray(at, bt) { + into[key] = bv + } + case nil: + switch bv.(type) { + case nil: + // Both nil, fine. + default: + into[key] = bv + } + default: + panic(fmt.Sprintf("Unknown type:%T in key %s", av, key)) + } + } + // Now add all deleted values as nil + for key := range a { + _, found := b[key] + if !found { + into[key] = nil + } + } + return into, nil +} diff --git a/v5/merge_test.go b/v5/merge_test.go new file mode 100644 index 0000000..c87b078 --- /dev/null +++ b/v5/merge_test.go @@ -0,0 +1,625 @@ +package jsonpatch + +import ( + "fmt" + "strings" + "testing" +) + +func mergePatch(doc, patch string) string { + out, err := MergePatch([]byte(doc), []byte(patch)) + + if err != nil { + panic(err) + } + + return string(out) +} + +func TestMergePatchReplaceKey(t *testing.T) { + doc := `{ "title": "hello" }` + pat := `{ "title": "goodbye" }` + + res := mergePatch(doc, pat) + + if !compareJSON(pat, res) { + t.Fatalf("Key was not replaced") + } +} + +func TestMergePatchIgnoresOtherValues(t *testing.T) { + doc := `{ "title": "hello", "age": 18 }` + pat := `{ "title": "goodbye" }` + + res := mergePatch(doc, pat) + + exp := `{ "title": "goodbye", "age": 18 }` + + if !compareJSON(exp, res) { + t.Fatalf("Key was not replaced") + } +} + +func TestMergePatchNilDoc(t *testing.T) { + doc := `{ "title": null }` + pat := `{ "title": {"foo": "bar"} }` + + res := mergePatch(doc, pat) + + exp := `{ "title": {"foo": "bar"} }` + + if !compareJSON(exp, res) { + t.Fatalf("Key was not replaced") + } +} + +func TestMergePatchRecursesIntoObjects(t *testing.T) { + doc := `{ "person": { "title": "hello", "age": 18 } }` + pat := `{ "person": { "title": "goodbye" } }` + + res := mergePatch(doc, pat) + + exp := `{ "person": { "title": "goodbye", "age": 18 } }` + + if !compareJSON(exp, res) { + t.Fatalf("Key was not replaced") + } +} + +type nonObjectCases struct { + doc, pat, res string +} + +func TestMergePatchReplacesNonObjectsWholesale(t *testing.T) { + a1 := `[1]` + a2 := `[2]` + o1 := `{ "a": 1 }` + o2 := `{ "a": 2 }` + o3 := `{ "a": 1, "b": 1 }` + o4 := `{ "a": 2, "b": 1 }` + + cases := []nonObjectCases{ + {a1, a2, a2}, + {o1, a2, a2}, + {a1, o1, o1}, + {o3, o2, o4}, + } + + for _, c := range cases { + act := mergePatch(c.doc, c.pat) + + if !compareJSON(c.res, act) { + t.Errorf("whole object replacement failed") + } + } +} + +func TestMergePatchReturnsErrorOnBadJSON(t *testing.T) { + _, err := MergePatch([]byte(`[[[[`), []byte(`1`)) + + if err == nil { + t.Errorf("Did not return an error for bad json: %s", err) + } + + _, err = MergePatch([]byte(`1`), []byte(`[[[[`)) + + if err == nil { + t.Errorf("Did not return an error for bad json: %s", err) + } +} + +func TestMergePatchReturnsEmptyArrayOnEmptyArray(t *testing.T) { + doc := `{ "array": ["one", "two"] }` + pat := `{ "array": [] }` + + exp := `{ "array": [] }` + + res, err := MergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(exp, string(res)) { + t.Fatalf("Emtpy array did not return not return as empty array") + } +} + +var rfcTests = []struct { + target string + patch string + expected string +}{ + // test cases from https://tools.ietf.org/html/rfc7386#appendix-A + {target: `{"a":"b"}`, patch: `{"a":"c"}`, expected: `{"a":"c"}`}, + {target: `{"a":"b"}`, patch: `{"b":"c"}`, expected: `{"a":"b","b":"c"}`}, + {target: `{"a":"b"}`, patch: `{"a":null}`, expected: `{}`}, + {target: `{"a":"b","b":"c"}`, patch: `{"a":null}`, expected: `{"b":"c"}`}, + {target: `{"a":["b"]}`, patch: `{"a":"c"}`, expected: `{"a":"c"}`}, + {target: `{"a":"c"}`, patch: `{"a":["b"]}`, expected: `{"a":["b"]}`}, + {target: `{"a":{"b": "c"}}`, patch: `{"a": {"b": "d","c": null}}`, expected: `{"a":{"b":"d"}}`}, + {target: `{"a":[{"b":"c"}]}`, patch: `{"a":[1]}`, expected: `{"a":[1]}`}, + {target: `["a","b"]`, patch: `["c","d"]`, expected: `["c","d"]`}, + {target: `{"a":"b"}`, patch: `["c"]`, expected: `["c"]`}, + // {target: `{"a":"foo"}`, patch: `null`, expected: `null`}, + // {target: `{"a":"foo"}`, patch: `"bar"`, expected: `"bar"`}, + {target: `{"e":null}`, patch: `{"a":1}`, expected: `{"a":1,"e":null}`}, + {target: `[1,2]`, patch: `{"a":"b","c":null}`, expected: `{"a":"b"}`}, + {target: `{}`, patch: `{"a":{"bb":{"ccc":null}}}`, expected: `{"a":{"bb":{}}}`}, +} + +func TestMergePatchRFCCases(t *testing.T) { + for i, c := range rfcTests { + out := mergePatch(c.target, c.patch) + + if !compareJSON(out, c.expected) { + t.Errorf("case[%d], patch '%s' did not apply properly to '%s'. expected:\n'%s'\ngot:\n'%s'", i, c.patch, c.target, c.expected, out) + } + } +} + +var rfcFailTests = ` + {"a":"foo"} | null + {"a":"foo"} | "bar" +` + +func TestMergePatchFailRFCCases(t *testing.T) { + tests := strings.Split(rfcFailTests, "\n") + + for _, c := range tests { + if strings.TrimSpace(c) == "" { + continue + } + + parts := strings.SplitN(c, "|", 2) + + doc := strings.TrimSpace(parts[0]) + pat := strings.TrimSpace(parts[1]) + + out, err := MergePatch([]byte(doc), []byte(pat)) + + if err != errBadJSONPatch { + t.Errorf("error not returned properly: %s, %s", err, string(out)) + } + } + +} + +func TestResembleJSONArray(t *testing.T) { + testCases := []struct { + input []byte + expected bool + }{ + // Failure cases + {input: []byte(``), expected: false}, + {input: []byte(`not an array`), expected: false}, + {input: []byte(`{"foo": "bar"}`), expected: false}, + {input: []byte(`{"fizz": ["buzz"]}`), expected: false}, + {input: []byte(`[bad suffix`), expected: false}, + {input: []byte(`bad prefix]`), expected: false}, + {input: []byte(`][`), expected: false}, + + // Valid cases + {input: []byte(`[]`), expected: true}, + {input: []byte(`["foo", "bar"]`), expected: true}, + {input: []byte(`[["foo", "bar"]]`), expected: true}, + {input: []byte(`[not valid syntax]`), expected: true}, + + // Valid cases with whitespace + {input: []byte(` []`), expected: true}, + {input: []byte(`[] `), expected: true}, + {input: []byte(` [] `), expected: true}, + {input: []byte(` [ ] `), expected: true}, + {input: []byte("\t[]"), expected: true}, + {input: []byte("[]\n"), expected: true}, + {input: []byte("\n\t\r[]"), expected: true}, + } + + for _, test := range testCases { + result := resemblesJSONArray(test.input) + if result != test.expected { + t.Errorf( + `expected "%t" but received "%t" for case: "%s"`, + test.expected, + result, + string(test.input), + ) + } + } +} + +func TestCreateMergePatchReplaceKey(t *testing.T) { + doc := `{ "title": "hello", "nested": {"one": 1, "two": 2} }` + pat := `{ "title": "goodbye", "nested": {"one": 2, "two": 2} }` + + exp := `{ "title": "goodbye", "nested": {"one": 2} }` + + res, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(exp, string(res)) { + t.Fatalf("Key was not replaced") + } +} + +func TestCreateMergePatchGetArray(t *testing.T) { + doc := `{ "title": "hello", "array": ["one", "two"], "notmatch": [1, 2, 3] }` + pat := `{ "title": "hello", "array": ["one", "two", "three"], "notmatch": [1, 2, 3] }` + + exp := `{ "array": ["one", "two", "three"] }` + + res, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(exp, string(res)) { + t.Fatalf("Array was not added") + } +} + +func TestCreateMergePatchGetObjArray(t *testing.T) { + doc := `{ "title": "hello", "array": [{"banana": true}, {"evil": false}], "notmatch": [{"one":1}, {"two":2}, {"three":3}] }` + pat := `{ "title": "hello", "array": [{"banana": false}, {"evil": true}], "notmatch": [{"one":1}, {"two":2}, {"three":3}] }` + + exp := `{ "array": [{"banana": false}, {"evil": true}] }` + + res, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(exp, string(res)) { + t.Fatalf("Object array was not added") + } +} + +func TestCreateMergePatchDeleteKey(t *testing.T) { + doc := `{ "title": "hello", "nested": {"one": 1, "two": 2} }` + pat := `{ "title": "hello", "nested": {"one": 1} }` + + exp := `{"nested":{"two":null}}` + + res, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + // We cannot use "compareJSON", since Equals does not report a difference if the value is null + if exp != string(res) { + t.Fatalf("Key was not removed") + } +} + +func TestCreateMergePatchEmptyArray(t *testing.T) { + doc := `{ "array": null }` + pat := `{ "array": [] }` + + exp := `{"array":[]}` + + res, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + // We cannot use "compareJSON", since Equals does not report a difference if the value is null + if exp != string(res) { + t.Fatalf("Key was not removed") + } +} + +func TestCreateMergePatchNil(t *testing.T) { + doc := `{ "title": "hello", "nested": {"one": 1, "two": [{"one":null}, {"two":null}, {"three":null}]} }` + pat := doc + + exp := `{}` + + res, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(exp, string(res)) { + t.Fatalf("Object array was not added") + } +} + +func TestCreateMergePatchObjArray(t *testing.T) { + doc := `{ "array": [ {"a": {"b": 2}}, {"a": {"b": 3}} ]}` + exp := `{}` + + res, err := CreateMergePatch([]byte(doc), []byte(doc)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + // We cannot use "compareJSON", since Equals does not report a difference if the value is null + if exp != string(res) { + t.Fatalf("Array was not empty, was " + string(res)) + } +} + +func TestCreateMergePatchSameOuterArray(t *testing.T) { + doc := `[{"foo": "bar"}]` + pat := doc + exp := `[{}]` + + res, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(exp, string(res)) { + t.Fatalf("Outer array was not unmodified") + } +} + +func TestCreateMergePatchModifiedOuterArray(t *testing.T) { + doc := `[{"name": "John"}, {"name": "Will"}]` + pat := `[{"name": "Jane"}, {"name": "Will"}]` + exp := `[{"name": "Jane"}, {}]` + + res, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(exp, string(res)) { + t.Fatalf("Expected %s but received %s", exp, res) + } +} + +func TestCreateMergePatchMismatchedOuterArray(t *testing.T) { + doc := `[{"name": "John"}, {"name": "Will"}]` + pat := `[{"name": "Jane"}]` + + _, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err == nil { + t.Errorf("Expected error due to array length differences but received none") + } +} + +func TestCreateMergePatchMismatchedOuterTypes(t *testing.T) { + doc := `[{"name": "John"}]` + pat := `{"name": "Jane"}` + + _, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err == nil { + t.Errorf("Expected error due to mismatched types but received none") + } +} + +func TestCreateMergePatchNoDifferences(t *testing.T) { + doc := `{ "title": "hello", "nested": {"one": 1, "two": 2} }` + pat := doc + + exp := `{}` + + res, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(exp, string(res)) { + t.Fatalf("Key was not replaced") + } +} + +func TestCreateMergePatchComplexMatch(t *testing.T) { + doc := `{"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4], "nested": {"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4]} }` + empty := `{}` + res, err := CreateMergePatch([]byte(doc), []byte(doc)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + // We cannot use "compareJSON", since Equals does not report a difference if the value is null + if empty != string(res) { + t.Fatalf("Did not get empty result, was:%s", string(res)) + } +} + +func TestCreateMergePatchComplexAddAll(t *testing.T) { + doc := `{"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4], "nested": {"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4]} }` + empty := `{}` + res, err := CreateMergePatch([]byte(empty), []byte(doc)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(doc, string(res)) { + t.Fatalf("Did not get everything as, it was:\n%s", string(res)) + } +} + +// createNestedMap created a series of nested map objects such that the number of +// objects is roughly 2^n (precisely, 2^(n+1)-1). +func createNestedMap(m map[string]interface{}, depth int, objectCount *int) { + if depth == 0 { + return + } + for i := 0; i< 2;i++ { + nested := map[string]interface{}{} + *objectCount += 1 + createNestedMap(nested, depth-1, objectCount) + m[fmt.Sprintf("key-%v", i)] = nested + } +} + +func benchmarkMatchesValueWithDeeplyNestedFields(depth int, b *testing.B) { + a := map[string]interface{}{} + objCount := 1 + createNestedMap(a, depth, &objCount) + b.ResetTimer() + b.Run(fmt.Sprintf("objectCount=%v", objCount), func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !matchesValue(a, a) { + b.Errorf("Should be equal") + } + } + }) +} + +func BenchmarkMatchesValue1(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(1, b) } +func BenchmarkMatchesValue2(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(2, b) } +func BenchmarkMatchesValue3(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(3, b) } +func BenchmarkMatchesValue4(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(4, b) } +func BenchmarkMatchesValue5(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(5, b) } +func BenchmarkMatchesValue6(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(6, b) } +func BenchmarkMatchesValue7(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(7, b) } +func BenchmarkMatchesValue8(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(8, b) } +func BenchmarkMatchesValue9(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(9, b) } +func BenchmarkMatchesValue10(b *testing.B) { benchmarkMatchesValueWithDeeplyNestedFields(10, b) } + +func TestCreateMergePatchComplexRemoveAll(t *testing.T) { + doc := `{"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4], "nested": {"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4]} }` + exp := `{"a":null,"f":null,"hello":null,"i":null,"n":null,"nested":null,"pi":null,"t":null}` + empty := `{}` + res, err := CreateMergePatch([]byte(doc), []byte(empty)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if exp != string(res) { + t.Fatalf("Did not get result, was:%s", string(res)) + } + + // FIXME: Crashes if using compareJSON like this: + /* + if !compareJSON(doc, string(res)) { + t.Fatalf("Did not get everything as, it was:\n%s", string(res)) + } + */ +} + +func TestCreateMergePatchObjectWithInnerArray(t *testing.T) { + stateString := `{ + "OuterArray": [ + { + "InnerArray": [ + { + "StringAttr": "abc123" + } + ], + "StringAttr": "def456" + } + ] + }` + + patch, err := CreateMergePatch([]byte(stateString), []byte(stateString)) + if err != nil { + t.Fatal(err) + } + + if string(patch) != "{}" { + t.Fatalf("Patch should have been {} but was: %v", string(patch)) + } +} + +func TestCreateMergePatchReplaceKeyNotEscape(t *testing.T) { + doc := `{ "title": "hello", "nested": {"title/escaped": 1, "two": 2} }` + pat := `{ "title": "goodbye", "nested": {"title/escaped": 2, "two": 2} }` + + exp := `{ "title": "goodbye", "nested": {"title/escaped": 2} }` + + res, err := CreateMergePatch([]byte(doc), []byte(pat)) + + if err != nil { + t.Errorf("Unexpected error: %s, %s", err, string(res)) + } + + if !compareJSON(exp, string(res)) { + t.Log(string(res)) + t.Fatalf("Key was not replaced") + } +} + +func TestMergePatchReplaceKeyNotEscaping(t *testing.T) { + doc := `{ "obj": { "title/escaped": "hello" } }` + pat := `{ "obj": { "title/escaped": "goodbye" } }` + exp := `{ "obj": { "title/escaped": "goodbye" } }` + + res := mergePatch(doc, pat) + + if !compareJSON(exp, res) { + t.Fatalf("Key was not replaced") + } +} + +func TestMergeMergePatches(t *testing.T) { + cases := []struct { + demonstrates string + p1 string + p2 string + exp string + }{ + { + demonstrates: "simple patches are merged normally", + p1: `{"add1": 1}`, + p2: `{"add2": 2}`, + exp: `{"add1": 1, "add2": 2}`, + }, + { + demonstrates: "nulls are kept", + p1: `{"del1": null}`, + p2: `{"del2": null}`, + exp: `{"del1": null, "del2": null}`, + }, + { + demonstrates: "a key added then deleted is kept deleted", + p1: `{"add_then_delete": "atd"}`, + p2: `{"add_then_delete": null}`, + exp: `{"add_then_delete": null}`, + }, + { + demonstrates: "a key deleted then added is kept added", + p1: `{"delete_then_add": null}`, + p2: `{"delete_then_add": "dta"}`, + exp: `{"delete_then_add": "dta"}`, + }, + { + demonstrates: "object overrides array", + p1: `[]`, + p2: `{"del": null, "add": "a"}`, + exp: `{"del": null, "add": "a"}`, + }, + { + demonstrates: "array overrides object", + p1: `{"del": null, "add": "a"}`, + p2: `[]`, + exp: `[]`, + }, + } + + for _, c := range cases { + out, err := MergeMergePatches([]byte(c.p1), []byte(c.p2)) + + if err != nil { + panic(err) + } + + if !compareJSON(c.exp, string(out)) { + t.Logf("Error while trying to demonstrate: %v", c.demonstrates) + t.Logf("Got %v", string(out)) + t.Logf("Expected %v", c.exp) + t.Fatalf("Merged merge patch is incorrect") + } + } +} diff --git a/v5/patch.go b/v5/patch.go new file mode 100644 index 0000000..e736759 --- /dev/null +++ b/v5/patch.go @@ -0,0 +1,788 @@ +package jsonpatch + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +const ( + eRaw = iota + eDoc + eAry +) + +var ( + // SupportNegativeIndices decides whether to support non-standard practice of + // allowing negative indices to mean indices starting at the end of an array. + // Default to true. + SupportNegativeIndices bool = true + // AccumulatedCopySizeLimit limits the total size increase in bytes caused by + // "copy" operations in a patch. + AccumulatedCopySizeLimit int64 = 0 +) + +var ( + ErrTestFailed = errors.New("test failed") + ErrMissing = errors.New("missing value") + ErrUnknownType = errors.New("unknown object type") + ErrInvalid = errors.New("invalid state detected") + ErrInvalidIndex = errors.New("invalid index referenced") +) + +type lazyNode struct { + raw *json.RawMessage + doc partialDoc + ary partialArray + which int +} + +// Operation is a single JSON-Patch step, such as a single 'add' operation. +type Operation map[string]*json.RawMessage + +// Patch is an ordered collection of Operations. +type Patch []Operation + +type partialDoc map[string]*lazyNode +type partialArray []*lazyNode + +type container interface { + get(key string) (*lazyNode, error) + set(key string, val *lazyNode) error + add(key string, val *lazyNode) error + remove(key string) error +} + +func newLazyNode(raw *json.RawMessage) *lazyNode { + return &lazyNode{raw: raw, doc: nil, ary: nil, which: eRaw} +} + +func (n *lazyNode) MarshalJSON() ([]byte, error) { + switch n.which { + case eRaw: + return json.Marshal(n.raw) + case eDoc: + return json.Marshal(n.doc) + case eAry: + return json.Marshal(n.ary) + default: + return nil, ErrUnknownType + } +} + +func (n *lazyNode) UnmarshalJSON(data []byte) error { + dest := make(json.RawMessage, len(data)) + copy(dest, data) + n.raw = &dest + n.which = eRaw + return nil +} + +func deepCopy(src *lazyNode) (*lazyNode, int, error) { + if src == nil { + return nil, 0, nil + } + a, err := src.MarshalJSON() + if err != nil { + return nil, 0, err + } + sz := len(a) + ra := make(json.RawMessage, sz) + copy(ra, a) + return newLazyNode(&ra), sz, nil +} + +func (n *lazyNode) intoDoc() (*partialDoc, error) { + if n.which == eDoc { + return &n.doc, nil + } + + if n.raw == nil { + return nil, ErrInvalid + } + + err := json.Unmarshal(*n.raw, &n.doc) + + if err != nil { + return nil, err + } + + n.which = eDoc + return &n.doc, nil +} + +func (n *lazyNode) intoAry() (*partialArray, error) { + if n.which == eAry { + return &n.ary, nil + } + + if n.raw == nil { + return nil, ErrInvalid + } + + err := json.Unmarshal(*n.raw, &n.ary) + + if err != nil { + return nil, err + } + + n.which = eAry + return &n.ary, nil +} + +func (n *lazyNode) compact() []byte { + buf := &bytes.Buffer{} + + if n.raw == nil { + return nil + } + + err := json.Compact(buf, *n.raw) + + if err != nil { + return *n.raw + } + + return buf.Bytes() +} + +func (n *lazyNode) tryDoc() bool { + if n.raw == nil { + return false + } + + err := json.Unmarshal(*n.raw, &n.doc) + + if err != nil { + return false + } + + n.which = eDoc + return true +} + +func (n *lazyNode) tryAry() bool { + if n.raw == nil { + return false + } + + err := json.Unmarshal(*n.raw, &n.ary) + + if err != nil { + return false + } + + n.which = eAry + return true +} + +func (n *lazyNode) equal(o *lazyNode) bool { + if n.which == eRaw { + if !n.tryDoc() && !n.tryAry() { + if o.which != eRaw { + return false + } + + return bytes.Equal(n.compact(), o.compact()) + } + } + + if n.which == eDoc { + if o.which == eRaw { + if !o.tryDoc() { + return false + } + } + + if o.which != eDoc { + return false + } + + if len(n.doc) != len(o.doc) { + return false + } + + for k, v := range n.doc { + ov, ok := o.doc[k] + + if !ok { + return false + } + + if (v == nil) != (ov == nil) { + return false + } + + if v == nil && ov == nil { + continue + } + + if !v.equal(ov) { + return false + } + } + + return true + } + + if o.which != eAry && !o.tryAry() { + return false + } + + if len(n.ary) != len(o.ary) { + return false + } + + for idx, val := range n.ary { + if !val.equal(o.ary[idx]) { + return false + } + } + + return true +} + +// Kind reads the "op" field of the Operation. +func (o Operation) Kind() string { + if obj, ok := o["op"]; ok && obj != nil { + var op string + + err := json.Unmarshal(*obj, &op) + + if err != nil { + return "unknown" + } + + return op + } + + return "unknown" +} + +// Path reads the "path" field of the Operation. +func (o Operation) Path() (string, error) { + if obj, ok := o["path"]; ok && obj != nil { + var op string + + err := json.Unmarshal(*obj, &op) + + if err != nil { + return "unknown", err + } + + return op, nil + } + + return "unknown", errors.Wrapf(ErrMissing, "operation missing path field") +} + +// From reads the "from" field of the Operation. +func (o Operation) From() (string, error) { + if obj, ok := o["from"]; ok && obj != nil { + var op string + + err := json.Unmarshal(*obj, &op) + + if err != nil { + return "unknown", err + } + + return op, nil + } + + return "unknown", errors.Wrapf(ErrMissing, "operation, missing from field") +} + +func (o Operation) value() *lazyNode { + if obj, ok := o["value"]; ok { + return newLazyNode(obj) + } + + return nil +} + +// ValueInterface decodes the operation value into an interface. +func (o Operation) ValueInterface() (interface{}, error) { + if obj, ok := o["value"]; ok && obj != nil { + var v interface{} + + err := json.Unmarshal(*obj, &v) + + if err != nil { + return nil, err + } + + return v, nil + } + + return nil, errors.Wrapf(ErrMissing, "operation, missing value field") +} + +func isArray(buf []byte) bool { +Loop: + for _, c := range buf { + switch c { + case ' ': + case '\n': + case '\t': + continue + case '[': + return true + default: + break Loop + } + } + + return false +} + +func findObject(pd *container, path string) (container, string) { + doc := *pd + + split := strings.Split(path, "/") + + if len(split) < 2 { + return nil, "" + } + + parts := split[1 : len(split)-1] + + key := split[len(split)-1] + + var err error + + for _, part := range parts { + + next, ok := doc.get(decodePatchKey(part)) + + if next == nil || ok != nil { + return nil, "" + } + + if isArray(*next.raw) { + doc, err = next.intoAry() + + if err != nil { + return nil, "" + } + } else { + doc, err = next.intoDoc() + + if err != nil { + return nil, "" + } + } + } + + return doc, decodePatchKey(key) +} + +func (d *partialDoc) set(key string, val *lazyNode) error { + (*d)[key] = val + return nil +} + +func (d *partialDoc) add(key string, val *lazyNode) error { + (*d)[key] = val + return nil +} + +func (d *partialDoc) get(key string) (*lazyNode, error) { + v, ok := (*d)[key] + if !ok { + return v, errors.Wrapf(ErrMissing, "unable to get nonexistent key: %s", key) + } + return v, nil +} + +func (d *partialDoc) remove(key string) error { + _, ok := (*d)[key] + if !ok { + return errors.Wrapf(ErrMissing, "unable to remove nonexistent key: %s", key) + } + + delete(*d, key) + return nil +} + +// set should only be used to implement the "replace" operation, so "key" must +// be an already existing index in "d". +func (d *partialArray) set(key string, val *lazyNode) error { + idx, err := strconv.Atoi(key) + if err != nil { + return err + } + (*d)[idx] = val + return nil +} + +func (d *partialArray) add(key string, val *lazyNode) error { + if key == "-" { + *d = append(*d, val) + return nil + } + + idx, err := strconv.Atoi(key) + if err != nil { + return errors.Wrapf(err, "value was not a proper array index: '%s'", key) + } + + sz := len(*d) + 1 + + ary := make([]*lazyNode, sz) + + cur := *d + + if idx >= len(ary) { + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) + } + + if idx < 0 { + if !SupportNegativeIndices { + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) + } + if idx < -len(ary) { + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) + } + idx += len(ary) + } + + copy(ary[0:idx], cur[0:idx]) + ary[idx] = val + copy(ary[idx+1:], cur[idx:]) + + *d = ary + return nil +} + +func (d *partialArray) get(key string) (*lazyNode, error) { + idx, err := strconv.Atoi(key) + + if err != nil { + return nil, err + } + + if idx >= len(*d) { + return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) + } + + return (*d)[idx], nil +} + +func (d *partialArray) remove(key string) error { + idx, err := strconv.Atoi(key) + if err != nil { + return err + } + + cur := *d + + if idx >= len(cur) { + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) + } + + if idx < 0 { + if !SupportNegativeIndices { + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) + } + if idx < -len(cur) { + return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) + } + idx += len(cur) + } + + ary := make([]*lazyNode, len(cur)-1) + + copy(ary[0:idx], cur[0:idx]) + copy(ary[idx:], cur[idx+1:]) + + *d = ary + return nil + +} + +func (p Patch) add(doc *container, op Operation) error { + path, err := op.Path() + if err != nil { + return errors.Wrapf(ErrMissing, "add operation failed to decode path") + } + + con, key := findObject(doc, path) + + if con == nil { + return errors.Wrapf(ErrMissing, "add operation does not apply: doc is missing path: \"%s\"", path) + } + + err = con.add(key, op.value()) + if err != nil { + return errors.Wrapf(err, "error in add for path: '%s'", path) + } + + return nil +} + +func (p Patch) remove(doc *container, op Operation) error { + path, err := op.Path() + if err != nil { + return errors.Wrapf(ErrMissing, "remove operation failed to decode path") + } + + con, key := findObject(doc, path) + + if con == nil { + return errors.Wrapf(ErrMissing, "remove operation does not apply: doc is missing path: \"%s\"", path) + } + + err = con.remove(key) + if err != nil { + return errors.Wrapf(err, "error in remove for path: '%s'", path) + } + + return nil +} + +func (p Patch) replace(doc *container, op Operation) error { + path, err := op.Path() + if err != nil { + return errors.Wrapf(err, "replace operation failed to decode path") + } + + con, key := findObject(doc, path) + + if con == nil { + return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing path: %s", path) + } + + _, ok := con.get(key) + if ok != nil { + return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing key: %s", path) + } + + err = con.set(key, op.value()) + if err != nil { + return errors.Wrapf(err, "error in remove for path: '%s'", path) + } + + return nil +} + +func (p Patch) move(doc *container, op Operation) error { + from, err := op.From() + if err != nil { + return errors.Wrapf(err, "move operation failed to decode from") + } + + con, key := findObject(doc, from) + + if con == nil { + return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing from path: %s", from) + } + + val, err := con.get(key) + if err != nil { + return errors.Wrapf(err, "error in move for path: '%s'", key) + } + + err = con.remove(key) + if err != nil { + return errors.Wrapf(err, "error in move for path: '%s'", key) + } + + path, err := op.Path() + if err != nil { + return errors.Wrapf(err, "move operation failed to decode path") + } + + con, key = findObject(doc, path) + + if con == nil { + return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing destination path: %s", path) + } + + err = con.add(key, val) + if err != nil { + return errors.Wrapf(err, "error in move for path: '%s'", path) + } + + return nil +} + +func (p Patch) test(doc *container, op Operation) error { + path, err := op.Path() + if err != nil { + return errors.Wrapf(err, "test operation failed to decode path") + } + + con, key := findObject(doc, path) + + if con == nil { + return errors.Wrapf(ErrMissing, "test operation does not apply: is missing path: %s", path) + } + + val, err := con.get(key) + if err != nil && errors.Cause(err) != ErrMissing { + return errors.Wrapf(err, "error in test for path: '%s'", path) + } + + if val == nil { + if op.value().raw == nil { + return nil + } + return errors.Wrapf(ErrTestFailed, "testing value %s failed", path) + } else if op.value() == nil { + return errors.Wrapf(ErrTestFailed, "testing value %s failed", path) + } + + if val.equal(op.value()) { + return nil + } + + return errors.Wrapf(ErrTestFailed, "testing value %s failed", path) +} + +func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64) error { + from, err := op.From() + if err != nil { + return errors.Wrapf(err, "copy operation failed to decode from") + } + + con, key := findObject(doc, from) + + if con == nil { + return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing from path: %s", from) + } + + val, err := con.get(key) + if err != nil { + return errors.Wrapf(err, "error in copy for from: '%s'", from) + } + + path, err := op.Path() + if err != nil { + return errors.Wrapf(ErrMissing, "copy operation failed to decode path") + } + + con, key = findObject(doc, path) + + if con == nil { + return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing destination path: %s", path) + } + + valCopy, sz, err := deepCopy(val) + if err != nil { + return errors.Wrapf(err, "error while performing deep copy") + } + + (*accumulatedCopySize) += int64(sz) + if AccumulatedCopySizeLimit > 0 && *accumulatedCopySize > AccumulatedCopySizeLimit { + return NewAccumulatedCopySizeError(AccumulatedCopySizeLimit, *accumulatedCopySize) + } + + err = con.add(key, valCopy) + if err != nil { + return errors.Wrapf(err, "error while adding value during copy") + } + + return nil +} + +// Equal indicates if 2 JSON documents have the same structural equality. +func Equal(a, b []byte) bool { + ra := make(json.RawMessage, len(a)) + copy(ra, a) + la := newLazyNode(&ra) + + rb := make(json.RawMessage, len(b)) + copy(rb, b) + lb := newLazyNode(&rb) + + return la.equal(lb) +} + +// DecodePatch decodes the passed JSON document as an RFC 6902 patch. +func DecodePatch(buf []byte) (Patch, error) { + var p Patch + + err := json.Unmarshal(buf, &p) + + if err != nil { + return nil, err + } + + return p, nil +} + +// Apply mutates a JSON document according to the patch, and returns the new +// document. +func (p Patch) Apply(doc []byte) ([]byte, error) { + return p.ApplyIndent(doc, "") +} + +// ApplyIndent mutates a JSON document according to the patch, and returns the new +// document indented. +func (p Patch) ApplyIndent(doc []byte, indent string) ([]byte, error) { + var pd container + if doc[0] == '[' { + pd = &partialArray{} + } else { + pd = &partialDoc{} + } + + err := json.Unmarshal(doc, pd) + + if err != nil { + return nil, err + } + + err = nil + + var accumulatedCopySize int64 + + for _, op := range p { + switch op.Kind() { + case "add": + err = p.add(&pd, op) + case "remove": + err = p.remove(&pd, op) + case "replace": + err = p.replace(&pd, op) + case "move": + err = p.move(&pd, op) + case "test": + err = p.test(&pd, op) + case "copy": + err = p.copy(&pd, op, &accumulatedCopySize) + default: + err = fmt.Errorf("Unexpected kind: %s", op.Kind()) + } + + if err != nil { + return nil, err + } + } + + if indent != "" { + return json.MarshalIndent(pd, "", indent) + } + + return json.Marshal(pd) +} + +// From http://tools.ietf.org/html/rfc6901#section-4 : +// +// Evaluation of each reference token begins by decoding any escaped +// character sequence. This is performed by first transforming any +// occurrence of the sequence '~1' to '/', and then transforming any +// occurrence of the sequence '~0' to '~'. + +var ( + rfc6901Decoder = strings.NewReplacer("~1", "/", "~0", "~") +) + +func decodePatchKey(k string) string { + return rfc6901Decoder.Replace(k) +} diff --git a/v5/patch_test.go b/v5/patch_test.go new file mode 100644 index 0000000..5cd519d --- /dev/null +++ b/v5/patch_test.go @@ -0,0 +1,676 @@ +package jsonpatch + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "testing" +) + +func reformatJSON(j string) string { + buf := new(bytes.Buffer) + + json.Indent(buf, []byte(j), "", " ") + + return buf.String() +} + +func compareJSON(a, b string) bool { + // return Equal([]byte(a), []byte(b)) + + var objA, objB map[string]interface{} + json.Unmarshal([]byte(a), &objA) + json.Unmarshal([]byte(b), &objB) + + // fmt.Printf("Comparing %#v\nagainst %#v\n", objA, objB) + return reflect.DeepEqual(objA, objB) +} + +func applyPatch(doc, patch string) (string, error) { + obj, err := DecodePatch([]byte(patch)) + + if err != nil { + panic(err) + } + + out, err := obj.Apply([]byte(doc)) + + if err != nil { + return "", err + } + + return string(out), nil +} + +type Case struct { + doc, patch, result string +} + +func repeatedA(r int) string { + var s string + for i := 0; i < r; i++ { + s += "A" + } + return s +} + +var Cases = []Case{ + { + `{ "foo": "bar"}`, + `[ + { "op": "add", "path": "/baz", "value": "qux" } + ]`, + `{ + "baz": "qux", + "foo": "bar" + }`, + }, + { + `{ "foo": [ "bar", "baz" ] }`, + `[ + { "op": "add", "path": "/foo/1", "value": "qux" } + ]`, + `{ "foo": [ "bar", "qux", "baz" ] }`, + }, + { + `{ "foo": [ "bar", "baz" ] }`, + `[ + { "op": "add", "path": "/foo/-1", "value": "qux" } + ]`, + `{ "foo": [ "bar", "baz", "qux" ] }`, + }, + { + `{ "baz": "qux", "foo": "bar" }`, + `[ { "op": "remove", "path": "/baz" } ]`, + `{ "foo": "bar" }`, + }, + { + `{ "foo": [ "bar", "qux", "baz" ] }`, + `[ { "op": "remove", "path": "/foo/1" } ]`, + `{ "foo": [ "bar", "baz" ] }`, + }, + { + `{ "baz": "qux", "foo": "bar" }`, + `[ { "op": "replace", "path": "/baz", "value": "boo" } ]`, + `{ "baz": "boo", "foo": "bar" }`, + }, + { + `{ + "foo": { + "bar": "baz", + "waldo": "fred" + }, + "qux": { + "corge": "grault" + } + }`, + `[ { "op": "move", "from": "/foo/waldo", "path": "/qux/thud" } ]`, + `{ + "foo": { + "bar": "baz" + }, + "qux": { + "corge": "grault", + "thud": "fred" + } + }`, + }, + { + `{ "foo": [ "all", "grass", "cows", "eat" ] }`, + `[ { "op": "move", "from": "/foo/1", "path": "/foo/3" } ]`, + `{ "foo": [ "all", "cows", "eat", "grass" ] }`, + }, + { + `{ "foo": [ "all", "grass", "cows", "eat" ] }`, + `[ { "op": "move", "from": "/foo/1", "path": "/foo/2" } ]`, + `{ "foo": [ "all", "cows", "grass", "eat" ] }`, + }, + { + `{ "foo": "bar" }`, + `[ { "op": "add", "path": "/child", "value": { "grandchild": { } } } ]`, + `{ "foo": "bar", "child": { "grandchild": { } } }`, + }, + { + `{ "foo": ["bar"] }`, + `[ { "op": "add", "path": "/foo/-", "value": ["abc", "def"] } ]`, + `{ "foo": ["bar", ["abc", "def"]] }`, + }, + { + `{ "foo": "bar", "qux": { "baz": 1, "bar": null } }`, + `[ { "op": "remove", "path": "/qux/bar" } ]`, + `{ "foo": "bar", "qux": { "baz": 1 } }`, + }, + { + `{ "foo": "bar" }`, + `[ { "op": "add", "path": "/baz", "value": null } ]`, + `{ "baz": null, "foo": "bar" }`, + }, + { + `{ "foo": ["bar"]}`, + `[ { "op": "replace", "path": "/foo/0", "value": "baz"}]`, + `{ "foo": ["baz"]}`, + }, + { + `{ "foo": ["bar","baz"]}`, + `[ { "op": "replace", "path": "/foo/0", "value": "bum"}]`, + `{ "foo": ["bum","baz"]}`, + }, + { + `{ "foo": ["bar","qux","baz"]}`, + `[ { "op": "replace", "path": "/foo/1", "value": "bum"}]`, + `{ "foo": ["bar", "bum","baz"]}`, + }, + { + `[ {"foo": ["bar","qux","baz"]}]`, + `[ { "op": "replace", "path": "/0/foo/0", "value": "bum"}]`, + `[ {"foo": ["bum","qux","baz"]}]`, + }, + { + `[ {"foo": ["bar","qux","baz"], "bar": ["qux","baz"]}]`, + `[ { "op": "copy", "from": "/0/foo/0", "path": "/0/bar/0"}]`, + `[ {"foo": ["bar","qux","baz"], "bar": ["bar", "baz"]}]`, + }, + { + `[ {"foo": ["bar","qux","baz"], "bar": ["qux","baz"]}]`, + `[ { "op": "copy", "from": "/0/foo/0", "path": "/0/bar"}]`, + `[ {"foo": ["bar","qux","baz"], "bar": ["bar", "qux", "baz"]}]`, + }, + { + `[ { "foo": {"bar": ["qux","baz"]}, "baz": {"qux": "bum"}}]`, + `[ { "op": "copy", "from": "/0/foo/bar", "path": "/0/baz/bar"}]`, + `[ { "baz": {"bar": ["qux","baz"], "qux":"bum"}, "foo": {"bar": ["qux","baz"]}}]`, + }, + { + `{ "foo": ["bar"]}`, + `[{"op": "copy", "path": "/foo/0", "from": "/foo"}]`, + `{ "foo": [["bar"], "bar"]}`, + }, + { + `{ "foo": null}`, + `[{"op": "copy", "path": "/bar", "from": "/foo"}]`, + `{ "foo": null, "bar": null}`, + }, + { + `{ "foo": ["bar","qux","baz"]}`, + `[ { "op": "remove", "path": "/foo/-2"}]`, + `{ "foo": ["bar", "baz"]}`, + }, + { + `{ "foo": []}`, + `[ { "op": "add", "path": "/foo/-1", "value": "qux"}]`, + `{ "foo": ["qux"]}`, + }, + { + `{ "bar": [{"baz": null}]}`, + `[ { "op": "replace", "path": "/bar/0/baz", "value": 1 } ]`, + `{ "bar": [{"baz": 1}]}`, + }, + { + `{ "bar": [{"baz": 1}]}`, + `[ { "op": "replace", "path": "/bar/0/baz", "value": null } ]`, + `{ "bar": [{"baz": null}]}`, + }, + { + `{ "bar": [null]}`, + `[ { "op": "replace", "path": "/bar/0", "value": 1 } ]`, + `{ "bar": [1]}`, + }, + { + `{ "bar": [1]}`, + `[ { "op": "replace", "path": "/bar/0", "value": null } ]`, + `{ "bar": [null]}`, + }, + { + fmt.Sprintf(`{ "foo": ["A", %q] }`, repeatedA(48)), + // The wrapping quotes around 'A's are included in the copy + // size, so each copy operation increases the size by 50 bytes. + `[ { "op": "copy", "path": "/foo/-", "from": "/foo/1" }, + { "op": "copy", "path": "/foo/-", "from": "/foo/1" }]`, + fmt.Sprintf(`{ "foo": ["A", %q, %q, %q] }`, repeatedA(48), repeatedA(48), repeatedA(48)), + }, +} + +type BadCase struct { + doc, patch string +} + +var MutationTestCases = []BadCase{ + { + `{ "foo": "bar", "qux": { "baz": 1, "bar": null } }`, + `[ { "op": "remove", "path": "/qux/bar" } ]`, + }, + { + `{ "foo": "bar", "qux": { "baz": 1, "bar": null } }`, + `[ { "op": "replace", "path": "/qux/baz", "value": null } ]`, + }, +} + +var BadCases = []BadCase{ + { + `{ "foo": "bar" }`, + `[ { "op": "add", "path": "/baz/bat", "value": "qux" } ]`, + }, + { + `{ "a": { "b": { "d": 1 } } }`, + `[ { "op": "remove", "path": "/a/b/c" } ]`, + }, + { + `{ "a": { "b": { "d": 1 } } }`, + `[ { "op": "move", "from": "/a/b/c", "path": "/a/b/e" } ]`, + }, + { + `{ "a": { "b": [1] } }`, + `[ { "op": "remove", "path": "/a/b/1" } ]`, + }, + { + `{ "a": { "b": [1] } }`, + `[ { "op": "move", "from": "/a/b/1", "path": "/a/b/2" } ]`, + }, + { + `{ "foo": "bar" }`, + `[ { "op": "add", "pathz": "/baz", "value": "qux" } ]`, + }, + { + `{ "foo": "bar" }`, + `[ { "op": "add", "path": "", "value": "qux" } ]`, + }, + { + `{ "foo": ["bar","baz"]}`, + `[ { "op": "replace", "path": "/foo/2", "value": "bum"}]`, + }, + { + `{ "foo": ["bar","baz"]}`, + `[ { "op": "add", "path": "/foo/-4", "value": "bum"}]`, + }, + { + `{ "name":{ "foo": "bat", "qux": "bum"}}`, + `[ { "op": "replace", "path": "/foo/bar", "value":"baz"}]`, + }, + { + `{ "foo": ["bar"]}`, + `[ {"op": "add", "path": "/foo/2", "value": "bum"}]`, + }, + { + `{ "foo": []}`, + `[ {"op": "remove", "path": "/foo/-"}]`, + }, + { + `{ "foo": []}`, + `[ {"op": "remove", "path": "/foo/-1"}]`, + }, + { + `{ "foo": ["bar"]}`, + `[ {"op": "remove", "path": "/foo/-2"}]`, + }, + { + `{}`, + `[ {"op":null,"path":""} ]`, + }, + { + `{}`, + `[ {"op":"add","path":null} ]`, + }, + { + `{}`, + `[ { "op": "copy", "from": null }]`, + }, + { + `{ "foo": ["bar"]}`, + `[{"op": "copy", "path": "/foo/6666666666", "from": "/"}]`, + }, + // Can't copy into an index greater than the size of the array + { + `{ "foo": ["bar"]}`, + `[{"op": "copy", "path": "/foo/2", "from": "/foo/0"}]`, + }, + // Accumulated copy size cannot exceed AccumulatedCopySizeLimit. + { + fmt.Sprintf(`{ "foo": ["A", %q] }`, repeatedA(49)), + // The wrapping quotes around 'A's are included in the copy + // size, so each copy operation increases the size by 51 bytes. + `[ { "op": "copy", "path": "/foo/-", "from": "/foo/1" }, + { "op": "copy", "path": "/foo/-", "from": "/foo/1" }]`, + }, + // Can't move into an index greater than or equal to the size of the array + { + `{ "foo": [ "all", "grass", "cows", "eat" ] }`, + `[ { "op": "move", "from": "/foo/1", "path": "/foo/4" } ]`, + }, + { + `{ "baz": "qux" }`, + `[ { "op": "replace", "path": "/foo", "value": "bar" } ]`, + }, + // Can't copy from non-existent "from" key. + { + `{ "foo": "bar"}`, + `[{"op": "copy", "path": "/qux", "from": "/baz"}]`, + }, +} + +// This is not thread safe, so we cannot run patch tests in parallel. +func configureGlobals(accumulatedCopySizeLimit int64) func() { + oldAccumulatedCopySizeLimit := AccumulatedCopySizeLimit + AccumulatedCopySizeLimit = accumulatedCopySizeLimit + return func() { + AccumulatedCopySizeLimit = oldAccumulatedCopySizeLimit + } +} + +func TestAllCases(t *testing.T) { + defer configureGlobals(int64(100))() + for _, c := range Cases { + out, err := applyPatch(c.doc, c.patch) + + if err != nil { + t.Errorf("Unable to apply patch: %s", err) + } + + if !compareJSON(out, c.result) { + t.Errorf("Patch did not apply. Expected:\n%s\n\nActual:\n%s", + reformatJSON(c.result), reformatJSON(out)) + } + } + + for _, c := range MutationTestCases { + out, err := applyPatch(c.doc, c.patch) + + if err != nil { + t.Errorf("Unable to apply patch: %s", err) + } + + if compareJSON(out, c.doc) { + t.Errorf("Patch did not apply. Original:\n%s\n\nPatched:\n%s", + reformatJSON(c.doc), reformatJSON(out)) + } + } + + for _, c := range BadCases { + _, err := applyPatch(c.doc, c.patch) + + if err == nil { + t.Errorf("Patch %q should have failed to apply but it did not", c.patch) + } + } +} + +type TestCase struct { + doc, patch string + result bool + failedPath string +} + +var TestCases = []TestCase{ + { + `{ + "baz": "qux", + "foo": [ "a", 2, "c" ] + }`, + `[ + { "op": "test", "path": "/baz", "value": "qux" }, + { "op": "test", "path": "/foo/1", "value": 2 } + ]`, + true, + "", + }, + { + `{ "baz": "qux" }`, + `[ { "op": "test", "path": "/baz", "value": "bar" } ]`, + false, + "/baz", + }, + { + `{ + "baz": "qux", + "foo": ["a", 2, "c"] + }`, + `[ + { "op": "test", "path": "/baz", "value": "qux" }, + { "op": "test", "path": "/foo/1", "value": "c" } + ]`, + false, + "/foo/1", + }, + { + `{ "baz": "qux" }`, + `[ { "op": "test", "path": "/foo", "value": 42 } ]`, + false, + "/foo", + }, + { + `{ "baz": "qux" }`, + `[ { "op": "test", "path": "/foo", "value": null } ]`, + true, + "", + }, + { + `{ "foo": null }`, + `[ { "op": "test", "path": "/foo", "value": null } ]`, + true, + "", + }, + { + `{ "foo": {} }`, + `[ { "op": "test", "path": "/foo", "value": null } ]`, + false, + "/foo", + }, + { + `{ "foo": [] }`, + `[ { "op": "test", "path": "/foo", "value": null } ]`, + false, + "/foo", + }, + { + `{ "baz/foo": "qux" }`, + `[ { "op": "test", "path": "/baz~1foo", "value": "qux"} ]`, + true, + "", + }, + { + `{ "foo": [] }`, + `[ { "op": "test", "path": "/foo"} ]`, + false, + "/foo", + }, + { + `{ "foo": "bar" }`, + `[ { "op": "test", "path": "/baz", "value": "bar" } ]`, + false, + "/baz", + }, + { + `{ "foo": "bar" }`, + `[ { "op": "test", "path": "/baz", "value": null } ]`, + true, + "/baz", + }, +} + +func TestAllTest(t *testing.T) { + for _, c := range TestCases { + _, err := applyPatch(c.doc, c.patch) + + if c.result && err != nil { + t.Errorf("Testing failed when it should have passed: %s", err) + } else if !c.result && err == nil { + t.Errorf("Testing passed when it should have faild: %s", err) + } else if !c.result { + expected := fmt.Sprintf("testing value %s failed: test failed", c.failedPath) + if err.Error() != expected { + t.Errorf("Testing failed as expected but invalid message: expected [%s], got [%s]", expected, err) + } + } + } +} + +func TestAdd(t *testing.T) { + testCases := []struct { + name string + key string + val lazyNode + arr partialArray + rejectNegativeIndicies bool + err string + }{ + { + name: "should work", + key: "0", + val: lazyNode{}, + arr: partialArray{}, + }, + { + name: "index too large", + key: "1", + val: lazyNode{}, + arr: partialArray{}, + err: "Unable to access invalid index: 1: invalid index referenced", + }, + { + name: "negative should work", + key: "-1", + val: lazyNode{}, + arr: partialArray{}, + }, + { + name: "negative too small", + key: "-2", + val: lazyNode{}, + arr: partialArray{}, + err: "Unable to access invalid index: -2: invalid index referenced", + }, + { + name: "negative but negative disabled", + key: "-1", + val: lazyNode{}, + arr: partialArray{}, + rejectNegativeIndicies: true, + err: "Unable to access invalid index: -1: invalid index referenced", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + SupportNegativeIndices = !tc.rejectNegativeIndicies + key := tc.key + arr := &tc.arr + val := &tc.val + err := arr.add(key, val) + if err == nil && tc.err != "" { + t.Errorf("Expected error but got none! %v", tc.err) + } else if err != nil && tc.err == "" { + t.Errorf("Did not expect error but go: %v", err) + } else if err != nil && err.Error() != tc.err { + t.Errorf("Expected error %v but got error %v", tc.err, err) + } + }) + } +} + +type EqualityCase struct { + name string + a, b string + equal bool +} + +var EqualityCases = []EqualityCase{ + { + "ExtraKeyFalse", + `{"foo": "bar"}`, + `{"foo": "bar", "baz": "qux"}`, + false, + }, + { + "StripWhitespaceTrue", + `{ + "foo": "bar", + "baz": "qux" + }`, + `{"foo": "bar", "baz": "qux"}`, + true, + }, + { + "KeysOutOfOrderTrue", + `{ + "baz": "qux", + "foo": "bar" + }`, + `{"foo": "bar", "baz": "qux"}`, + true, + }, + { + "ComparingNullFalse", + `{"foo": null}`, + `{"foo": "bar"}`, + false, + }, + { + "ComparingNullTrue", + `{"foo": null}`, + `{"foo": null}`, + true, + }, + { + "ArrayOutOfOrderFalse", + `["foo", "bar", "baz"]`, + `["bar", "baz", "foo"]`, + false, + }, + { + "ArrayTrue", + `["foo", "bar", "baz"]`, + `["foo", "bar", "baz"]`, + true, + }, + { + "NonStringTypesTrue", + `{"int": 6, "bool": true, "float": 7.0, "string": "the_string", "null": null}`, + `{"int": 6, "bool": true, "float": 7.0, "string": "the_string", "null": null}`, + true, + }, + { + "NestedNullFalse", + `{"foo": ["an", "array"], "bar": {"an": "object"}}`, + `{"foo": null, "bar": null}`, + false, + }, + { + "NullCompareStringFalse", + `"foo"`, + `null`, + false, + }, + { + "NullCompareIntFalse", + `6`, + `null`, + false, + }, + { + "NullCompareFloatFalse", + `6.01`, + `null`, + false, + }, + { + "NullCompareBoolFalse", + `false`, + `null`, + false, + }, +} + +func TestEquality(t *testing.T) { + for _, tc := range EqualityCases { + t.Run(tc.name, func(t *testing.T) { + got := Equal([]byte(tc.a), []byte(tc.b)) + if got != tc.equal { + t.Errorf("Expected Equal(%s, %s) to return %t, but got %t", tc.a, tc.b, tc.equal, got) + } + + got = Equal([]byte(tc.b), []byte(tc.a)) + if got != tc.equal { + t.Errorf("Expected Equal(%s, %s) to return %t, but got %t", tc.b, tc.a, tc.equal, got) + } + }) + } +} From 9f7d3fd5c24bae2057d228018cf2254d79336d7d Mon Sep 17 00:00:00 2001 From: Benjamin Elder Date: Thu, 26 Mar 2020 14:38:20 -0700 Subject: [PATCH 20/23] update paths for v5 --- v5/cmd/json-patch/main.go | 2 +- v5/go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/v5/cmd/json-patch/main.go b/v5/cmd/json-patch/main.go index 2be1826..b496cb4 100644 --- a/v5/cmd/json-patch/main.go +++ b/v5/cmd/json-patch/main.go @@ -6,7 +6,7 @@ import ( "log" "os" - jsonpatch "github.com/evanphx/json-patch" + jsonpatch "github.com/evanphx/json-patch/v5" flags "github.com/jessevdk/go-flags" ) diff --git a/v5/go.mod b/v5/go.mod index a858cab..e336404 100644 --- a/v5/go.mod +++ b/v5/go.mod @@ -1,4 +1,4 @@ -module github.com/evanphx/json-patch +module github.com/evanphx/json-patch/v5 go 1.12 From a8691c6cf046ac8f7ace34d2169d473d0f4a0d2c Mon Sep 17 00:00:00 2001 From: Benjamin Elder Date: Thu, 26 Mar 2020 14:40:03 -0700 Subject: [PATCH 21/23] correct module requirements --- go.mod | 5 ++++- go.sum | 2 ++ v5/go.mod | 5 ++++- v5/go.sum | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index a858cab..43a44b1 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/evanphx/json-patch go 1.12 -require github.com/pkg/errors v0.8.1 +require ( + github.com/jessevdk/go-flags v1.4.0 + github.com/pkg/errors v0.8.1 +) diff --git a/go.sum b/go.sum index f29ab35..0edb941 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/v5/go.mod b/v5/go.mod index e336404..c731d4c 100644 --- a/v5/go.mod +++ b/v5/go.mod @@ -2,4 +2,7 @@ module github.com/evanphx/json-patch/v5 go 1.12 -require github.com/pkg/errors v0.8.1 +require ( + github.com/jessevdk/go-flags v1.4.0 + github.com/pkg/errors v0.8.1 +) diff --git a/v5/go.sum b/v5/go.sum index f29ab35..0edb941 100644 --- a/v5/go.sum +++ b/v5/go.sum @@ -1,2 +1,4 @@ +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From 39fd76ecae93aa7dfd78fd12525251888c0fa2c4 Mon Sep 17 00:00:00 2001 From: Benjamin Elder Date: Thu, 26 Mar 2020 14:43:16 -0700 Subject: [PATCH 22/23] also test v5 in travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2092c72..d89d7b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,9 @@ install: script: - go get - go test -cover ./... + - cd ./v5 + - go get + - go test -cover ./... notifications: email: false From c781b2a96bf675490d795bbd274979ef2662c793 Mon Sep 17 00:00:00 2001 From: Benjamin Elder Date: Thu, 26 Mar 2020 14:57:21 -0700 Subject: [PATCH 23/23] update travis to most recent go versions --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d89d7b8..50e4afd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: go go: - - 1.8 - - 1.7 + - 1.14 + - 1.13 install: - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi