mirror of https://github.com/knative/pkg.git
Update jsonpatch lib that correctly handle object removal (#1078)
* update jsonpatch lib * add more patch tests for removal
This commit is contained in:
parent
d428fbc250
commit
1cc3c3e852
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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{},
|
||||
|
|
|
@ -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{}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue