Improve and fix diff package
This commit is contained in:
parent
c0110b8af9
commit
33391e0a28
2 changed files with 240 additions and 75 deletions
|
@ -18,63 +18,98 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/abcum/surreal/sql"
|
||||||
|
|
||||||
|
"github.com/abcum/surreal/util/data"
|
||||||
|
|
||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"github.com/sergi/go-diff/diffmatchpatch"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Operation struct {
|
type operations struct {
|
||||||
Op string `cork:"op,omietmpty" json:"op,omietmpty"`
|
ops []*operation
|
||||||
From string `cork:"from,omitempty" json:"from,omitempty"`
|
|
||||||
Path string `cork:"path,omitempty" json:"path,omitempty"`
|
|
||||||
Value interface{} `cork:"value,omitempty" json:"value,omitempty"`
|
|
||||||
Before interface{} `cork:"-" json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Operations struct {
|
type operation struct {
|
||||||
Ops []*Operation
|
op string
|
||||||
|
from string
|
||||||
|
path string
|
||||||
|
value interface{}
|
||||||
|
before interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Diff(old, now map[string]interface{}) (ops *Operations) {
|
func Diff(old, now map[string]interface{}) []interface{} {
|
||||||
|
out := &operations{}
|
||||||
|
out.diff(old, now, "")
|
||||||
|
return out.diffs()
|
||||||
|
}
|
||||||
|
|
||||||
ops = &Operations{}
|
func Patch(old map[string]interface{}, ops []interface{}) map[string]interface{} {
|
||||||
|
out := &operations{}
|
||||||
|
out.load(ops)
|
||||||
|
return out.patch(old)
|
||||||
|
}
|
||||||
|
|
||||||
ops.diff(old, now, "")
|
func (o *operations) load(ops []interface{}) {
|
||||||
|
|
||||||
return
|
for _, v := range ops {
|
||||||
|
|
||||||
|
if obj, ok := v.(map[string]interface{}); ok {
|
||||||
|
|
||||||
|
op := &operation{}
|
||||||
|
|
||||||
|
op.value = obj["value"]
|
||||||
|
|
||||||
|
if str, ok := obj["op"].(string); ok {
|
||||||
|
op.op = str
|
||||||
|
}
|
||||||
|
|
||||||
|
if str, ok := obj["from"].(string); ok {
|
||||||
|
op.from = str
|
||||||
|
}
|
||||||
|
|
||||||
|
if str, ok := obj["path"].(string); ok {
|
||||||
|
op.path = str
|
||||||
|
}
|
||||||
|
|
||||||
|
o.ops = append(o.ops, op)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Operations) Patch(old map[string]interface{}) (now map[string]interface{}, err error) {
|
func (o *operations) diffs() (ops []interface{}) {
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Operations) Rebase(other *Operations) (ops *Operations, err error) {
|
ops = make([]interface{}, len(o.ops))
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Operations) Out() (ops []map[string]interface{}) {
|
sort.Slice(o.ops, func(i, j int) bool {
|
||||||
|
return o.ops[i].path < o.ops[j].path
|
||||||
|
})
|
||||||
|
|
||||||
for _, v := range o.Ops {
|
for k, v := range o.ops {
|
||||||
|
|
||||||
op := make(map[string]interface{})
|
op := make(map[string]interface{})
|
||||||
|
|
||||||
if len(v.Op) > 0 {
|
if len(v.op) > 0 {
|
||||||
op["op"] = v.Op
|
op["op"] = v.op
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(v.From) > 0 {
|
if len(v.from) > 0 {
|
||||||
op["from"] = v.From
|
op["from"] = v.from
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(v.Path) > 0 {
|
if len(v.path) > 0 {
|
||||||
op["path"] = v.Path
|
op["path"] = v.path
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.Value != nil {
|
if v.value != nil {
|
||||||
op["value"] = v.Value
|
op["value"] = v.value
|
||||||
}
|
}
|
||||||
|
|
||||||
ops = append(ops, op)
|
ops[k] = op
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,11 +117,20 @@ func (o *Operations) Out() (ops []map[string]interface{}) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isIn(a int, list []int) bool {
|
||||||
|
for _, b := range list {
|
||||||
|
if b == a {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func route(path string, part string) string {
|
func route(path string, part string) string {
|
||||||
if len(path) == 0 {
|
if len(path) == 0 {
|
||||||
return "/" + part
|
return "/" + part
|
||||||
} else {
|
} else {
|
||||||
if path[0] == '/' {
|
if part[0] == '/' {
|
||||||
return path + part
|
return path + part
|
||||||
} else {
|
} else {
|
||||||
return path + "/" + part
|
return path + "/" + part
|
||||||
|
@ -94,19 +138,19 @@ func route(path string, part string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Operations) op(op, from, path string, before, after interface{}) {
|
func (o *operations) op(op, from, path string, before, after interface{}) {
|
||||||
|
|
||||||
o.Ops = append(o.Ops, &Operation{
|
o.ops = append(o.ops, &operation{
|
||||||
Op: op,
|
op: op,
|
||||||
From: from,
|
from: from,
|
||||||
Path: path,
|
path: path,
|
||||||
Value: after,
|
value: after,
|
||||||
Before: before,
|
before: before,
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Operations) diff(old, now map[string]interface{}, path string) {
|
func (o *operations) diff(old, now map[string]interface{}, path string) {
|
||||||
|
|
||||||
for key, after := range now {
|
for key, after := range now {
|
||||||
|
|
||||||
|
@ -149,13 +193,13 @@ func (o *Operations) diff(old, now map[string]interface{}, path string) {
|
||||||
|
|
||||||
var used []int
|
var used []int
|
||||||
|
|
||||||
for i := len(o.Ops) - 1; i >= 0; i-- {
|
for i := len(o.ops) - 1; i >= 0; i-- {
|
||||||
if iv := o.Ops[i]; !isIn(i, used) && iv.Op == "add" {
|
if iv := o.ops[i]; !isIn(i, used) && iv.op == "add" {
|
||||||
for j := len(o.Ops) - 1; j >= 0; j-- {
|
for j := len(o.ops) - 1; j >= 0; j-- {
|
||||||
if jv := o.Ops[j]; !isIn(j, used) && jv.Op == "remove" {
|
if jv := o.ops[j]; !isIn(j, used) && jv.op == "remove" {
|
||||||
if reflect.DeepEqual(iv.Value, jv.Before) {
|
if reflect.DeepEqual(iv.value, jv.before) {
|
||||||
used = append(used, []int{i, j}...)
|
used = append(used, []int{i, j}...)
|
||||||
o.op("move", jv.Path, iv.Path, nil, nil)
|
o.op("move", jv.path, iv.path, nil, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,21 +209,49 @@ func (o *Operations) diff(old, now map[string]interface{}, path string) {
|
||||||
sort.Sort(sort.Reverse(sort.IntSlice(used)))
|
sort.Sort(sort.Reverse(sort.IntSlice(used)))
|
||||||
|
|
||||||
for _, i := range used {
|
for _, i := range used {
|
||||||
o.Ops = append(o.Ops[:i], o.Ops[i+1:]...)
|
o.ops = append(o.ops[:i], o.ops[i+1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isIn(a int, list []int) bool {
|
func (o *operations) patch(old map[string]interface{}) (now map[string]interface{}) {
|
||||||
for _, b := range list {
|
|
||||||
if b == a {
|
obj := data.Consume(old)
|
||||||
return true
|
|
||||||
|
for _, v := range o.ops {
|
||||||
|
|
||||||
|
path := strings.Split(v.path, "/")
|
||||||
|
|
||||||
|
prev := path[:len(path)-1]
|
||||||
|
|
||||||
|
switch v.op {
|
||||||
|
case "add":
|
||||||
|
switch obj.Get(prev...).Data().(type) {
|
||||||
|
case []interface{}:
|
||||||
|
obj.Append(v.value, prev...)
|
||||||
|
default:
|
||||||
|
obj.Set(v.value, path...)
|
||||||
|
}
|
||||||
|
case "remove":
|
||||||
|
obj.Del(path...)
|
||||||
|
case "replace":
|
||||||
|
obj.Set(v.value, path...)
|
||||||
|
case "change":
|
||||||
|
if txt, ok := obj.Get(path...).Data().(string); ok {
|
||||||
|
dmp := diffmatchpatch.New()
|
||||||
|
dif, _ := dmp.DiffFromDelta(txt, v.value.(string))
|
||||||
|
str := dmp.DiffText2(dif)
|
||||||
|
obj.Set(str, path...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
return old
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Operations) text(old, now string, path string) {
|
func (o *operations) text(old, now string, path string) {
|
||||||
|
|
||||||
dmp := diffmatchpatch.New()
|
dmp := diffmatchpatch.New()
|
||||||
|
|
||||||
|
@ -191,7 +263,7 @@ func (o *Operations) text(old, now string, path string) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Operations) vals(old, now interface{}, path string) {
|
func (o *operations) vals(old, now interface{}, path string) {
|
||||||
|
|
||||||
if reflect.TypeOf(old) != reflect.TypeOf(now) {
|
if reflect.TypeOf(old) != reflect.TypeOf(now) {
|
||||||
o.op("replace", "", path, old, now)
|
o.op("replace", "", path, old, now)
|
||||||
|
@ -203,6 +275,11 @@ func (o *Operations) vals(old, now interface{}, path string) {
|
||||||
if !reflect.DeepEqual(old, now) {
|
if !reflect.DeepEqual(old, now) {
|
||||||
o.op("replace", "", path, old, now)
|
o.op("replace", "", path, old, now)
|
||||||
}
|
}
|
||||||
|
case *sql.Thing:
|
||||||
|
nv := now.(*sql.Thing)
|
||||||
|
if ov.TB != nv.TB && ov.ID != nv.ID {
|
||||||
|
o.op("replace", "", path, old, now)
|
||||||
|
}
|
||||||
case bool:
|
case bool:
|
||||||
if ov != now.(bool) {
|
if ov != now.(bool) {
|
||||||
o.op("replace", "", path, old, now)
|
o.op("replace", "", path, old, now)
|
||||||
|
@ -233,12 +310,12 @@ func (o *Operations) vals(old, now interface{}, path string) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Operations) arrs(old, now []interface{}, path string) {
|
func (o *operations) arrs(old, now []interface{}, path string) {
|
||||||
|
|
||||||
var i int
|
var i int
|
||||||
|
|
||||||
for i = 0; i < len(old) && i < len(now); i++ {
|
for i = 0; i < len(old) && i < len(now); i++ {
|
||||||
o.vals(old[i], now[i], strconv.Itoa(i))
|
o.vals(old[i], now[i], route(path, strconv.Itoa(i)))
|
||||||
}
|
}
|
||||||
|
|
||||||
for j := i; j < len(now); j++ {
|
for j := i; j < len(now); j++ {
|
||||||
|
|
|
@ -20,36 +20,124 @@ import (
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var old = map[string]interface{}{
|
||||||
|
"age": 18,
|
||||||
|
"name": map[string]interface{}{
|
||||||
|
"first": "T",
|
||||||
|
"last": "M H",
|
||||||
|
},
|
||||||
|
"dates": []interface{}{1, 2, 4},
|
||||||
|
"changing": true,
|
||||||
|
"different": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = map[string]interface{}{
|
||||||
|
"age": 29,
|
||||||
|
"name": map[string]interface{}{
|
||||||
|
"first": "Tobie",
|
||||||
|
"last": "Morgan Hitchcock",
|
||||||
|
},
|
||||||
|
"changed": "This is a string",
|
||||||
|
"different": true,
|
||||||
|
"dates": []interface{}{1, 2, 3, 4, 4},
|
||||||
|
"addedArr": []interface{}{1, 2, 3},
|
||||||
|
"addedMap": map[string]interface{}{
|
||||||
|
"first": map[string]interface{}{
|
||||||
|
"embedded": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var chg = []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "add",
|
||||||
|
"path": "/addedArr",
|
||||||
|
"value": []interface{}{1, 2, 3},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "add",
|
||||||
|
"path": "/addedMap",
|
||||||
|
"value": map[string]interface{}{
|
||||||
|
"first": map[string]interface{}{
|
||||||
|
"embedded": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/age",
|
||||||
|
"value": 29,
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "add",
|
||||||
|
"path": "/changed",
|
||||||
|
"value": "This is a string",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "remove",
|
||||||
|
"path": "/changing",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/dates/2",
|
||||||
|
"value": 3,
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "add",
|
||||||
|
"path": "/dates/3",
|
||||||
|
"value": 4,
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "add",
|
||||||
|
"path": "/dates/4",
|
||||||
|
"value": 4,
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/different",
|
||||||
|
"value": true,
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "change",
|
||||||
|
"path": "/name/first",
|
||||||
|
"value": "=1\t+obie",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"op": "change",
|
||||||
|
"path": "/name/last",
|
||||||
|
"value": "=1\t+organ\t=2\t+itchcock",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func TestMain(t *testing.T) {
|
func TestMain(t *testing.T) {
|
||||||
|
|
||||||
Convey("Main", t, func() {
|
var obj interface{}
|
||||||
So(nil, ShouldBeNil)
|
var dif []interface{}
|
||||||
|
|
||||||
|
Convey("Confirm that the item can be diffed correctly", t, func() {
|
||||||
|
dif = Diff(old, now)
|
||||||
|
So(dif, ShouldResemble, chg)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Confirm that the item can be patched correctly", t, func() {
|
||||||
|
obj = Patch(old, dif)
|
||||||
|
So(obj, ShouldResemble, now)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkFib1(b *testing.B) {
|
func BenchmarkDiff(b *testing.B) {
|
||||||
|
|
||||||
old := map[string]interface{}{
|
|
||||||
"age": 18,
|
|
||||||
"name": map[string]interface{}{
|
|
||||||
"first": "T",
|
|
||||||
"last": "M H",
|
|
||||||
},
|
|
||||||
"chainging": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
now := map[string]interface{}{
|
|
||||||
"age": 29,
|
|
||||||
"name": map[string]interface{}{
|
|
||||||
"first": "Tobie",
|
|
||||||
"last": "Morgan Hitchcock",
|
|
||||||
},
|
|
||||||
"changing": "This is a string",
|
|
||||||
}
|
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
Diff(old, now)
|
Diff(old, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkPatch(b *testing.B) {
|
||||||
|
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
Patch(old, chg)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue