blob: 3a87706525f7a662e5d7be68a0cb874ab74b6b5a [file] [log] [blame]
Matteo Scandolof65e6872020-04-15 15:18:43 -07001// Package structs contains various utilities functions to work with structs.
2package structs
3
4import (
5 "fmt"
6
7 "reflect"
8)
9
10var (
11 // DefaultTagName is the default tag name for struct fields which provides
12 // a more granular to tweak certain structs. Lookup the necessary functions
13 // for more info.
14 DefaultTagName = "structs" // struct's field default tag name
15)
16
17// Struct encapsulates a struct type to provide several high level functions
18// around the struct.
19type Struct struct {
20 raw interface{}
21 value reflect.Value
22 TagName string
23}
24
25// New returns a new *Struct with the struct s. It panics if the s's kind is
26// not struct.
27func New(s interface{}) *Struct {
28 return &Struct{
29 raw: s,
30 value: strctVal(s),
31 TagName: DefaultTagName,
32 }
33}
34
35// Map converts the given struct to a map[string]interface{}, where the keys
36// of the map are the field names and the values of the map the associated
37// values of the fields. The default key string is the struct field name but
38// can be changed in the struct field's tag value. The "structs" key in the
39// struct's field tag value is the key name. Example:
40//
41// // Field appears in map as key "myName".
42// Name string `structs:"myName"`
43//
44// A tag value with the content of "-" ignores that particular field. Example:
45//
46// // Field is ignored by this package.
47// Field bool `structs:"-"`
48//
49// A tag value with the content of "string" uses the stringer to get the value. Example:
50//
51// // The value will be output of Animal's String() func.
52// // Map will panic if Animal does not implement String().
53// Field *Animal `structs:"field,string"`
54//
55// A tag value with the option of "flatten" used in a struct field is to flatten its fields
56// in the output map. Example:
57//
58// // The FieldStruct's fields will be flattened into the output map.
59// FieldStruct time.Time `structs:",flatten"`
60//
61// A tag value with the option of "omitnested" stops iterating further if the type
62// is a struct. Example:
63//
64// // Field is not processed further by this package.
65// Field time.Time `structs:"myName,omitnested"`
66// Field *http.Request `structs:",omitnested"`
67//
68// A tag value with the option of "omitempty" ignores that particular field if
69// the field value is empty. Example:
70//
71// // Field appears in map as key "myName", but the field is
72// // skipped if empty.
73// Field string `structs:"myName,omitempty"`
74//
75// // Field appears in map as key "Field" (the default), but
76// // the field is skipped if empty.
77// Field string `structs:",omitempty"`
78//
79// Note that only exported fields of a struct can be accessed, non exported
80// fields will be neglected.
81func (s *Struct) Map() map[string]interface{} {
82 out := make(map[string]interface{})
83 s.FillMap(out)
84 return out
85}
86
87// FillMap is the same as Map. Instead of returning the output, it fills the
88// given map.
89func (s *Struct) FillMap(out map[string]interface{}) {
90 if out == nil {
91 return
92 }
93
94 fields := s.structFields()
95
96 for _, field := range fields {
97 name := field.Name
98 val := s.value.FieldByName(name)
99 isSubStruct := false
100 var finalVal interface{}
101
102 tagName, tagOpts := parseTag(field.Tag.Get(s.TagName))
103 if tagName != "" {
104 name = tagName
105 }
106
107 // if the value is a zero value and the field is marked as omitempty do
108 // not include
109 if tagOpts.Has("omitempty") {
110 zero := reflect.Zero(val.Type()).Interface()
111 current := val.Interface()
112
113 if reflect.DeepEqual(current, zero) {
114 continue
115 }
116 }
117
118 if !tagOpts.Has("omitnested") {
119 finalVal = s.nested(val)
120
121 v := reflect.ValueOf(val.Interface())
122 if v.Kind() == reflect.Ptr {
123 v = v.Elem()
124 }
125
126 switch v.Kind() {
127 case reflect.Map, reflect.Struct:
128 isSubStruct = true
129 }
130 } else {
131 finalVal = val.Interface()
132 }
133
134 if tagOpts.Has("string") {
135 s, ok := val.Interface().(fmt.Stringer)
136 if ok {
137 out[name] = s.String()
138 }
139 continue
140 }
141
142 if isSubStruct && (tagOpts.Has("flatten")) {
143 for k := range finalVal.(map[string]interface{}) {
144 out[k] = finalVal.(map[string]interface{})[k]
145 }
146 } else {
147 out[name] = finalVal
148 }
149 }
150}
151
152// Values converts the given s struct's field values to a []interface{}. A
153// struct tag with the content of "-" ignores the that particular field.
154// Example:
155//
156// // Field is ignored by this package.
157// Field int `structs:"-"`
158//
159// A value with the option of "omitnested" stops iterating further if the type
160// is a struct. Example:
161//
162// // Fields is not processed further by this package.
163// Field time.Time `structs:",omitnested"`
164// Field *http.Request `structs:",omitnested"`
165//
166// A tag value with the option of "omitempty" ignores that particular field and
167// is not added to the values if the field value is empty. Example:
168//
169// // Field is skipped if empty
170// Field string `structs:",omitempty"`
171//
172// Note that only exported fields of a struct can be accessed, non exported
173// fields will be neglected.
174func (s *Struct) Values() []interface{} {
175 fields := s.structFields()
176
177 var t []interface{}
178
179 for _, field := range fields {
180 val := s.value.FieldByName(field.Name)
181
182 _, tagOpts := parseTag(field.Tag.Get(s.TagName))
183
184 // if the value is a zero value and the field is marked as omitempty do
185 // not include
186 if tagOpts.Has("omitempty") {
187 zero := reflect.Zero(val.Type()).Interface()
188 current := val.Interface()
189
190 if reflect.DeepEqual(current, zero) {
191 continue
192 }
193 }
194
195 if tagOpts.Has("string") {
196 s, ok := val.Interface().(fmt.Stringer)
197 if ok {
198 t = append(t, s.String())
199 }
200 continue
201 }
202
203 if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
204 // look out for embedded structs, and convert them to a
205 // []interface{} to be added to the final values slice
206 t = append(t, Values(val.Interface())...)
207 } else {
208 t = append(t, val.Interface())
209 }
210 }
211
212 return t
213}
214
215// Fields returns a slice of Fields. A struct tag with the content of "-"
216// ignores the checking of that particular field. Example:
217//
218// // Field is ignored by this package.
219// Field bool `structs:"-"`
220//
221// It panics if s's kind is not struct.
222func (s *Struct) Fields() []*Field {
223 return getFields(s.value, s.TagName)
224}
225
226// Names returns a slice of field names. A struct tag with the content of "-"
227// ignores the checking of that particular field. Example:
228//
229// // Field is ignored by this package.
230// Field bool `structs:"-"`
231//
232// It panics if s's kind is not struct.
233func (s *Struct) Names() []string {
234 fields := getFields(s.value, s.TagName)
235
236 names := make([]string, len(fields))
237
238 for i, field := range fields {
239 names[i] = field.Name()
240 }
241
242 return names
243}
244
245func getFields(v reflect.Value, tagName string) []*Field {
246 if v.Kind() == reflect.Ptr {
247 v = v.Elem()
248 }
249
250 t := v.Type()
251
252 var fields []*Field
253
254 for i := 0; i < t.NumField(); i++ {
255 field := t.Field(i)
256
257 if tag := field.Tag.Get(tagName); tag == "-" {
258 continue
259 }
260
261 f := &Field{
262 field: field,
263 value: v.FieldByName(field.Name),
264 }
265
266 fields = append(fields, f)
267
268 }
269
270 return fields
271}
272
273// Field returns a new Field struct that provides several high level functions
274// around a single struct field entity. It panics if the field is not found.
275func (s *Struct) Field(name string) *Field {
276 f, ok := s.FieldOk(name)
277 if !ok {
278 panic("field not found")
279 }
280
281 return f
282}
283
284// FieldOk returns a new Field struct that provides several high level functions
285// around a single struct field entity. The boolean returns true if the field
286// was found.
287func (s *Struct) FieldOk(name string) (*Field, bool) {
288 t := s.value.Type()
289
290 field, ok := t.FieldByName(name)
291 if !ok {
292 return nil, false
293 }
294
295 return &Field{
296 field: field,
297 value: s.value.FieldByName(name),
298 defaultTag: s.TagName,
299 }, true
300}
301
302// IsZero returns true if all fields in a struct is a zero value (not
303// initialized) A struct tag with the content of "-" ignores the checking of
304// that particular field. Example:
305//
306// // Field is ignored by this package.
307// Field bool `structs:"-"`
308//
309// A value with the option of "omitnested" stops iterating further if the type
310// is a struct. Example:
311//
312// // Field is not processed further by this package.
313// Field time.Time `structs:"myName,omitnested"`
314// Field *http.Request `structs:",omitnested"`
315//
316// Note that only exported fields of a struct can be accessed, non exported
317// fields will be neglected. It panics if s's kind is not struct.
318func (s *Struct) IsZero() bool {
319 fields := s.structFields()
320
321 for _, field := range fields {
322 val := s.value.FieldByName(field.Name)
323
324 _, tagOpts := parseTag(field.Tag.Get(s.TagName))
325
326 if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
327 ok := IsZero(val.Interface())
328 if !ok {
329 return false
330 }
331
332 continue
333 }
334
335 // zero value of the given field, such as "" for string, 0 for int
336 zero := reflect.Zero(val.Type()).Interface()
337
338 // current value of the given field
339 current := val.Interface()
340
341 if !reflect.DeepEqual(current, zero) {
342 return false
343 }
344 }
345
346 return true
347}
348
349// HasZero returns true if a field in a struct is not initialized (zero value).
350// A struct tag with the content of "-" ignores the checking of that particular
351// field. Example:
352//
353// // Field is ignored by this package.
354// Field bool `structs:"-"`
355//
356// A value with the option of "omitnested" stops iterating further if the type
357// is a struct. Example:
358//
359// // Field is not processed further by this package.
360// Field time.Time `structs:"myName,omitnested"`
361// Field *http.Request `structs:",omitnested"`
362//
363// Note that only exported fields of a struct can be accessed, non exported
364// fields will be neglected. It panics if s's kind is not struct.
365func (s *Struct) HasZero() bool {
366 fields := s.structFields()
367
368 for _, field := range fields {
369 val := s.value.FieldByName(field.Name)
370
371 _, tagOpts := parseTag(field.Tag.Get(s.TagName))
372
373 if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
374 ok := HasZero(val.Interface())
375 if ok {
376 return true
377 }
378
379 continue
380 }
381
382 // zero value of the given field, such as "" for string, 0 for int
383 zero := reflect.Zero(val.Type()).Interface()
384
385 // current value of the given field
386 current := val.Interface()
387
388 if reflect.DeepEqual(current, zero) {
389 return true
390 }
391 }
392
393 return false
394}
395
396// Name returns the structs's type name within its package. For more info refer
397// to Name() function.
398func (s *Struct) Name() string {
399 return s.value.Type().Name()
400}
401
402// structFields returns the exported struct fields for a given s struct. This
403// is a convenient helper method to avoid duplicate code in some of the
404// functions.
405func (s *Struct) structFields() []reflect.StructField {
406 t := s.value.Type()
407
408 var f []reflect.StructField
409
410 for i := 0; i < t.NumField(); i++ {
411 field := t.Field(i)
412 // we can't access the value of unexported fields
413 if field.PkgPath != "" {
414 continue
415 }
416
417 // don't check if it's omitted
418 if tag := field.Tag.Get(s.TagName); tag == "-" {
419 continue
420 }
421
422 f = append(f, field)
423 }
424
425 return f
426}
427
428func strctVal(s interface{}) reflect.Value {
429 v := reflect.ValueOf(s)
430
431 // if pointer get the underlying element≤
432 for v.Kind() == reflect.Ptr {
433 v = v.Elem()
434 }
435
436 if v.Kind() != reflect.Struct {
437 panic("not struct")
438 }
439
440 return v
441}
442
443// Map converts the given struct to a map[string]interface{}. For more info
444// refer to Struct types Map() method. It panics if s's kind is not struct.
445func Map(s interface{}) map[string]interface{} {
446 return New(s).Map()
447}
448
449// FillMap is the same as Map. Instead of returning the output, it fills the
450// given map.
451func FillMap(s interface{}, out map[string]interface{}) {
452 New(s).FillMap(out)
453}
454
455// Values converts the given struct to a []interface{}. For more info refer to
456// Struct types Values() method. It panics if s's kind is not struct.
457func Values(s interface{}) []interface{} {
458 return New(s).Values()
459}
460
461// Fields returns a slice of *Field. For more info refer to Struct types
462// Fields() method. It panics if s's kind is not struct.
463func Fields(s interface{}) []*Field {
464 return New(s).Fields()
465}
466
467// Names returns a slice of field names. For more info refer to Struct types
468// Names() method. It panics if s's kind is not struct.
469func Names(s interface{}) []string {
470 return New(s).Names()
471}
472
473// IsZero returns true if all fields is equal to a zero value. For more info
474// refer to Struct types IsZero() method. It panics if s's kind is not struct.
475func IsZero(s interface{}) bool {
476 return New(s).IsZero()
477}
478
479// HasZero returns true if any field is equal to a zero value. For more info
480// refer to Struct types HasZero() method. It panics if s's kind is not struct.
481func HasZero(s interface{}) bool {
482 return New(s).HasZero()
483}
484
485// IsStruct returns true if the given variable is a struct or a pointer to
486// struct.
487func IsStruct(s interface{}) bool {
488 v := reflect.ValueOf(s)
489 if v.Kind() == reflect.Ptr {
490 v = v.Elem()
491 }
492
493 // uninitialized zero value of a struct
494 if v.Kind() == reflect.Invalid {
495 return false
496 }
497
498 return v.Kind() == reflect.Struct
499}
500
501// Name returns the structs's type name within its package. It returns an
502// empty string for unnamed types. It panics if s's kind is not struct.
503func Name(s interface{}) string {
504 return New(s).Name()
505}
506
507// nested retrieves recursively all types for the given value and returns the
508// nested value.
509func (s *Struct) nested(val reflect.Value) interface{} {
510 var finalVal interface{}
511
512 v := reflect.ValueOf(val.Interface())
513 if v.Kind() == reflect.Ptr {
514 v = v.Elem()
515 }
516
517 switch v.Kind() {
518 case reflect.Struct:
519 n := New(val.Interface())
520 n.TagName = s.TagName
521 m := n.Map()
522
523 // do not add the converted value if there are no exported fields, ie:
524 // time.Time
525 if len(m) == 0 {
526 finalVal = val.Interface()
527 } else {
528 finalVal = m
529 }
530 case reflect.Map:
531 // get the element type of the map
532 mapElem := val.Type()
533 switch val.Type().Kind() {
534 case reflect.Ptr, reflect.Array, reflect.Map,
535 reflect.Slice, reflect.Chan:
536 mapElem = val.Type().Elem()
537 if mapElem.Kind() == reflect.Ptr {
538 mapElem = mapElem.Elem()
539 }
540 }
541
542 // only iterate over struct types, ie: map[string]StructType,
543 // map[string][]StructType,
544 if mapElem.Kind() == reflect.Struct ||
545 (mapElem.Kind() == reflect.Slice &&
546 mapElem.Elem().Kind() == reflect.Struct) {
547 m := make(map[string]interface{}, val.Len())
548 for _, k := range val.MapKeys() {
549 m[k.String()] = s.nested(val.MapIndex(k))
550 }
551 finalVal = m
552 break
553 }
554
555 // TODO(arslan): should this be optional?
556 finalVal = val.Interface()
557 case reflect.Slice, reflect.Array:
558 if val.Type().Kind() == reflect.Interface {
559 finalVal = val.Interface()
560 break
561 }
562
563 // TODO(arslan): should this be optional?
564 // do not iterate of non struct types, just pass the value. Ie: []int,
565 // []string, co... We only iterate further if it's a struct.
566 // i.e []foo or []*foo
567 if val.Type().Elem().Kind() != reflect.Struct &&
568 !(val.Type().Elem().Kind() == reflect.Ptr &&
569 val.Type().Elem().Elem().Kind() == reflect.Struct) {
570 finalVal = val.Interface()
571 break
572 }
573
574 slices := make([]interface{}, val.Len())
575 for x := 0; x < val.Len(); x++ {
576 slices[x] = s.nested(val.Index(x))
577 }
578 finalVal = slices
579 default:
580 finalVal = val.Interface()
581 }
582
583 return finalVal
584}