Update jsonpatch lib that correctly handle object removal (#1078)

* update jsonpatch lib

* add more patch tests for removal
This commit is contained in:
cshou 2020-02-12 08:27:22 -08:00 committed by GitHub
parent d428fbc250
commit 1cc3c3e852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 243 additions and 86 deletions

18
Gopkg.lock generated
View File

@ -422,14 +422,6 @@
revision = "24b83195037b3bc61fcda2d28b7b0518bce293b6"
version = "v1.0.4"
[[projects]]
branch = "master"
digest = "1:0e9bfc47ab9941ecc3344e580baca5deb4091177e84dd9773b48b38ec26b93d5"
name = "github.com/mattbaird/jsonpatch"
packages = ["."]
pruneopts = "NUT"
revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f"
[[projects]]
digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6"
name = "github.com/matttproud/golang_protobuf_extensions"
@ -752,6 +744,14 @@
pruneopts = "NUT"
revision = "8b927904ee0dec805c89aaf9172f4459296ed6e8"
[[projects]]
digest = "1:8392b5c29adedc5f85c66b48bb53b6c49dd91d8540f3cad5d43ef7277946718f"
name = "gomodules.xyz/jsonpatch"
packages = ["v2"]
pruneopts = "NUT"
revision = "e8422f09d27ee2c8cfb2c7f8089eb9eeb0764849"
version = "v2.0.1"
[[projects]]
digest = "1:081608ceb454c46b54d24b7561e5744088f3ff69478b23f50277ec83bd8636b0"
name = "google.golang.org/api"
@ -1371,7 +1371,6 @@
"github.com/gorilla/websocket",
"github.com/kballard/go-shellquote",
"github.com/markbates/inflect",
"github.com/mattbaird/jsonpatch",
"github.com/openzipkin/zipkin-go",
"github.com/openzipkin/zipkin-go/model",
"github.com/openzipkin/zipkin-go/reporter",
@ -1397,6 +1396,7 @@
"golang.org/x/net/http2/h2c",
"golang.org/x/oauth2",
"golang.org/x/sync/errgroup",
"gomodules.xyz/jsonpatch/v2",
"google.golang.org/api/container/v1beta1",
"google.golang.org/api/iterator",
"google.golang.org/api/option",

View File

@ -20,7 +20,7 @@ import (
"encoding/json"
jsonmergepatch "github.com/evanphx/json-patch"
"github.com/mattbaird/jsonpatch"
jsonpatch "gomodules.xyz/jsonpatch/v2"
)
func marshallBeforeAfter(before, after interface{}) ([]byte, []byte, error) {

View File

@ -193,6 +193,84 @@ func TestCreatePatch(t *testing.T) {
Path: "/status/patchable/array/1",
Value: "bar",
}},
}, {
name: "patch with remove elements from array",
before: &Patch{
Spec: PatchSpec{
Patchable: &Patchable{
Array: []string{"foo", "bar"},
},
},
},
after: &Patch{
Spec: PatchSpec{
Patchable: &Patchable{
Array: []string{"bar"},
},
},
},
want: JSONPatch{{
Operation: "remove",
Path: "/status/patchable/array/0",
}},
}, {
name: "patch with remove collection",
before: &Patch{
Spec: PatchSpec{
Patchable: &Patchable{
Array: []string{"foo", "bar"},
},
},
},
after: &Patch{
Spec: PatchSpec{
Patchable: &Patchable{},
},
},
want: JSONPatch{{
Operation: "remove",
Path: "/status/patchable/array",
}},
}, {
name: "patch with add elements to array",
before: &Patch{
Spec: PatchSpec{
Patchable: &Patchable{
Array: []string{"bar"},
},
},
},
after: &Patch{
Spec: PatchSpec{
Patchable: &Patchable{
Array: []string{"foo", "bar"},
},
},
},
want: JSONPatch{{
Operation: "add",
Path: "/status/patchable/array/0",
Value: "foo",
}},
}, {
name: "patch with add array",
before: &Patch{
Spec: PatchSpec{
Patchable: &Patchable{},
},
},
after: &Patch{
Spec: PatchSpec{
Patchable: &Patchable{
Array: []string{"foo"},
},
},
},
want: JSONPatch{{
Operation: "add",
Path: "/status/patchable/array",
Value: []interface{}{string("foo")},
}},
}, {
name: "before doesn't marshal",
before: &DoesntMarshal{},

View File

@ -8,20 +8,22 @@ import (
"strings"
)
var errBadJSONDoc = fmt.Errorf("Invalid JSON Document")
var errBadJSONDoc = fmt.Errorf("invalid JSON Document")
type JsonPatchOperation struct {
type JsonPatchOperation = Operation
type Operation struct {
Operation string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value,omitempty"`
}
func (j *JsonPatchOperation) Json() string {
func (j *Operation) Json() string {
b, _ := json.Marshal(j)
return string(b)
}
func (j *JsonPatchOperation) MarshalJSON() ([]byte, error) {
func (j *Operation) MarshalJSON() ([]byte, error) {
var b bytes.Buffer
b.WriteString("{")
b.WriteString(fmt.Sprintf(`"op":"%s"`, j.Operation))
@ -39,14 +41,14 @@ func (j *JsonPatchOperation) MarshalJSON() ([]byte, error) {
return b.Bytes(), nil
}
type ByPath []JsonPatchOperation
type ByPath []Operation
func (a ByPath) Len() int { return len(a) }
func (a ByPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByPath) Less(i, j int) bool { return a[i].Path < a[j].Path }
func NewPatch(operation, path string, value interface{}) JsonPatchOperation {
return JsonPatchOperation{Operation: operation, Path: path, Value: value}
func NewPatch(operation, path string, value interface{}) Operation {
return Operation{Operation: operation, Path: path, Value: value}
}
// CreatePatch creates a patch as specified in http://jsonpatch.com/
@ -55,9 +57,9 @@ func NewPatch(operation, path string, value interface{}) JsonPatchOperation {
// The function will return an array of JsonPatchOperations
//
// An error will be returned if any of the two documents are invalid.
func CreatePatch(a, b []byte) ([]JsonPatchOperation, error) {
aI := map[string]interface{}{}
bI := map[string]interface{}{}
func CreatePatch(a, b []byte) ([]Operation, error) {
var aI interface{}
var bI interface{}
err := json.Unmarshal(a, &aI)
if err != nil {
return nil, errBadJSONDoc
@ -66,7 +68,7 @@ func CreatePatch(a, b []byte) ([]JsonPatchOperation, error) {
if err != nil {
return nil, errBadJSONDoc
}
return diff(aI, bI, "", []JsonPatchOperation{})
return handleValues(aI, bI, "", []Operation{})
}
// Returns true if the values matches (must be json types)
@ -78,22 +80,25 @@ func matchesValue(av, bv interface{}) bool {
}
switch at := av.(type) {
case string:
bt := bv.(string)
if bt == at {
bt, ok := bv.(string)
if ok && bt == at {
return true
}
case float64:
bt := bv.(float64)
if bt == at {
bt, ok := bv.(float64)
if ok && bt == at {
return true
}
case bool:
bt := bv.(bool)
if bt == at {
bt, ok := bv.(bool)
if ok && bt == at {
return true
}
case map[string]interface{}:
bt := bv.(map[string]interface{})
bt, ok := bv.(map[string]interface{})
if !ok {
return false
}
for key := range at {
if !matchesValue(at[key], bt[key]) {
return false
@ -106,7 +111,10 @@ func matchesValue(av, bv interface{}) bool {
}
return true
case []interface{}:
bt := bv.([]interface{})
bt, ok := bv.([]interface{})
if !ok {
return false
}
if len(bt) != len(at) {
return false
}
@ -148,7 +156,7 @@ func makePath(path string, newPart interface{}) string {
}
// diff returns the (recursive) difference between a and b as an array of JsonPatchOperations.
func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) {
func diff(a, b map[string]interface{}, path string, patch []Operation) ([]Operation, error) {
for key, bv := range b {
p := makePath(path, key)
av, ok := a[key]
@ -157,11 +165,6 @@ func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation)
patch = append(patch, NewPatch("add", p, bv))
continue
}
// If types have changed, replace completely
if reflect.TypeOf(av) != reflect.TypeOf(bv) {
patch = append(patch, NewPatch("replace", p, bv))
continue
}
// Types are the same, compare values
var err error
patch, err = handleValues(av, bv, p, patch)
@ -181,7 +184,21 @@ func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation)
return patch, nil
}
func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) {
func handleValues(av, bv interface{}, p string, patch []Operation) ([]Operation, error) {
{
at := reflect.TypeOf(av)
bt := reflect.TypeOf(bv)
if at == nil && bt == nil {
// do nothing
return patch, nil
} else if at == nil && bt != nil {
return append(patch, NewPatch("add", p, bv)), nil
} else if at != bt {
// If types have changed, replace completely (preserves null in destination)
return append(patch, NewPatch("replace", p, bv)), nil
}
}
var err error
switch at := av.(type) {
case map[string]interface{}:
@ -195,63 +212,125 @@ func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation) ([]J
patch = append(patch, NewPatch("replace", p, bv))
}
case []interface{}:
bt, ok := bv.([]interface{})
if !ok {
// array replaced by non-array
patch = append(patch, NewPatch("replace", p, bv))
} else if len(at) != len(bt) {
// arrays are not the same length
patch = append(patch, compareArray(at, bt, p)...)
bt := bv.([]interface{})
if isSimpleArray(at) && isSimpleArray(bt) {
patch = append(patch, compareEditDistance(at, bt, p)...)
} else {
for i := range bt {
n := min(len(at), len(bt))
for i := len(at) - 1; i >= n; i-- {
patch = append(patch, NewPatch("remove", makePath(p, i), nil))
}
for i := n; i < len(bt); i++ {
patch = append(patch, NewPatch("add", makePath(p, i), bt[i]))
}
for i := 0; i < n; i++ {
var err error
patch, err = handleValues(at[i], bt[i], makePath(p, i), patch)
if err != nil {
return nil, err
}
}
}
case nil:
switch bv.(type) {
case nil:
// Both nil, fine.
default:
patch = append(patch, NewPatch("add", p, bv))
}
default:
panic(fmt.Sprintf("Unknown type:%T ", av))
}
return patch, nil
}
func compareArray(av, bv []interface{}, p string) []JsonPatchOperation {
retval := []JsonPatchOperation{}
// var err error
for i, v := range av {
found := false
for _, v2 := range bv {
if reflect.DeepEqual(v, v2) {
found = true
break
}
}
if !found {
retval = append(retval, NewPatch("remove", makePath(p, i), nil))
}
func isBasicType(a interface{}) bool {
switch a.(type) {
case string, float64, bool:
default:
return false
}
for i, v := range bv {
found := false
for _, v2 := range av {
if reflect.DeepEqual(v, v2) {
found = true
break
}
}
if !found {
retval = append(retval, NewPatch("add", makePath(p, i), v))
}
}
return retval
return true
}
func isSimpleArray(a []interface{}) bool {
for i := range a {
switch a[i].(type) {
case string, float64, bool:
default:
val := reflect.ValueOf(a[i])
if val.Kind() == reflect.Map {
for _, k := range val.MapKeys() {
av := val.MapIndex(k)
if av.Kind() == reflect.Ptr || av.Kind() == reflect.Interface {
if av.IsNil() {
continue
}
av = av.Elem()
}
if av.Kind() != reflect.String && av.Kind() != reflect.Float64 && av.Kind() != reflect.Bool {
return false
}
}
return true
}
return false
}
}
return true
}
// https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm
// Adapted from https://github.com/texttheater/golang-levenshtein
func compareEditDistance(s, t []interface{}, p string) []Operation {
m := len(s)
n := len(t)
d := make([][]int, m+1)
for i := 0; i <= m; i++ {
d[i] = make([]int, n+1)
d[i][0] = i
}
for j := 0; j <= n; j++ {
d[0][j] = j
}
for j := 1; j <= n; j++ {
for i := 1; i <= m; i++ {
if reflect.DeepEqual(s[i-1], t[j-1]) {
d[i][j] = d[i-1][j-1] // no op required
} else {
del := d[i-1][j] + 1
add := d[i][j-1] + 1
rep := d[i-1][j-1] + 1
d[i][j] = min(rep, min(add, del))
}
}
}
return backtrace(s, t, p, m, n, d)
}
func min(x int, y int) int {
if y < x {
return y
}
return x
}
func backtrace(s, t []interface{}, p string, i int, j int, matrix [][]int) []Operation {
if i > 0 && matrix[i-1][j]+1 == matrix[i][j] {
op := NewPatch("remove", makePath(p, i-1), nil)
return append([]Operation{op}, backtrace(s, t, p, i-1, j, matrix)...)
}
if j > 0 && matrix[i][j-1]+1 == matrix[i][j] {
op := NewPatch("add", makePath(p, i), t[j-1])
return append([]Operation{op}, backtrace(s, t, p, i, j-1, matrix)...)
}
if i > 0 && j > 0 && matrix[i-1][j-1]+1 == matrix[i][j] {
if isBasicType(s[0]) {
op := NewPatch("replace", makePath(p, i-1), t[j-1])
return append([]Operation{op}, backtrace(s, t, p, i-1, j-1, matrix)...)
}
p2, _ := handleValues(s[i-1], t[j-1], makePath(p, i-1), []Operation{})
return append(p2, backtrace(s, t, p, i-1, j-1, matrix)...)
}
if i > 0 && j > 0 && matrix[i-1][j-1] == matrix[i][j] {
return backtrace(s, t, p, i-1, j-1, matrix)
}
return []Operation{}
}

View File

@ -27,8 +27,8 @@ import (
"strings"
"testing"
"github.com/mattbaird/jsonpatch"
"golang.org/x/sync/errgroup"
jsonpatch "gomodules.xyz/jsonpatch/v2"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

View File

@ -24,7 +24,7 @@ import (
"testing"
"time"
"github.com/mattbaird/jsonpatch"
jsonpatch "gomodules.xyz/jsonpatch/v2"
"knative.dev/pkg/apis"
"knative.dev/pkg/client/injection/ducks/duck/v1/podspecable"
kubeclient "knative.dev/pkg/client/injection/kube/client/fake"

View File

@ -26,8 +26,8 @@ import (
"strings"
"github.com/markbates/inflect"
"github.com/mattbaird/jsonpatch"
"go.uber.org/zap"
jsonpatch "gomodules.xyz/jsonpatch/v2"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/runtime/schema"

View File

@ -26,7 +26,7 @@ import (
_ "knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1beta1/mutatingwebhookconfiguration/fake"
_ "knative.dev/pkg/client/injection/kube/informers/core/v1/secret/fake"
"github.com/mattbaird/jsonpatch"
jsonpatch "gomodules.xyz/jsonpatch/v2"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
authenticationv1 "k8s.io/api/authentication/v1"

View File

@ -24,7 +24,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/mattbaird/jsonpatch"
jsonpatch "gomodules.xyz/jsonpatch/v2"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/system"