-
Notifications
You must be signed in to change notification settings - Fork 1
/
loader.go
152 lines (124 loc) · 4.61 KB
/
loader.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
package patcher
import (
"errors"
"reflect"
"slices"
"strings"
)
var (
// ErrInvalidType is returned when the provided type is not a pointer to a struct
ErrInvalidType = errors.New("invalid type: must pointer to struct")
)
type IgnoreFieldsFunc func(field reflect.StructField, oldValue, newValue any) bool
type loader struct {
// includeZeroValues determines whether zero values should be included in the patch
includeZeroValues bool
// includeNilValues determines whether nil values should be included in the patch
includeNilValues bool
// ignoreFields is a list of fields to ignore when patching
ignoreFields []string
// ignoreFieldsFunc is a function that determines whether a field should be ignored
//
// This func should return true is the field is to be ignored
ignoreFieldsFunc IgnoreFieldsFunc
}
func newLoader(opts ...LoaderOption) *loader {
// Default options
l := &loader{
includeZeroValues: false,
includeNilValues: false,
ignoreFields: nil,
ignoreFieldsFunc: nil,
}
for _, opt := range opts {
opt(l)
}
return l
}
// LoadDiff inserts the fields provided in the new struct pointer into the old struct pointer and injects the new
// values into the old struct
//
// Note that it only pushes non-zero value updates, meaning you cannot set any field to zero, the empty string, etc.
// This is configurable by setting the includeZeroValues option to true or for nil values by setting includeNilValues.
// Please see the LoaderOption's for more configuration options.
//
// This can be if you are inserting a patch into an existing object but require a new object to be returned with
// all fields
func LoadDiff[T any](old *T, newT *T, opts ...LoaderOption) error {
return newLoader(opts...).loadDiff(old, newT)
}
func (l *loader) loadDiff(old, newT any) error {
if !isPointerToStruct(old) || !isPointerToStruct(newT) {
return ErrInvalidType
}
oElem := reflect.ValueOf(old).Elem()
nElem := reflect.ValueOf(newT).Elem()
for i := 0; i < oElem.NumField(); i++ {
// Include only exported fields
if !oElem.Field(i).CanSet() || !nElem.Field(i).CanSet() {
continue
}
// Handle embedded structs (Anonymous fields)
if oElem.Type().Field(i).Anonymous {
// If the embedded field is a pointer, dereference it
if oElem.Field(i).Kind() == reflect.Ptr {
if !oElem.Field(i).IsNil() && !nElem.Field(i).IsNil() { // If both are not nil, we need to recursively call LoadDiff
if err := l.loadDiff(oElem.Field(i).Interface(), nElem.Field(i).Interface()); err != nil {
return err
}
} else if nElem.Field(i).IsValid() && !nElem.Field(i).IsNil() {
oElem.Field(i).Set(nElem.Field(i))
}
continue
}
if err := l.loadDiff(oElem.Field(i).Addr().Interface(), nElem.Field(i).Addr().Interface()); err != nil {
return err
}
continue
}
// If the field is a struct, we need to recursively call LoadDiff
if oElem.Field(i).Kind() == reflect.Struct {
if err := l.loadDiff(oElem.Field(i).Addr().Interface(), nElem.Field(i).Addr().Interface()); err != nil {
return err
}
continue
}
// See if the field should be ignored.
if l.checkSkipField(oElem.Type().Field(i), oElem.Field(i).Interface(), nElem.Field(i).Interface()) {
continue
}
// Compare the old and new fields.
//
// New fields take priority over old fields if they are provided based on the configuration.
if nElem.Field(i).Kind() != reflect.Ptr && (!nElem.Field(i).IsZero() || l.includeZeroValues) {
oElem.Field(i).Set(nElem.Field(i))
} else if nElem.Field(i).Kind() == reflect.Ptr && (!nElem.Field(i).IsNil() || l.includeNilValues) {
oElem.Field(i).Set(nElem.Field(i))
}
}
return nil
}
func (l *loader) checkSkipField(field reflect.StructField, oldValue, newValue any) bool {
// The ignore fields tag takes precedence over the ignore fields list
if l.checkSkipTag(field) {
return true
}
return l.ignoredFieldsCheck(field, oldValue, newValue)
}
func (l *loader) checkSkipTag(field reflect.StructField) bool {
val, ok := field.Tag.Lookup(TagOptsName)
if !ok {
return false
}
tags := strings.Split(val, TagOptSeparator)
return slices.Contains(tags, TagOptSkip)
}
func (l *loader) ignoredFieldsCheck(field reflect.StructField, oldValue, newValue any) bool {
return l.checkIgnoredFields(strings.ToLower(field.Name)) || l.checkIgnoreFunc(field, oldValue, newValue)
}
func (l *loader) checkIgnoreFunc(field reflect.StructField, oldValue, newValue any) bool {
return l.ignoreFieldsFunc != nil && l.ignoreFieldsFunc(field, oldValue, newValue)
}
func (l *loader) checkIgnoredFields(field string) bool {
return len(l.ignoreFields) > 0 && slices.Contains(l.ignoreFields, strings.ToLower(field))
}