-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathmodel.go
executable file
·410 lines (372 loc) · 12.7 KB
/
model.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
// Copyright 2015 Alex Browne. All rights reserved.
// Use of this source code is governed by the MIT
// license, which can be found in the LICENSE file.
// File model.go contains code related to the Model interface.
// The Register() method and associated methods are also included here.
package zoom
import (
"fmt"
"reflect"
"strings"
"time"
"github.com/garyburd/redigo/redis"
)
// RandomID can be embedded in any model struct in order to satisfy
// the Model interface. The first time the ModelID method is called
// on an embedded RandomID, it will generate a pseudo-random id which
// is highly likely to be unique.
type RandomID struct {
ID string
}
// Model is an interface encapsulating anything that can be saved and
// retrieved by Zoom. The only requirement is that a Model must have
// a getter and a setter for a unique id property.
type Model interface {
ModelID() string
SetModelID(string)
}
// ModelID returns the id of the model, satisfying the Model interface.
// If r.ID is an empty string, it will generate a pseudo-random id which
// is highly likely to be unique.
func (r *RandomID) ModelID() string {
if r.ID == "" {
r.ID = generateRandomID()
}
return r.ID
}
// SetModelID sets the id of the model, satisfying the Model interface.
func (r *RandomID) SetModelID(id string) {
r.ID = id
}
// modelSpec contains parsed information about a particular type of model.
type modelSpec struct {
typ reflect.Type
name string
fieldsByName map[string]*fieldSpec
fields []*fieldSpec
fallback MarshalerUnmarshaler
}
// fieldSpec contains parsed information about a particular field.
type fieldSpec struct {
kind fieldKind
name string
redisName string
typ reflect.Type
indexKind indexKind
}
// fieldKind is the kind of a particular field, and is either a primitive,
// a pointer, or an inconvertible.
type fieldKind int
const (
primativeField fieldKind = iota // any primitive type
pointerField // pointer to any primitive type
inconvertibleField // all other types
)
// indexKind is the kind of an index, and is either noIndex, numericIndex,
// stringIndex, or booleanIndex.
type indexKind int
const (
noIndex indexKind = iota
numericIndex
stringIndex
booleanIndex
)
// compilesModelSpec examines typ using reflection, parses its fields,
// and returns a modelSpec.
func compileModelSpec(typ reflect.Type) (*modelSpec, error) {
ms := &modelSpec{
name: getDefaultModelSpecName(typ),
fieldsByName: map[string]*fieldSpec{},
typ: typ,
}
// Iterate through fields
elem := typ.Elem()
numFields := elem.NumField()
for i := 0; i < numFields; i++ {
field := elem.Field(i)
// Skip unexported fields. Prior to go 1.6, field.PkgPath won't give us
// the behavior we want. Unlike packages such as encoding/json and
// encoding/gob, Zoom does not save unexported embedded structs with
// exported fields. So instead, we check if the first character of the
// field name is lowercase.
if strings.ToLower(field.Name[0:1]) == field.Name[0:1] {
continue
}
// Skip the RandomID field
if field.Type == reflect.TypeOf(RandomID{}) {
continue
}
// Parse the "redis" tag
tag := field.Tag
redisTag := tag.Get("redis")
if redisTag == "-" {
continue // skip field
}
fs := &fieldSpec{name: field.Name, typ: field.Type}
ms.fieldsByName[fs.name] = fs
ms.fields = append(ms.fields, fs)
if redisTag != "" {
fs.redisName = redisTag
} else {
fs.redisName = fs.name
}
// Parse the "zoom" tag (currently only "index" is supported)
zoomTag := tag.Get("zoom")
shouldIndex := false
if zoomTag != "" {
options := strings.Split(zoomTag, ",")
for _, op := range options {
switch op {
case "index":
shouldIndex = true
default:
return nil, fmt.Errorf("zoom: unrecognized option specified in struct tag: %s", op)
}
}
}
// Detect the kind of the field and (if applicable) the kind of the index
if typeIsPrimative(field.Type) {
// Primitive
fs.kind = primativeField
if shouldIndex {
if err := setIndexKind(fs, field.Type); err != nil {
return nil, err
}
}
} else if field.Type.Kind() == reflect.Ptr && typeIsPrimative(field.Type.Elem()) {
// Pointer to a primitive
fs.kind = pointerField
if shouldIndex {
if err := setIndexKind(fs, field.Type.Elem()); err != nil {
return nil, err
}
}
} else {
// All other types are considered inconvertible
if shouldIndex {
return nil, fmt.Errorf("zoom: Requested index on unsupported type %s", field.Type)
}
fs.kind = inconvertibleField
}
}
return ms, nil
}
// getDefaultModelSpecName returns the default name for the given type, which is
// simply the name of the type without the package prefix or dereference
// operators.
func getDefaultModelSpecName(typ reflect.Type) string {
// Strip any dereference operators
for typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
nameWithPackage := typ.String()
// Strip the package name
return strings.Join(strings.Split(nameWithPackage, ".")[1:], "")
}
// setIndexKind sets the indexKind field of fs based on fieldType.
func setIndexKind(fs *fieldSpec, fieldType reflect.Type) error {
switch {
case typeIsNumeric(fieldType):
fs.indexKind = numericIndex
case typeIsString(fieldType):
fs.indexKind = stringIndex
case typeIsBool(fieldType):
fs.indexKind = booleanIndex
default:
return fmt.Errorf("zoom: Requested index on unsupported type %s", fieldType.String())
}
return nil
}
// allIndexKey returns a key which is used in redis to store all the ids of every model of a
// given type
func (ms *modelSpec) indexKey() string {
return ms.name + ":all"
}
// modelKey returns the key that identifies a hash in the database
// which contains all the fields of the model corresponding to the given
// id. It returns an error iff id is empty.
func (ms *modelSpec) modelKey(id string) (string, error) {
if id == "" {
return "", fmt.Errorf("zoom: Error in modelKey: id was empty")
}
return ms.name + ":" + id, nil
}
// fieldNames returns all the field names for the given modelSpec
func (ms modelSpec) fieldNames() []string {
names := make([]string, len(ms.fields))
count := 0
for _, field := range ms.fields {
names[count] = field.name
count++
}
return names
}
// fieldRedisNames returns all the redis names (which might be custom names specified via
// the `redis:"custonName"` struct tag) for each field in the given modelSpec
func (ms modelSpec) fieldRedisNames() []string {
names := make([]string, len(ms.fields))
count := 0
for _, field := range ms.fields {
names[count] = field.redisName
count++
}
return names
}
func (ms modelSpec) redisNamesForFieldNames(fieldNames []string) ([]string, error) {
redisNames := []string{}
for _, fieldName := range fieldNames {
fs, found := ms.fieldsByName[fieldName]
if !found {
return nil, fmt.Errorf("Type %s has no field named %s", ms.typ.Name(), fieldName)
}
redisNames = append(redisNames, fs.redisName)
}
return redisNames, nil
}
// fieldIndexKey returns the key for the sorted set used to index the field identified
// by fieldName. It returns an error if fieldName does not identify a field in the spec
// or if the field it identifies is not an indexed field.
func (ms *modelSpec) fieldIndexKey(fieldName string) (string, error) {
fs, found := ms.fieldsByName[fieldName]
if !found {
return "", fmt.Errorf("Type %s has no field named %s", ms.typ.Name(), fieldName)
} else if fs.indexKind == noIndex {
return "", fmt.Errorf("%s.%s is not an indexed field", ms.typ.Name(), fieldName)
}
return ms.name + ":" + fs.redisName, nil
}
// sortArgs returns arguments that can be used to get all the fields in includeFields
// for all the models which have corresponding ids in setKey. Any fields not in
// includeFields will not be included in the arguments and will not be retrieved from
// redis when the command is eventually run. If limit or offset are not 0, the LIMIT
// option will be added to the arguments with the given limit and offset. setKey must
// be the key of a set or a sorted set which consists of model ids. The arguments
// use they "BY nosort" option, so if a specific order is required, the setKey should be
// a sorted set.
func (ms *modelSpec) sortArgs(idsKey string, redisFieldNames []string, limit int, offset uint, reverse bool) redis.Args {
args := redis.Args{idsKey, "BY", "nosort"}
for _, fieldName := range redisFieldNames {
args = append(args, "GET", ms.name+":*->"+fieldName)
}
// We always want to get the id
args = append(args, "GET", "#")
if !(limit == 0 && offset == 0) {
args = append(args, "LIMIT", offset, limit)
}
if reverse {
args = append(args, "DESC")
} else {
args = append(args, "ASC")
}
return args
}
// checkModelType returns an error iff model is not of the registered type that
// corresponds to modelSpec.
func (ms *modelSpec) checkModelType(model Model) error {
if reflect.TypeOf(model) != ms.typ {
return fmt.Errorf("model was the wrong type. Expected %s but got %T", ms.typ.String(), model)
}
return nil
}
// checkModelsType returns an error iff models is not a pointer to a slice of models of the
// registered type that corresponds to modelSpec.
func (ms *modelSpec) checkModelsType(models interface{}) error {
if reflect.TypeOf(models).Kind() != reflect.Ptr {
return fmt.Errorf("models should be a pointer to a slice or array of models")
}
modelsVal := reflect.ValueOf(models).Elem()
elemType := modelsVal.Type().Elem()
switch {
case !typeIsSliceOrArray(modelsVal.Type()):
return fmt.Errorf("models should be a pointer to a slice or array of models")
case !typeIsPointerToStruct(elemType):
return fmt.Errorf("the elements in models should be pointers to structs")
case elemType != ms.typ:
return fmt.Errorf("models were the wrong type. Expected slice or array of %s but got %T", ms.typ.String(), models)
}
return nil
}
// modelRef represents a reference to a particular model. It consists of the model object
// itself and a pointer to the corresponding spec. This allows us to avoid constant lookups
// in the modelTypeToSpec map.
type modelRef struct {
collection *Collection
model Model
spec *modelSpec
}
// value is an alias for reflect.ValueOf(mr.model)
func (mr *modelRef) value() reflect.Value {
return reflect.ValueOf(mr.model)
}
// elemValue dereferences the model and returns the
// underlying struct. If the model is a nil pointer,
// it will panic if the model is a nil pointer
func (mr *modelRef) elemValue() reflect.Value {
if mr.value().IsNil() {
msg := fmt.Sprintf("zoom: panic in elemValue(). Model of type %T was nil", mr.model)
panic(msg)
}
return mr.value().Elem()
}
// fieldValue is an alias for mr.elemValue().FieldByName(name). It panics if
// the model behind mr does not have a field with the given name or if
// the model is nil.
func (mr *modelRef) fieldValue(name string) reflect.Value {
return mr.elemValue().FieldByName(name)
}
// key returns a key which is used in redis to store the model
func (mr *modelRef) key() string {
return mr.spec.name + ":" + mr.model.ModelID()
}
// mainHashArgs returns the args for the main hash for this model. Typically
// these args should part of an HMSET command.
func (mr *modelRef) mainHashArgs() (redis.Args, error) {
return mr.mainHashArgsForFields(mr.spec.fieldNames())
}
// mainHashArgsForFields is like mainHashArgs but only returns the hash
// fields which match the given fieldNames.
func (mr *modelRef) mainHashArgsForFields(fieldNames []string) (redis.Args, error) {
args := redis.Args{mr.key()}
ms := mr.spec
for _, fs := range ms.fields {
// Skip fields whose names do not appear in fieldNames.
if !stringSliceContains(fieldNames, fs.name) {
continue
}
fieldVal := mr.fieldValue(fs.name)
switch fs.kind {
case primativeField:
// Add a special case for time.Duration. By default, the redigo driver
// will fall back to fmt.Sprintf, but we want to save it as an int64 in
// this case.
if fs.typ == reflect.TypeOf(time.Duration(0)) {
args = args.Add(fs.redisName, int64(fieldVal.Interface().(time.Duration)))
} else {
args = args.Add(fs.redisName, fieldVal.Interface())
}
case pointerField:
if !fieldVal.IsNil() {
args = args.Add(fs.redisName, fieldVal.Elem().Interface())
} else {
args = args.Add(fs.redisName, "NULL")
}
case inconvertibleField:
switch fieldVal.Type().Kind() {
// For nilable types that are nil store NULL
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Interface:
if fieldVal.IsNil() {
args = args.Add(fs.redisName, "NULL")
continue
}
}
// For inconvertibles, that are not nil, convert the value to bytes
// using the gob package.
valBytes, err := mr.spec.fallback.Marshal(fieldVal.Interface())
if err != nil {
return nil, err
}
args = args.Add(fs.redisName, valBytes)
}
}
return args, nil
}