-
Notifications
You must be signed in to change notification settings - Fork 84
/
diff.go
417 lines (355 loc) · 8.83 KB
/
diff.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package diff
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/vmihailenco/msgpack/v5"
)
const (
// CREATE represents when an element has been added
CREATE = "create"
// UPDATE represents when an element has been updated
UPDATE = "update"
// DELETE represents when an element has been removed
DELETE = "delete"
)
// DiffType represents an enum with all the supported diff types
type DiffType uint8
const (
UNSUPPORTED DiffType = iota
STRUCT
SLICE
ARRAY
STRING
BOOL
INT
UINT
FLOAT
MAP
PTR
INTERFACE
)
func (t DiffType) String() string {
switch t {
case STRUCT:
return "STRUCT"
case SLICE:
return "SLICE"
case ARRAY:
return "ARRAY"
case STRING:
return "STRING"
case BOOL:
return "BOOL"
case INT:
return "INT"
case UINT:
return "UINT"
case FLOAT:
return "FLOAT"
case MAP:
return "MAP"
case PTR:
return "PTR"
case INTERFACE:
return "INTERFACE"
default:
return "UNSUPPORTED"
}
}
// DiffFunc represents the built-in diff functions
type DiffFunc func([]string, reflect.Value, reflect.Value, interface{}) error
// Differ a configurable diff instance
type Differ struct {
TagName string
SliceOrdering bool
DisableStructValues bool
customValueDiffers []ValueDiffer
cl Changelog
AllowTypeMismatch bool
DiscardParent bool
StructMapKeys bool
FlattenEmbeddedStructs bool
ConvertCompatibleTypes bool
Filter FilterFunc
}
// Changelog stores a list of changed items
type Changelog []Change
// Change stores information about a changed item
type Change struct {
Type string `json:"type"`
Path []string `json:"path"`
From interface{} `json:"from"`
To interface{} `json:"to"`
parent interface{} `json:"parent"`
}
// ValueDiffer is an interface for custom differs
type ValueDiffer interface {
Match(a, b reflect.Value) bool
Diff(dt DiffType, df DiffFunc, cl *Changelog, path []string, a, b reflect.Value, parent interface{}) error
InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error)
}
// Changed returns true if both values differ
func Changed(a, b interface{}) bool {
cl, _ := Diff(a, b)
return len(cl) > 0
}
// Diff returns a changelog of all mutated values from both
func Diff(a, b interface{}, opts ...func(d *Differ) error) (Changelog, error) {
d, err := NewDiffer(opts...)
if err != nil {
return nil, err
}
return d.Diff(a, b)
}
// NewDiffer creates a new configurable diffing object
func NewDiffer(opts ...func(d *Differ) error) (*Differ, error) {
d := Differ{
TagName: "diff",
DiscardParent: false,
}
for _, opt := range opts {
err := opt(&d)
if err != nil {
return nil, err
}
}
return &d, nil
}
// FilterFunc is a function that determines whether to descend into a struct field.
// parent is the struct being examined and field is a field on that struct. path
// is the path to the field from the root of the diff.
type FilterFunc func(path []string, parent reflect.Type, field reflect.StructField) bool
// StructValues gets all values from a struct
// values are stored as "created" or "deleted" entries in the changelog,
// depending on the change type specified
func StructValues(t string, path []string, s interface{}) (Changelog, error) {
d := Differ{
TagName: "diff",
DiscardParent: false,
}
v := reflect.ValueOf(s)
return d.cl, d.structValues(t, path, v)
}
// FilterOut filter out the changes based on path. Paths may contain valid regexp to match items
func (cl *Changelog) FilterOut(path []string) Changelog {
var ncl Changelog
for _, c := range *cl {
if !pathmatch(path, c.Path) {
ncl = append(ncl, c)
}
}
return ncl
}
// Filter filter changes based on path. Paths may contain valid regexp to match items
func (cl *Changelog) Filter(path []string) Changelog {
var ncl Changelog
for _, c := range *cl {
if pathmatch(path, c.Path) {
ncl = append(ncl, c)
}
}
return ncl
}
func (d *Differ) getDiffType(a, b reflect.Value) (DiffType, DiffFunc) {
switch {
case are(a, b, reflect.Struct, reflect.Invalid):
return STRUCT, d.diffStruct
case are(a, b, reflect.Slice, reflect.Invalid):
return SLICE, d.diffSlice
case are(a, b, reflect.Array, reflect.Invalid):
return ARRAY, d.diffSlice
case are(a, b, reflect.String, reflect.Invalid):
return STRING, d.diffString
case are(a, b, reflect.Bool, reflect.Invalid):
return BOOL, d.diffBool
case are(a, b, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Invalid):
return INT, d.diffInt
case are(a, b, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Invalid):
return UINT, d.diffUint
case are(a, b, reflect.Float32, reflect.Float64, reflect.Invalid):
return FLOAT, d.diffFloat
case are(a, b, reflect.Map, reflect.Invalid):
return MAP, d.diffMap
case are(a, b, reflect.Ptr, reflect.Invalid):
return PTR, d.diffPtr
case are(a, b, reflect.Interface, reflect.Invalid):
return INTERFACE, d.diffInterface
default:
return UNSUPPORTED, nil
}
}
// Diff returns a changelog of all mutated values from both
func (d *Differ) Diff(a, b interface{}) (Changelog, error) {
// reset the state of the diff
d.cl = Changelog{}
return d.cl, d.diff([]string{}, reflect.ValueOf(a), reflect.ValueOf(b), nil)
}
func (d *Differ) diff(path []string, a, b reflect.Value, parent interface{}) error {
//look and see if we need to discard the parent
if parent != nil {
if d.DiscardParent || reflect.TypeOf(parent).Kind() != reflect.Struct {
parent = nil
}
}
// check if types match or are
if invalid(a, b) {
if d.AllowTypeMismatch {
d.cl.Add(UPDATE, path, a.Interface(), b.Interface())
return nil
}
return ErrTypeMismatch
}
// get the diff type and the corresponding built-int diff function to handle this type
diffType, diffFunc := d.getDiffType(a, b)
// first go through custom diff functions
if len(d.customValueDiffers) > 0 {
for _, vd := range d.customValueDiffers {
if vd.Match(a, b) {
err := vd.Diff(diffType, diffFunc, &d.cl, path, a, b, parent)
if err != nil {
return err
}
return nil
}
}
}
// then built-in diff functions
if diffType == UNSUPPORTED {
return errors.New("unsupported type: " + a.Kind().String())
}
return diffFunc(path, a, b, parent)
}
func (cl *Changelog) Add(t string, path []string, ftco ...interface{}) {
change := Change{
Type: t,
Path: path,
From: ftco[0],
To: ftco[1],
}
if len(ftco) > 2 {
change.parent = ftco[2]
}
(*cl) = append((*cl), change)
}
func tagName(tag string, f reflect.StructField) string {
t := f.Tag.Get(tag)
parts := strings.Split(t, ",")
if len(parts) < 1 {
return "-"
}
return parts[0]
}
func identifier(tag string, v reflect.Value) interface{} {
if v.Kind() != reflect.Struct {
return nil
}
for i := 0; i < v.NumField(); i++ {
if hasTagOption(tag, v.Type().Field(i), "identifier") {
return v.Field(i).Interface()
}
}
return nil
}
func hasTagOption(tag string, f reflect.StructField, opt string) bool {
parts := strings.Split(f.Tag.Get(tag), ",")
if len(parts) < 2 {
return false
}
for _, option := range parts[1:] {
if option == opt {
return true
}
}
return false
}
func swapChange(t string, c Change) Change {
nc := Change{
Type: t,
Path: c.Path,
}
switch t {
case CREATE:
nc.To = c.To
case DELETE:
nc.From = c.To
}
return nc
}
func idComplex(v interface{}) string {
switch v := v.(type) {
case string:
return v
case int:
return strconv.Itoa(v)
default:
b, err := msgpack.Marshal(v)
if err != nil {
panic(err)
}
return string(b)
}
}
func idstring(v interface{}) string {
switch v := v.(type) {
case string:
return v
case int:
return strconv.Itoa(v)
default:
return fmt.Sprint(v)
}
}
func invalid(a, b reflect.Value) bool {
if a.Kind() == b.Kind() {
return false
}
if a.Kind() == reflect.Invalid {
return false
}
if b.Kind() == reflect.Invalid {
return false
}
return true
}
func are(a, b reflect.Value, kinds ...reflect.Kind) bool {
var amatch, bmatch bool
for _, k := range kinds {
if a.Kind() == k {
amatch = true
}
if b.Kind() == k {
bmatch = true
}
}
return amatch && bmatch
}
func AreType(a, b reflect.Value, types ...reflect.Type) bool {
var amatch, bmatch bool
for _, t := range types {
if a.Kind() != reflect.Invalid {
if a.Type() == t {
amatch = true
}
}
if b.Kind() != reflect.Invalid {
if b.Type() == t {
bmatch = true
}
}
}
return amatch && bmatch
}
func copyAppend(src []string, elems ...string) []string {
dst := make([]string, len(src)+len(elems))
copy(dst, src)
for i := len(src); i < len(src)+len(elems); i++ {
dst[i] = elems[i-len(src)]
}
return dst
}