-
Notifications
You must be signed in to change notification settings - Fork 9
/
flatten.go
117 lines (102 loc) · 3.07 KB
/
flatten.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
// Package flatbson provides a function for recursively flattening a Go struct using its BSON tags.
package flatbson
import (
"errors"
"fmt"
"reflect"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsoncodec"
)
// Flatten returns a map with keys and values corresponding to the field name
// and values of struct v and its nested structs according to its BSON tags.
// It iterates over each field recursively and sets fields that are not nil.
//
// The BSON struct tags behave in line with the bsoncodec specification. See:
// https://godoc.org/go.mongodb.org/mongo-driver/bson/bsoncodec#StructTags
// for definitions. The supported tags are name, skip, omitempty, and inline.
//
// Flatten does not flatten structs with unexported fields, e.g. time.Time.
// It returns an error if v is not a struct or a pointer to a struct, or if
// the tags produce duplicate keys.
//
// type A struct {
// B *X `bson:"b,omitempty"`
// C X `bson:"c"`
// }
//
// type X struct { Y string `bson:"y"` }
//
// Flatten(A{nil, X{"hello"}})
// // Returns:
// // map[string]interface{}{"c.y": "hello"}
func Flatten(v interface{}) (map[string]interface{}, error) {
val := reflect.ValueOf(v)
val, ok := asStruct(val)
if !ok {
return nil, errors.New("v must be a struct or a pointer to a struct")
}
m := make(map[string]interface{})
if err := flattenFields(val, m, ""); err != nil {
return nil, err
}
return m, nil
}
// flattenFields recursively adds the values of v's fields to map m.
func flattenFields(v reflect.Value, m map[string]interface{}, p string) error {
for i := 0; i < v.NumField(); i++ {
tags, _ := bsoncodec.DefaultStructTagParser(v.Type().Field(i))
if tags.Skip {
continue
}
field := v.Field(i)
if tags.OmitEmpty && field.IsZero() || !field.CanInterface() {
continue
}
// If the field can marshal itself into a BSON type, or it's a struct has no
// exported fields, like time.Time, we shouldn't recurse into its fields.
if _, ok := field.Interface().(bson.ValueMarshaler); !ok {
if s, ok := asStruct(field); ok && hasExportedField(s) {
fp := p
if !tags.Inline {
fp = p + tags.Name + "."
}
if err := flattenFields(s, m, fp); err != nil {
return err
}
continue
}
}
key := p + tags.Name
if _, ok := m[key]; ok {
return fmt.Errorf("duplicated key %s", key)
}
m[key] = field.Interface()
}
return nil
}
// asStruct returns that value of v as a struct.
// - If v is already a struct, it is returned immediately.
// - If v is a pointer, it is dereferenced till a struct is found.
// - If a non-struct value is found, it returns false.
func asStruct(v reflect.Value) (reflect.Value, bool) {
for {
switch v.Kind() {
case reflect.Struct:
return v, true
case reflect.Ptr:
v = v.Elem()
default:
return reflect.Value{}, false
}
}
}
// hasUnexportedField returns true if struct s has a field
// that is not exported.
func hasExportedField(s reflect.Value) bool {
for i := 0; i < s.NumField(); i++ {
if s.Field(i).CanInterface() {
return true
}
}
return false
}