surrealpatch/util/diff/diff.go
2021-12-14 08:13:19 +00:00

345 lines
6.4 KiB
Go

// Copyright © 2016 SurrealDB Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package diff
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/surrealdb/surrealdb/sql"
"github.com/surrealdb/surrealdb/util/data"
"github.com/sergi/go-diff/diffmatchpatch"
)
type operations struct {
ops []*operation
}
type operation struct {
op string
from string
path string
value interface{}
before interface{}
}
func Diff(old, now map[string]interface{}) []interface{} {
out := &operations{}
out.diff(old, now, "")
return out.diffs()
}
func Patch(old map[string]interface{}, ops []interface{}) map[string]interface{} {
out := &operations{}
out.load(ops)
return out.patch(old)
}
func (o *operations) load(ops []interface{}) {
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) diffs() (ops []interface{}) {
ops = make([]interface{}, len(o.ops))
sort.Slice(o.ops, func(i, j int) bool {
return o.ops[i].path < o.ops[j].path
})
for k, v := range o.ops {
op := make(map[string]interface{})
if len(v.op) > 0 {
op["op"] = v.op
}
if len(v.from) > 0 {
op["from"] = v.from
}
if len(v.path) > 0 {
op["path"] = v.path
}
if v.value != nil {
op["value"] = v.value
}
ops[k] = op
}
return
}
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 {
if len(path) == 0 {
return "/" + part
} else {
if part[0] == '/' {
return path + part
} else {
return path + "/" + part
}
}
}
func (o *operations) op(op, from, path string, before, after interface{}) {
o.ops = append(o.ops, &operation{
op: op,
from: from,
path: path,
value: after,
before: before,
})
}
func (o *operations) diff(old, now map[string]interface{}, path string) {
for key, after := range now {
p := route(path, key)
// Check if the value existed
before, ok := old[key]
// Value did not previously exist
if !ok {
o.op("add", "", p, nil, after)
continue
}
// Data type is now completely different
if reflect.TypeOf(after) != reflect.TypeOf(before) {
o.op("replace", "", p, before, after)
continue
}
// Check whether the values have changed
o.vals(before, after, p)
}
for key, before := range old {
p := route(path, key)
// Check if the value exists
after, ok := now[key]
// Value now no longer exists
if !ok {
o.op("remove", "", p, before, after)
continue
}
}
var used []int
for i := len(o.ops) - 1; i >= 0; i-- {
if iv := o.ops[i]; !isIn(i, used) && iv.op == "add" {
for j := len(o.ops) - 1; j >= 0; j-- {
if jv := o.ops[j]; !isIn(j, used) && jv.op == "remove" {
if reflect.DeepEqual(iv.value, jv.before) {
used = append(used, []int{i, j}...)
o.op("move", jv.path, iv.path, nil, nil)
}
}
}
}
}
sort.Sort(sort.Reverse(sort.IntSlice(used)))
for _, i := range used {
o.ops = append(o.ops[:i], o.ops[i+1:]...)
}
}
func (o *operations) patch(old map[string]interface{}) (now map[string]interface{}) {
obj := data.Consume(old)
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":
switch val := obj.Get(path...).Data().(type) {
case fmt.Stringer:
txt := val.String()
dmp := diffmatchpatch.New()
if pch, err := dmp.PatchFromText(v.value.(string)); err == nil {
txt, _ := dmp.PatchApply(pch, txt)
obj.Set(txt, path...)
}
case string:
dmp := diffmatchpatch.New()
if pch, err := dmp.PatchFromText(v.value.(string)); err == nil {
txt, _ := dmp.PatchApply(pch, val)
obj.Set(txt, path...)
}
}
}
}
return old
}
func (o *operations) text(old, now string, path string) {
dmp := diffmatchpatch.New()
pch := dmp.PatchMake(old, now)
txt := dmp.PatchToText(pch)
o.op("change", "", path, old, txt)
}
func (o *operations) vals(old, now interface{}, path string) {
if reflect.TypeOf(old) != reflect.TypeOf(now) {
o.op("replace", "", path, old, now)
return
}
switch ov := old.(type) {
default:
if !reflect.DeepEqual(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:
if ov != now.(bool) {
o.op("replace", "", path, old, now)
}
case int64:
if ov != now.(int64) {
o.op("replace", "", path, old, now)
}
case float64:
if ov != now.(float64) {
o.op("replace", "", path, old, now)
}
case string:
if ov != now.(string) {
o.text(ov, now.(string), path)
}
case nil:
switch now.(type) {
case nil:
default:
o.op("replace", "", path, old, now)
}
case map[string]interface{}:
o.diff(ov, now.(map[string]interface{}), path)
case []interface{}:
o.arrs(ov, now.([]interface{}), path)
}
}
func (o *operations) arrs(old, now []interface{}, path string) {
var i int
for i = 0; i < len(old) && i < len(now); i++ {
o.vals(old[i], now[i], route(path, strconv.Itoa(i)))
}
for j := i; j < len(now); j++ {
if j >= len(old) || !reflect.DeepEqual(now[j], old[j]) {
o.op("add", "", route(path, strconv.Itoa(j)), nil, now[j])
}
}
for j := i; j < len(old); j++ {
if j >= len(now) || !reflect.DeepEqual(old[j], now[j]) {
o.op("remove", "", route(path, strconv.Itoa(j)), old[j], nil)
}
}
}