-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgoflat.go
278 lines (252 loc) · 8.62 KB
/
goflat.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
package goflat
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"sort"
"strings"
)
var ErrInvalidType = errors.New("not a valid JSON input")
// `FlattenerConfig` holds configuration options for flattening.
type FlattenerConfig struct {
Prefix string
Separator string
OmitEmpty bool
OmitNil bool
SortKeys bool
KeysToLower bool
}
// `DefaultFlattenerConfig` returns a FlattenerConfig with default values.
func defaultConfiguration() FlattenerConfig {
return FlattenerConfig{
Prefix: "",
Separator: ".",
OmitEmpty: true,
OmitNil: true,
SortKeys: false,
KeysToLower: false,
}
}
// `FlatStruct` flattens a Go struct into a map with flattened keys.
func FlatStruct(input interface{}, config ...FlattenerConfig) map[string]interface{} {
cfg := defaultConfiguration()
if len(config) > 0 {
cfg = config[0]
}
result := make(map[string]interface{})
flattenFields(reflect.ValueOf(input), cfg.Prefix, result, cfg)
if cfg.SortKeys {
sortKeys(&result)
}
if cfg.KeysToLower {
keysToLower(&result)
}
return result
}
// `FlatJSON` flattens a JSON string into a flattened JSON string.
func FlatJSON(jsonStr string, config ...FlattenerConfig) (string, error) {
cfg := defaultConfiguration()
if len(config) > 0 {
cfg = config[0]
}
var data interface{}
err := json.Unmarshal([]byte(jsonStr), &data)
if err != nil {
return "", ErrInvalidType
}
flattenedMap := make(map[string]interface{})
flatten(cfg.Prefix, data, flattenedMap, cfg)
if cfg.SortKeys {
sortKeys(&flattenedMap)
}
if cfg.KeysToLower {
keysToLower(&flattenedMap)
}
flattenedJSON, err := json.Marshal(flattenedMap)
if err != nil {
return "", ErrInvalidType
}
return string(flattenedJSON), nil
}
// `FlatJSONToMap` flattens a JSON string into a map with flattened keys.
func FlatJSONToMap(jsonStr string, config ...FlattenerConfig) (map[string]interface{}, error) {
cfg := defaultConfiguration()
if len(config) > 0 {
cfg = config[0]
}
var data interface{}
err := json.Unmarshal([]byte(jsonStr), &data)
if err != nil {
return nil, ErrInvalidType
}
flattenedMap := make(map[string]interface{})
flatten(cfg.Prefix, data, flattenedMap, cfg)
if cfg.SortKeys {
sortKeys(&flattenedMap)
}
if cfg.KeysToLower {
keysToLower(&flattenedMap)
}
return flattenedMap, nil
}
// `sortKeys` sorts keys in the flattened structure.
func sortKeys(result *map[string]interface{}) {
keys := make(map[string]string)
for key := range *result {
keys[key] = ""
}
sortedResult := make(map[string]interface{})
sortedKeys := make([]string, 0, len(keys))
for key := range keys {
sortedKeys = append(sortedKeys, key)
}
sort.Strings(sortedKeys)
for _, key := range sortedKeys {
sortedResult[key] = (*result)[key]
}
*result = sortedResult
}
// `flatten` flattens a nested structure into a map with flattened keys.
func flatten(prefix string, value interface{}, result map[string]interface{}, config FlattenerConfig) {
switch v := value.(type) {
case map[string]interface{}:
// For each key-value pair in the map, recursively flatten the nested structure.
for key, val := range v {
fullKey := key
if prefix != "" {
fullKey = prefix + config.Separator + key
}
flatten(fullKey, val, result, config)
}
case []interface{}:
// For each element in the array, recursively flatten the nested structure.
flattenArray(prefix, v, result, config)
default:
// If the value is neither a map nor an array, add it to the result map.
// Optionally omitting empty or nil values based on the configuration.
if !(config.OmitEmpty && isEmptyValue(reflect.ValueOf(v))) && !(config.OmitNil && isNilValue(reflect.ValueOf(v))) {
result[prefix] = v
}
}
}
// `flattenArray` flattens an array into a map with flattened keys.
func flattenArray(prefix string, arr []interface{}, result map[string]interface{}, config FlattenerConfig) {
for i, v := range arr {
// Generate the full key for each element in the array.
fullKey := fmt.Sprintf("%s%s%s%d", config.Prefix, prefix, config.Separator, i)
// Remove leading separator if it's present.
if strings.Index(fullKey, config.Separator) == 0+len(config.Prefix) {
fullKey = fullKey[1:]
}
// Recursively flatten the nested structure for each array element.
flatten(fullKey, v, result, config)
}
}
// `flattenFields` flattens fields of a struct into a map with flattened keys.
func flattenFields(val reflect.Value, prefix string, result map[string]interface{}, config FlattenerConfig) {
typ := val.Type()
if val.Kind() == reflect.Ptr && !val.IsNil() {
val = val.Elem()
typ = val.Type()
}
switch val.Kind() {
case reflect.Struct:
// For each field in the struct, recursively flatten the nested structure.
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldName := typ.Field(i).Name
if field.Kind() == reflect.Slice || field.Kind() == reflect.Array {
fullKey := prefix + fieldName
flattenArrayFields(fullKey, "", field, result, config)
} else if !(config.OmitEmpty && isEmptyValue(field)) && !(config.OmitNil && isNilValue(field)) {
// Recursively flatten the nested structure for each struct field.
flattenFields(field, prefix+fieldName+config.Separator, result, config)
}
}
case reflect.Map:
// For each key-value pair in the map, recursively flatten the nested structure.
for _, key := range val.MapKeys() {
field := val.MapIndex(key)
fieldName := key.String()
fullKey := prefix + fieldName
// Optionally omitting empty or nil values based on the configuration.
if !(config.OmitEmpty && isEmptyValue(field)) && !(config.OmitNil && isNilValue(field)) {
if field.Kind() == reflect.Struct {
// If the value is a struct, recursively flatten the nested structure.
flattenFields(field, fullKey+config.Separator, result, config)
} else if field.Kind() == reflect.Slice || field.Kind() == reflect.Array {
// If the value is a slice or array, flatten each element in the collection.
flattenArrayFields(fullKey, "", field, result, config)
} else {
// If the value is neither a struct nor a slice/array, add it to the result map.
result[fullKey] = field.Interface()
}
}
}
default:
// If the value is neither a struct nor a map, add it to the result map.
// Optionally omitting empty or nil values based on the configuration.
if !(config.OmitEmpty && isEmptyValue(val)) && !(config.OmitNil && isNilValue(val)) && val.CanInterface() {
prefix = prefix[:len(prefix)-1]
// If `val` is a valid JSON likely this was *string; flat it
if js := isJSON(val.String()); js != nil {
flatMap, _ := FlatJSONToMap(val.String(), config)
flatten(prefix, flatMap, result, config)
} else {
result[prefix] = val.Interface()
}
}
}
}
// `flattenArrayFields` flattens fields of an array into a map with flattened keys.
func flattenArrayFields(prefix, fieldName string, field reflect.Value, result map[string]interface{}, config FlattenerConfig) {
for i := 0; i < field.Len(); i++ {
// Extract each element from the array and generate a key for it.
item := field.Index(i).Interface()
key := fmt.Sprintf("%s%s%d", prefix+fieldName+config.Separator, config.Separator, i)
if field.Index(i).Kind() == reflect.Ptr {
key = fmt.Sprintf("%s%d%s", prefix+fieldName+config.Separator, i, config.Separator)
flattenFields(field.Index(i), key, result, config)
} else {
// Optionally omitting empty or nil values based on the configuration.
if !(config.OmitEmpty && isEmptyValue(reflect.ValueOf(item))) && !(config.OmitNil && isNilValue(reflect.ValueOf(item))) {
// Add the key-value pair to the result map.
result[key] = item
}
}
}
}
// `keysToLower` return a map with all keys on lowercase
func keysToLower(result *map[string]interface{}) {
new_result := make(map[string]interface{}, len(*result))
for k, v := range *result {
new_result[strings.ToLower(k)] = v
}
*result = new_result
}
// `isEmptyValue` checks if a reflect.Value is empty.
func isEmptyValue(field reflect.Value) bool {
// if the type is bool when having false this will be erased; keep it instead
if field.IsValid() && field.Type().Kind() == reflect.Bool {
return false
}
if !field.IsValid() || !field.CanInterface() {
return true
}
zero := reflect.Zero(field.Type())
return reflect.DeepEqual(field.Interface(), zero.Interface())
}
// `isNilValue` checks if a reflect.Value is nil.
func isNilValue(field reflect.Value) bool {
// Check if the field is a pointer and is nil.
return field.Kind() == reflect.Ptr && field.IsNil()
}
// `jsJSON` checks if a string it's a valid JSON string.
func isJSON(str string) json.RawMessage {
var js json.RawMessage
if err := json.Unmarshal([]byte(str), &js); err != nil {
return nil
}
return js
}