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
|
package flatten
import (
"encoding/json"
"errors"
"regexp"
"strconv"
)
// The style of keys. If there is an input with two
// nested keys "f" and "g", with "f" at the root,
// { "f": { "g": ... } }
// the output will be the concatenation
// f{Middle}{Before}g{After}...
// Any struct element may be blank.
// If you use Middle, you will probably leave Before & After blank, and vice-versa.
// See examples in flatten_test.go and the "Default styles" here.
type SeparatorStyle struct {
Before string // Prepend to key
Middle string // Add between keys
After string // Append to key
}
// Default styles
var (
// Separate nested key components with dots, e.g. "a.b.1.c.d"
DotStyle = SeparatorStyle{Middle: "."}
// Separate with path-like slashes, e.g. a/b/1/c/d
PathStyle = SeparatorStyle{Middle: "/"}
// Separate ala Rails, e.g. "a[b][c][1][d]"
RailsStyle = SeparatorStyle{Before: "[", After: "]"}
// Separate with underscores, e.g. "a_b_1_c_d"
UnderscoreStyle = SeparatorStyle{Middle: "_"}
)
// Nested input must be a map or slice
var NotValidInputError = errors.New("Not a valid input: map or slice")
// Flatten generates a flat map from a nested one. The original may include values of type map, slice and scalar,
// but not struct. Keys in the flat map will be a compound of descending map keys and slice iterations.
// The presentation of keys is set by style. A prefix is joined to each key.
func Flatten(nested map[string]interface{}, prefix string, style SeparatorStyle) (map[string]interface{}, error) {
flatmap := make(map[string]interface{})
err := flatten(true, flatmap, nested, prefix, style)
if err != nil {
return nil, err
}
return flatmap, nil
}
// JSON nested input must be a map
var NotValidJsonInputError = errors.New("Not a valid input, must be a map")
var isJsonMap = regexp.MustCompile(`^\s*\{`)
// FlattenString generates a flat JSON map from a nested one. Keys in the flat map will be a compound of
// descending map keys and slice iterations. The presentation of keys is set by style. A prefix is joined
// to each key.
func FlattenString(nestedstr, prefix string, style SeparatorStyle) (string, error) {
if !isJsonMap.MatchString(nestedstr) {
return "", NotValidJsonInputError
}
var nested map[string]interface{}
err := json.Unmarshal([]byte(nestedstr), &nested)
if err != nil {
return "", err
}
flatmap, err := Flatten(nested, prefix, style)
if err != nil {
return "", err
}
flatb, err := json.Marshal(&flatmap)
if err != nil {
return "", err
}
return string(flatb), nil
}
func flatten(top bool, flatMap map[string]interface{}, nested interface{}, prefix string, style SeparatorStyle) error {
assign := func(newKey string, v interface{}) error {
switch v.(type) {
case map[string]interface{}, []interface{}:
if err := flatten(false, flatMap, v, newKey, style); err != nil {
return err
}
default:
flatMap[newKey] = v
}
return nil
}
switch nested.(type) {
case map[string]interface{}:
for k, v := range nested.(map[string]interface{}) {
newKey := enkey(top, prefix, k, style)
assign(newKey, v)
}
case []interface{}:
for i, v := range nested.([]interface{}) {
newKey := enkey(top, prefix, strconv.Itoa(i), style)
assign(newKey, v)
}
default:
return NotValidInputError
}
return nil
}
func enkey(top bool, prefix, subkey string, style SeparatorStyle) string {
key := prefix
if top {
key += subkey
} else {
key += style.Before + style.Middle + subkey + style.After
}
return key
}
|