-
Notifications
You must be signed in to change notification settings - Fork 0
/
shallow.go
141 lines (122 loc) · 4.74 KB
/
shallow.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
package shallow
import (
"reflect"
"strings"
"github.com/proemergotech/errors/v2"
)
type options struct {
tag string
}
// Diff compare structs based on the following rule: for every field of the first struct,
// if the field's tag (specified by tag option, default "json") can be found in the keys map,
// compare the corresponding value in the first struct to the value in the second struct.
// If the keys map is nil, all field will be compared.
//
// First and second must be a pointer to a non-nil struct of the same type.
//
// Returns with a list of diff keys. This list can include elements that are NOT actually different if the first struct
// and the second struct had the same value for the given key, and the keys map contained this key.
//
// Traverses anonym fields with struct or struct pointer type, even if they are nested (anonym structs
// within anonym structs). Other types of anonym fields are not supported and will raise an error.
//
// Does NOT check nested fields other than anonym.
func Diff(first interface{}, second interface{}, keys map[string]interface{}, opts ...Option) (diffKeys []string, err error) {
return process(first, second, keys, false, opts...)
}
// Merge the update struct into the dest struct based on the following rule: for every field of the update struct,
// if the field's tag (specified by tag option, default "json") can be found in the keys map,
// set the corresponding value in the dest struct to the value in the update struct.
//
// Dest and update must be a pointer to a non-nil struct of the same type.
//
// Returns with a list of updated keys. This list can include elements that are NOT actually changed if the dest struct
// and the update struct had the same value for the given key, and the keys map contained this key.
//
// Traverses anonym fields with struct or struct pointer type, even if they are nested (anonym structs
// within anonym structs). Other types of anonym fields are not supported and will raise an error.
//
// Does NOT merge nested fields other than anonym. For these, either the dest value
// is kept intact, or the update value is used, but the two are never merged.
func Merge(dest interface{}, update interface{}, keys map[string]interface{}, opts ...Option) (updatedKeys []string, err error) {
return process(dest, update, keys, true, opts...)
}
func process(target interface{}, source interface{}, keys map[string]interface{}, merge bool, opts ...Option) (processedKeys []string, err error) {
targetV := reflect.ValueOf(target)
sourceV := reflect.ValueOf(source)
if targetV.Kind() != reflect.Ptr || targetV.Elem().Kind() != reflect.Struct {
return nil, errors.New("target and source must be a non-nil pointer to a struct with the same type")
}
if targetV.Type() != sourceV.Type() {
return nil, errors.New("target and source must be a non-nil pointer to a struct with the same type")
}
if targetV.IsNil() || sourceV.IsNil() {
return nil, errors.New("target and source must be a non-nil pointer to a struct with the same type")
}
o := &options{
tag: "json",
}
for _, opt := range opts {
opt(o)
}
processedKeys = make([]string, 0)
err = processStructs(targetV.Elem(), sourceV.Elem(), o.tag, keys, &processedKeys, merge)
if err != nil {
return nil, err
}
return processedKeys, nil
}
func processStructs(targetV reflect.Value, sourceV reflect.Value, tag string, keys map[string]interface{}, processedKeys *[]string, merge bool) error {
for i := 0; i < sourceV.NumField(); i++ {
ft := sourceV.Type().Field(i)
if ft.Anonymous {
destAVal := targetV.Field(i)
upAVal := sourceV.Field(i)
if destAVal.Kind() == reflect.Struct {
err := processStructs(destAVal, upAVal, tag, keys, processedKeys, merge)
if err != nil {
return err
}
} else if destAVal.Kind() == reflect.Ptr && destAVal.Elem().Kind() == reflect.Struct {
if upAVal.IsNil() {
continue
}
if destAVal.IsNil() {
destAVal.Set(reflect.New(destAVal.Type().Elem()))
}
err := processStructs(destAVal.Elem(), upAVal.Elem(), tag, keys, processedKeys, merge)
if err != nil {
return err
}
} else {
return errors.New("this method only handles anonym fields of kind struct or pointer to struct")
}
continue
}
tagVal := ft.Tag.Get(tag)
tagVal = strings.SplitN(tagVal, ",", 2)[0]
if tagVal == "" {
continue
}
if keys != nil {
if _, ok := keys[tagVal]; !ok {
continue
}
}
if reflect.DeepEqual(targetV.Field(i).Interface(), sourceV.Field(i).Interface()) {
continue
}
*processedKeys = append(*processedKeys, tagVal)
if merge {
targetV.Field(i).Set(sourceV.Field(i))
}
}
return nil
}
type Option func(*options)
// UseTag can be used to use struct tags other than json.
func UseTag(tag string) Option {
return func(o *options) {
o.tag = tag
}
}