-
Notifications
You must be signed in to change notification settings - Fork 32
/
sheriff.go
372 lines (320 loc) · 10.3 KB
/
sheriff.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
package sheriff
import (
"encoding"
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/hashicorp/go-version"
)
// A KVStore is a simple key-value store.
// The default implementation uses a map[string]interface{}.
// This is fast, however will lead to inconsistent ordering of the keys in the generated JSON.
// A custom implementation can be used to maintain the order of the keys.
type KVStore interface {
Set(k string, v interface{})
Each(f func(k string, v interface{}))
}
// A FieldFilter is a function that decides whether a field should be marshalled or not.
// If it returns true, the field will be marshalled, otherwise it will be skipped.
type FieldFilter func(field reflect.StructField) (bool, error)
// Options determine which struct fields are being added to the output map.
type Options struct {
// The FieldFilter makes the decision whether a field should be marshalled or not.
// It receives the reflect.StructField of the field and should return true if the field should be included.
// If this is not set then the default FieldFilter will be used, which uses the Groups and ApiVersion fields.
// Setting this value will result in the other options being ignored.
FieldFilter FieldFilter
// Groups determine which fields are getting marshalled based on the groups tag.
// A field with multiple groups (comma-separated) will result in marshalling of that
// field if one of their groups is specified.
Groups []string
// ApiVersion sets the API version to use when marshalling.
// The tags `since` and `until` use the API version setting.
// Specifying the API version as "1.0.0" and having an until setting of "2"
// will result in the field being marshalled.
// Specifying a since setting of "2" with the same API version specified,
// will not marshal the field.
ApiVersion *version.Version
// IncludeEmptyTag determines whether a field without the
// `groups` tag should be marshalled ot not.
// This option is false by default.
IncludeEmptyTag bool
// The KVStoreFactory is a function that returns a new KVStore.
// The default implementation uses a map[string]interface{}, which is fast but does not maintain the order of the
// keys.
// A custom implementation can be used to maintain the order of the keys, i.e. using github.com/wk8/go-ordered-map
KVStoreFactory func() KVStore
// This is used internally so that we can propagate anonymous fields groups tag to all child field.
nestedGroupsMap map[string][]string
}
// MarshalInvalidTypeError is an error returned to indicate the wrong type has been
// passed to Marshal.
type MarshalInvalidTypeError struct {
// t reflects the type of the data
t reflect.Kind
// data contains the passed data itself
data interface{}
}
func (e MarshalInvalidTypeError) Error() string {
return fmt.Sprintf("marshaller: Unable to marshal type %s. Struct required.", e.t)
}
// Marshaller is the interface models have to implement in order to conform to marshalling.
type Marshaller interface {
Marshal(options *Options) (interface{}, error)
}
// Marshal encodes the passed data into a map which can be used to pass to json.Marshal().
//
// If the passed argument `data` is a struct, the return value will be of type `map[string]interface{}`.
// In all other cases we can't derive the type in a meaningful way and is therefore an `interface{}`.
func Marshal(options *Options, data interface{}) (interface{}, error) {
v := reflect.ValueOf(data)
if !v.IsValid() || v.Kind() == reflect.Ptr && v.IsNil() {
return data, nil
}
t := v.Type()
// Initialise nestedGroupsMap,
// TODO: this may impact the performance, find a better place for this.
if options.nestedGroupsMap == nil {
options.nestedGroupsMap = make(map[string][]string)
}
if options.FieldFilter == nil {
options.FieldFilter = createDefaultFieldFilter(options)
}
if options.KVStoreFactory == nil {
options.KVStoreFactory = func() KVStore {
return kvStore{}
}
}
if t.Kind() == reflect.Ptr {
// follow pointer
t = t.Elem()
}
if v.Kind() == reflect.Ptr {
// follow pointer
v = v.Elem()
}
if t.Kind() != reflect.Struct {
return marshalValue(options, v)
}
dest := options.KVStoreFactory()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
val := v.Field(i)
jsonTagVal, jsonTagExists := field.Tag.Lookup("json")
jsonTag, jsonOpts := parseTag(jsonTagVal)
// If no json tag is provided, use the field Name
if jsonTag == "" {
jsonTag = field.Name
}
if jsonTag == "-" {
continue
}
if jsonOpts.Contains("omitempty") && isEmptyValue(val) {
continue
}
// skip unexported fields
if !val.IsValid() || !val.CanInterface() {
continue
}
quoted := false
if jsonOpts.Contains("string") {
switch val.Kind() {
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64,
reflect.String:
quoted = true
}
}
// if there is an anonymous field which is a struct
// we want the childs exposed at the toplevel to be
// consistent with the embedded json marshaller
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
// we can skip the group checkif if the field is a composition field
isEmbeddedField := field.Anonymous && val.Kind() == reflect.Struct
if isEmbeddedField && field.Type.Kind() == reflect.Struct {
tt := field.Type
parentGroups := strings.Split(field.Tag.Get("groups"), ",")
for i := 0; i < tt.NumField(); i++ {
nestedField := tt.Field(i)
options.nestedGroupsMap[nestedField.Name] = parentGroups
}
}
if !isEmbeddedField {
include, err := options.FieldFilter(field)
if err != nil {
return nil, err
}
if !include {
// skip this field
continue
}
}
v, err := marshalValue(options, val)
if err != nil {
return nil, err
}
if quoted {
v = fmt.Sprintf("%v", v)
}
// when a composition field we want to bring the child
// nodes to the top
nestedVal, ok := v.(KVStore)
if !jsonTagExists && isEmbeddedField && ok {
nestedVal.Each(func(k string, v interface{}) {
dest.Set(k, v)
})
} else {
dest.Set(jsonTag, v)
}
}
return dest, nil
}
// createDefaultFieldFilter creates a default FieldFilter function which uses the options.Groups and options.ApiVersion
// fields in order to determine whether a field should be marshalled or not.
func createDefaultFieldFilter(options *Options) FieldFilter {
checkGroups := len(options.Groups) > 0
return func(field reflect.StructField) (bool, error) {
if checkGroups {
var groups []string
if field.Tag.Get("groups") != "" {
groups = strings.Split(field.Tag.Get("groups"), ",")
}
if len(groups) == 0 && options.nestedGroupsMap[field.Name] != nil {
groups = append(groups, options.nestedGroupsMap[field.Name]...)
}
// Marshall the field if
// - it has at least one of the requested groups
// or
// - it has no group and 'IncludeEmptyTag' is set to true
shouldShow := listContains(groups, options.Groups) || (len(groups) == 0 && options.IncludeEmptyTag)
// Prevent marshalling of the field if
// - it should not be shown (above)
// or
// - it has no groups and 'IncludeEmptyTag' is set to false
shouldHide := !shouldShow || (len(groups) == 0 && !options.IncludeEmptyTag)
if shouldHide {
// skip this field
return false, nil
}
}
if since := field.Tag.Get("since"); since != "" {
sinceVersion, err := version.NewVersion(since)
if err != nil {
return true, err
}
if options.ApiVersion.LessThan(sinceVersion) {
// skip this field
return false, nil
}
}
if until := field.Tag.Get("until"); until != "" {
untilVersion, err := version.NewVersion(until)
if err != nil {
return true, err
}
if options.ApiVersion.GreaterThan(untilVersion) {
// skip this field
return false, nil
}
}
return true, nil
}
}
// marshalValue is being used for getting the actual value of a field.
//
// There is support for types implementing the Marshaller interface, arbitrary structs, slices, maps and base types.
func marshalValue(options *Options, v reflect.Value) (interface{}, error) {
// return nil on nil pointer struct fields
if !v.IsValid() || !v.CanInterface() {
return nil, nil
}
val := v.Interface()
if marshaller, ok := val.(Marshaller); ok {
return marshaller.Marshal(options)
}
// types which are e.g. structs, slices or maps and implement one of the following interfaces should not be
// marshalled by sheriff because they'll be correctly marshalled by json.Marshal instead.
// Otherwise (e.g. net.IP) a byte slice may be output as a list of uints instead of as an IP string.
// This needs to be checked for both value and pointer types.
switch val.(type) {
case json.Marshaler, encoding.TextMarshaler, fmt.Stringer:
return val, nil
}
if v.CanAddr() {
addrVal := v.Addr().Interface()
switch addrVal.(type) {
case json.Marshaler, encoding.TextMarshaler, fmt.Stringer:
return addrVal, nil
}
}
k := v.Kind()
switch k {
case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
if v.IsNil() {
return val, nil
}
}
if k == reflect.Ptr {
v = v.Elem()
val = v.Interface()
k = v.Kind()
}
if k == reflect.Interface || k == reflect.Struct {
return Marshal(options, val)
}
if k == reflect.Slice {
l := v.Len()
dest := make([]interface{}, l)
for i := 0; i < l; i++ {
d, err := marshalValue(options, v.Index(i))
if err != nil {
return nil, err
}
dest[i] = d
}
return dest, nil
}
if k == reflect.Map {
mapKeys := v.MapKeys()
if len(mapKeys) == 0 {
return val, nil
}
if mapKeys[0].Kind() != reflect.String {
return nil, MarshalInvalidTypeError{t: mapKeys[0].Kind(), data: val}
}
dest := options.KVStoreFactory()
for _, key := range mapKeys {
d, err := marshalValue(options, v.MapIndex(key))
if err != nil {
return nil, err
}
dest.Set(key.String(), d)
}
return dest, nil
}
return val, nil
}
// contains check if a given key is contained in a slice of strings.
func contains(key string, list []string) bool {
for _, innerKey := range list {
if key == innerKey {
return true
}
}
return false
}
// listContains operates on two string slices and checks if one of the strings in `a`
// is contained in `b`.
func listContains(a []string, b []string) bool {
for _, key := range a {
if contains(key, b) {
return true
}
}
return false
}