-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathupdate.go
242 lines (208 loc) · 6.89 KB
/
update.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
package grocery
import (
"errors"
"fmt"
"reflect"
"strings"
"time"
"github.com/redis/go-redis/v9"
)
// UpdateOptions provides options that may be passed to UpdateWithOptions if
// the default behavior of Update needs to be changed.
type UpdateOptions struct {
// Notify should be set to true if you would like a message to be published
// to the <struct name>:<id> channel once this update completes.
Notify bool
// SetZeroValues should be set to true if you would like to update all zero
// values in Redis (e.g. empty strings, 0 ints). By default, when creating
// a struct to pass to Update, you may not set each value, which is why
// this defaults to false. At this time, setting one zero value at a time
// is not supported.
SetZeroValues bool
// If you would like to run this store/update alongside other Redis
// updates, you may specify a pipeline.
Pipeline redis.Pipeliner
isStore bool
storeOverwrite bool
}
// Update updates an object with a given ID. By default, only non-zero values
// are updated in Redis, allowing you to update one property at a time.
//
// type Item struct {
// grocery.Base
// Name string
// Price float64
// }
//
// item := &Item{
// Price: 5.64,
// }
//
// // Name is not updated, but price is
// itemID := "asdf"
// db.Update(itemID, item)
func Update(id string, ptr interface{}) error {
return updateInternal(id, ptr, &UpdateOptions{})
}
// UpdateWithOptions updates an object in Redis, like Update, but with options.
func UpdateWithOptions(id string, ptr interface{}, opts *UpdateOptions) error {
return updateInternal(id, ptr, opts)
}
func updateInternal(id string, ptr interface{}, opts *UpdateOptions) error {
if id == "" {
return errors.New("ID must not be empty")
}
var val reflect.Value
var typ reflect.Type
switch reflect.TypeOf(ptr).Kind() {
case reflect.Ptr:
if reflect.TypeOf(ptr).Elem().Kind() != reflect.Struct {
return errors.New("ptr must be a struct pointer")
}
val = reflect.ValueOf(ptr).Elem()
typ = reflect.TypeOf(ptr).Elem()
case reflect.Struct:
val = reflect.ValueOf(ptr)
typ = reflect.TypeOf(ptr)
default:
return errors.New("ptr must be a struct pointer")
}
// Get prefix for the struct (e.g. 'answer:' from Answer)
prefix := strings.ToLower(typ.Name())
// Make sure the object exists on an update, or not on a store
exists, _ := C.Exists(ctx, prefix+":"+id).Result()
if opts.isStore && exists == 1 && !opts.storeOverwrite {
return fmt.Errorf("%s:%s already exists", prefix, id)
} else if !opts.isStore && exists == 0 {
return fmt.Errorf("%s:%s does not exist", prefix, id)
}
pip := opts.Pipeline
if opts.Pipeline == nil {
pip = C.TxPipeline()
}
for i := 0; i < typ.NumField(); i++ {
typeField := typ.Field(i)
structField := val.Field(i)
tagName := typeField.Tag.Get("grocery")
k := tagName
if tagName == "-" {
continue
} else if structField.Kind() == reflect.Struct && typeField.Anonymous {
// Skip embedded structs
continue
} else if strings.Contains(tagName, ",") {
tagParts := strings.Split(tagName, ",")
k = tagParts[0]
tagInfo := tagParts[1]
if tagInfo == "immutable" {
continue
}
} else if tagName == "" {
// Tag was not specified, assume field name
if len(typeField.Name) > 1 {
k = strings.ToLower(string(typeField.Name[0])) + string(typeField.Name[1:])
} else {
k = strings.ToLower(typeField.Name)
}
}
if !opts.SetZeroValues && structField.IsZero() {
continue
}
switch typeField.Type.Kind() {
case reflect.Ptr:
if structField.IsNil() {
continue
}
if typeField.Type == mapType || structField.Elem().FieldByName("CustomMapType").IsValid() {
pip.Del(ctx, prefix+":"+id+":"+k)
structField.MethodByName("Range").Call([]reflect.Value{
reflect.ValueOf(func(key, value interface{}) bool {
pip.HSet(ctx, prefix+":"+id+":"+k, key, value)
return true
}),
})
} else if typeField.Type == setType || structField.Elem().FieldByName("CustomSetType").IsValid() {
pip.Del(ctx, prefix+":"+id+":"+k)
structField.MethodByName("Range").Call([]reflect.Value{
reflect.ValueOf(func(key, value interface{}) bool {
pip.SAdd(ctx, prefix+":"+id+":"+k, key)
return true
}),
})
} else if !structField.Elem().FieldByName("Base").IsZero() {
val := structField.Elem().FieldByName("Base").FieldByName("ID").String()
pip.HSet(ctx, prefix+":"+id, k, val)
} else {
return fmt.Errorf("can't set unknown field '%s'", tagName)
}
case reflect.Slice:
// Delete old list before adding new entries
pip.Del(ctx, prefix+":"+id+":"+k)
for i := 0; i < structField.Len(); i++ {
if !structField.Index(i).Elem().FieldByName("Base").IsZero() {
itemID := structField.Index(i).Elem().FieldByName("Base").FieldByName("ID").String()
pip.RPush(ctx, prefix+":"+id+":"+k, itemID)
} else {
return fmt.Errorf("can't set unknown array item in %s", tagName)
}
}
case reflect.Map:
return fmt.Errorf("type of field '%s' must be changed to *grocery.Map", tagName)
case reflect.Struct:
switch structField.Type() {
case reflect.TypeOf(time.Now()):
timeVal := structField.MethodByName("Unix").Call([]reflect.Value{})[0].Int()
pip.HSet(ctx, prefix+":"+id, k, timeVal)
default:
return fmt.Errorf("can't set unknown struct for field '%s'", tagName)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
// Handle int alias types
val := structField.Convert(reflect.TypeOf(0)).Int()
pip.HSet(ctx, prefix+":"+id, k, val)
case reflect.String:
// Handle string alias types
val := structField.Convert(reflect.TypeOf("")).String()
pip.HSet(ctx, prefix+":"+id, k, val)
case reflect.Bool:
if loadFunc := structField.MethodByName("Load"); loadFunc.IsValid() {
// Skip custom boolean values; they don't get stored
continue
} else {
pip.HSet(ctx, prefix+":"+id, k, structField.Bool())
}
case reflect.Float64, reflect.Float32:
val := structField.Float()
pip.HSet(ctx, prefix+":"+id, k, val)
case reflect.Interface:
if structField.Type().Name() == "ModelHook" {
// Skip ModelHook fields
continue
}
return fmt.Errorf("don't know how to set interface field '%s'", k)
default:
return fmt.Errorf("don't know how to set field '%s'", k)
}
}
// Set updatedAt timestamp
pip.HSet(ctx, prefix+":"+id, "updatedAt", time.Now().Unix())
if opts.isStore {
// Set createdAt timestamp
pip.HSet(ctx, prefix+":"+id, "createdAt", time.Now().Unix())
// Call hook after calling store, if the object has one
if hook, ok := ptr.(ModelHook); ok {
hook.PostStore(pip)
}
}
if opts.Notify {
// Publish message if notify is enabled
pip.Publish(ctx, prefix+":"+id, "")
}
// Don't exec if a pipeline was provided to us
if opts.Pipeline == nil {
if _, err := pip.Exec(ctx); err != nil {
return err
}
}
return nil
}