sops/stores/flatten.go

188 строки
4.9 KiB
Go

package stores
import (
"fmt"
"strconv"
"strings"
)
const mapSeparator = "__map_"
const listSeparator = "__list_"
// flattenAndMerge flattens the provided value and merges into the
// into map using prefix
func flattenAndMerge(into map[string]interface{}, prefix string, value interface{}) {
flattenedValue := flattenValue(value)
if flattenedValue, ok := flattenedValue.(map[string]interface{}); ok {
for flatK, flatV := range flattenedValue {
into[prefix+flatK] = flatV
}
} else {
into[prefix] = value
}
}
func flattenValue(value interface{}) interface{} {
var output interface{}
switch value := value.(type) {
case map[string]interface{}:
newMap := make(map[string]interface{})
for k, v := range value {
flattenAndMerge(newMap, mapSeparator+k, v)
}
output = newMap
case []interface{}:
newMap := make(map[string]interface{})
for i, v := range value {
flattenAndMerge(newMap, listSeparator+fmt.Sprintf("%d", i), v)
}
output = newMap
default:
output = value
}
return output
}
// Flatten flattens a map with potentially nested maps into a flat
// map. Only string keys are allowed on both the top-level map and
// child maps.
func Flatten(in map[string]interface{}) map[string]interface{} {
newMap := make(map[string]interface{})
for k, v := range in {
if flat, ok := flattenValue(v).(map[string]interface{}); ok {
for flatK, flatV := range flat {
newMap[k+flatK] = flatV
}
} else {
newMap[k] = v
}
}
return newMap
}
type token interface{}
type mapToken struct {
key string
}
type listToken struct {
position int
}
// tokenize converts a path generated by Flatten to be used as a key
// in the flattened map, and converts it to a slice of tokens
func tokenize(path string) []token {
const (
StateNormal = 0
StateMap = iota
StateList = iota
)
var tokens []token
state := StateNormal
lastTokenEnd := 0
i := 0
finishPrevToken := func() {
var t token
switch state {
case StateNormal:
t = mapToken{path[lastTokenEnd:i]}
case StateMap:
t = mapToken{path[lastTokenEnd+len(mapSeparator) : i]}
case StateList:
pos, _ := strconv.Atoi(path[lastTokenEnd+len(listSeparator) : i])
t = listToken{pos}
}
lastTokenEnd = i
tokens = append(tokens, t)
}
for i < len(path) {
if strings.HasPrefix(path[i:], mapSeparator) {
finishPrevToken()
state = StateMap
i += len(mapSeparator)
} else if strings.HasPrefix(path[i:], listSeparator) {
finishPrevToken()
state = StateList
i += len(listSeparator)
} else {
i++
}
}
finishPrevToken()
return tokens
}
// unflatten takes the currentNode, currentToken, nextToken and value
// and populates currentNode such that currentToken can be considered
// processed. It inspects nextToken to decide what type to allocate
// and assign under currentNode.
func unflatten(currentNode interface{}, currentToken, nextToken token, value interface{}) interface{} {
switch currentToken := currentToken.(type) {
case mapToken:
currentNode := currentNode.(map[string]interface{})
switch nextToken := nextToken.(type) {
case mapToken:
if _, ok := currentNode[currentToken.key]; !ok {
currentNode[currentToken.key] = make(map[string]interface{})
}
next := currentNode[currentToken.key].(map[string]interface{})
return next
case listToken:
if _, ok := currentNode[currentToken.key]; !ok {
currentNode[currentToken.key] = make([]interface{}, nextToken.position+1)
}
next := currentNode[currentToken.key].([]interface{})
if nextToken.position >= len(next) {
// Grow the slice and reassign it
newNext := make([]interface{}, nextToken.position+1)
copy(newNext, next)
next = newNext
currentNode[currentToken.key] = next
}
return next
default:
currentNode[currentToken.key] = value
}
case listToken:
currentNode := currentNode.([]interface{})
switch nextToken := nextToken.(type) {
case mapToken:
if currentNode[currentToken.position] == nil {
currentNode[currentToken.position] = make(map[string]interface{})
}
next := currentNode[currentToken.position].(map[string]interface{})
return next
case listToken:
if currentNode[currentToken.position] == nil {
currentNode[currentToken.position] = make([]interface{}, nextToken.position+1)
}
next := currentNode[currentToken.position].([]interface{})
if nextToken.position >= len(next) {
// Grow the slice and reassign it
newNext := make([]interface{}, nextToken.position+1)
copy(newNext, next)
next = newNext
currentNode[currentToken.position] = next
}
return next
default:
currentNode[currentToken.position] = value
}
}
return nil
}
// Unflatten unflattens a map flattened by Flatten
func Unflatten(in map[string]interface{}) map[string]interface{} {
newMap := make(map[string]interface{})
for k, v := range in {
var current interface{} = newMap
tokens := append(tokenize(k), nil)
for i := 0; i < len(tokens)-1; i++ {
current = unflatten(current, tokens[i], tokens[i+1], v)
}
}
return newMap
}