Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix array comparison #13

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ _testmain.go
*.exe
*.test
*.prof

.idea/
33 changes: 33 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true


[[constraint]]
name = "github.com/evanphx/json-patch"
version = "3.0.0"

[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"

[prune]
go-tests = true
unused-packages = true
201 changes: 139 additions & 62 deletions jsonpatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ import (
"strings"
)

var errBadJSONDoc = fmt.Errorf("Invalid JSON Document")
var errBadJSONDoc = fmt.Errorf("invalid JSON Document")

type JsonPatchOperation struct {
type Operation struct {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamalsaha Why changing the name?
Controller-runtime is considering switching to use appscode's fork.
But it can be a problem for the existing users due to the breaking change.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what can we do in our fork. Will adding a type alias help?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should help

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added the type alias to 1.0.0 release.

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))
Expand All @@ -39,14 +39,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/
Expand All @@ -55,7 +55,7 @@ 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) {
func CreatePatch(a, b []byte) ([]Operation, error) {
aI := map[string]interface{}{}
bI := map[string]interface{}{}
err := json.Unmarshal(a, &aI)
Expand All @@ -66,7 +66,7 @@ func CreatePatch(a, b []byte) ([]JsonPatchOperation, error) {
if err != nil {
return nil, errBadJSONDoc
}
return diff(aI, bI, "", []JsonPatchOperation{})
return diff(aI, bI, "", []Operation{})
}

// Returns true if the values matches (must be json types)
Expand All @@ -78,22 +78,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
Expand All @@ -106,7 +109,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
}
Expand Down Expand Up @@ -148,7 +154,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]
Expand All @@ -157,11 +163,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)
Expand All @@ -181,7 +182,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{}:
Expand All @@ -195,63 +210,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
func isBasicType(a interface{}) bool {
switch a.(type) {
case string, float64, bool:
default:
return false
}
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
}
if !found {
retval = append(retval, NewPatch("remove", makePath(p, i), nil))
}
}
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 i, v := range bv {
found := false
for _, v2 := range av {
if reflect.DeepEqual(v, v2) {
found = true
break
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))
}
}
if !found {
retval = append(retval, NewPatch("add", makePath(p, i), v))
}
}

return retval
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[j-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{}
}
Loading