This commit is contained in:
James Bardin 2020-12-01 16:17:18 -05:00
parent 276dfe634f
commit 4ad7c41ab4
6 changed files with 0 additions and 738 deletions

View File

@ -1,152 +0,0 @@
package flatmap
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/hashicorp/terraform/configs/hcl2shim"
)
// Expand takes a map and a key (prefix) and expands that value into
// a more complex structure. This is the reverse of the Flatten operation.
func Expand(m map[string]string, key string) interface{} {
// If the key is exactly a key in the map, just return it
if v, ok := m[key]; ok {
if v == "true" {
return true
} else if v == "false" {
return false
}
return v
}
// Check if the key is an array, and if so, expand the array
if v, ok := m[key+".#"]; ok {
// If the count of the key is unknown, then just put the unknown
// value in the value itself. This will be detected by Terraform
// core later.
if v == hcl2shim.UnknownVariableValue {
return v
}
return expandArray(m, key)
}
// Check if this is a prefix in the map
prefix := key + "."
for k := range m {
if strings.HasPrefix(k, prefix) {
return expandMap(m, prefix)
}
}
return nil
}
func expandArray(m map[string]string, prefix string) []interface{} {
num, err := strconv.ParseInt(m[prefix+".#"], 0, 0)
if err != nil {
panic(err)
}
// If the number of elements in this array is 0, then return an
// empty slice as there is nothing to expand. Trying to expand it
// anyway could lead to crashes as any child maps, arrays or sets
// that no longer exist are still shown as empty with a count of 0.
if num == 0 {
return []interface{}{}
}
// NOTE: "num" is not necessarily accurate, e.g. if a user tampers
// with state, so the following code should not crash when given a
// number of items more or less than what's given in num. The
// num key is mainly just a hint that this is a list or set.
// The Schema "Set" type stores its values in an array format, but
// using numeric hash values instead of ordinal keys. Take the set
// of keys regardless of value, and expand them in numeric order.
// See GH-11042 for more details.
keySet := map[int]bool{}
computed := map[string]bool{}
for k := range m {
if !strings.HasPrefix(k, prefix+".") {
continue
}
key := k[len(prefix)+1:]
idx := strings.Index(key, ".")
if idx != -1 {
key = key[:idx]
}
// skip the count value
if key == "#" {
continue
}
// strip the computed flag if there is one
if strings.HasPrefix(key, "~") {
key = key[1:]
computed[key] = true
}
k, err := strconv.Atoi(key)
if err != nil {
panic(err)
}
keySet[int(k)] = true
}
keysList := make([]int, 0, num)
for key := range keySet {
keysList = append(keysList, key)
}
sort.Ints(keysList)
result := make([]interface{}, len(keysList))
for i, key := range keysList {
keyString := strconv.Itoa(key)
if computed[keyString] {
keyString = "~" + keyString
}
result[i] = Expand(m, fmt.Sprintf("%s.%s", prefix, keyString))
}
return result
}
func expandMap(m map[string]string, prefix string) map[string]interface{} {
// Submaps may not have a '%' key, so we can't count on this value being
// here. If we don't have a count, just proceed as if we have have a map.
if count, ok := m[prefix+"%"]; ok && count == "0" {
return map[string]interface{}{}
}
result := make(map[string]interface{})
for k := range m {
if !strings.HasPrefix(k, prefix) {
continue
}
key := k[len(prefix):]
idx := strings.Index(key, ".")
if idx != -1 {
key = key[:idx]
}
if _, ok := result[key]; ok {
continue
}
// skip the map count value
if key == "%" {
continue
}
result[key] = Expand(m, k[:len(prefix)+len(key)])
}
return result
}

View File

@ -1,225 +0,0 @@
package flatmap
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/configs/hcl2shim"
)
func TestExpand(t *testing.T) {
cases := []struct {
Map map[string]string
Key string
Output interface{}
}{
{
Map: map[string]string{
"foo": "bar",
"bar": "baz",
},
Key: "foo",
Output: "bar",
},
{
Map: map[string]string{
"foo.#": "2",
"foo.0": "one",
"foo.1": "two",
},
Key: "foo",
Output: []interface{}{
"one",
"two",
},
},
{
Map: map[string]string{
// # mismatches actual number of keys; actual number should
// "win" here, since the # is just a hint that this is a list.
"foo.#": "1",
"foo.0": "one",
"foo.1": "two",
"foo.2": "three",
},
Key: "foo",
Output: []interface{}{
"one",
"two",
"three",
},
},
{
Map: map[string]string{
// # mismatches actual number of keys; actual number should
// "win" here, since the # is just a hint that this is a list.
"foo.#": "5",
"foo.0": "one",
"foo.1": "two",
"foo.2": "three",
},
Key: "foo",
Output: []interface{}{
"one",
"two",
"three",
},
},
{
Map: map[string]string{
"foo.#": "1",
"foo.0.name": "bar",
"foo.0.port": "3000",
"foo.0.enabled": "true",
},
Key: "foo",
Output: []interface{}{
map[string]interface{}{
"name": "bar",
"port": "3000",
"enabled": true,
},
},
},
{
Map: map[string]string{
"foo.#": "1",
"foo.0.name": "bar",
"foo.0.ports.#": "2",
"foo.0.ports.0": "1",
"foo.0.ports.1": "2",
},
Key: "foo",
Output: []interface{}{
map[string]interface{}{
"name": "bar",
"ports": []interface{}{
"1",
"2",
},
},
},
},
{
Map: map[string]string{
"list_of_map.#": "2",
"list_of_map.0.%": "1",
"list_of_map.0.a": "1",
"list_of_map.1.%": "2",
"list_of_map.1.b": "2",
"list_of_map.1.c": "3",
},
Key: "list_of_map",
Output: []interface{}{
map[string]interface{}{
"a": "1",
},
map[string]interface{}{
"b": "2",
"c": "3",
},
},
},
{
Map: map[string]string{
"map_of_list.%": "2",
"map_of_list.list2.#": "1",
"map_of_list.list2.0": "c",
"map_of_list.list1.#": "2",
"map_of_list.list1.0": "a",
"map_of_list.list1.1": "b",
},
Key: "map_of_list",
Output: map[string]interface{}{
"list1": []interface{}{"a", "b"},
"list2": []interface{}{"c"},
},
},
{
Map: map[string]string{
"set.#": "3",
"set.1234": "a",
"set.1235": "b",
"set.1236": "c",
},
Key: "set",
Output: []interface{}{"a", "b", "c"},
},
{
Map: map[string]string{
"computed_set.#": "1",
"computed_set.~1234.a": "a",
"computed_set.~1234.b": "b",
"computed_set.~1234.c": "c",
},
Key: "computed_set",
Output: []interface{}{
map[string]interface{}{"a": "a", "b": "b", "c": "c"},
},
},
{
Map: map[string]string{
"struct.#": "1",
"struct.0.name": "hello",
"struct.0.rules.#": hcl2shim.UnknownVariableValue,
},
Key: "struct",
Output: []interface{}{
map[string]interface{}{
"name": "hello",
"rules": hcl2shim.UnknownVariableValue,
},
},
},
{
Map: map[string]string{
"struct.#": "1",
"struct.0.name": "hello",
"struct.0.set.#": "0",
"struct.0.set.0.key": "value",
},
Key: "struct",
Output: []interface{}{
map[string]interface{}{
"name": "hello",
"set": []interface{}{},
},
},
},
{
Map: map[string]string{
"empty_map_of_sets.%": "0",
"empty_map_of_sets.set1.#": "0",
"empty_map_of_sets.set1.1234": "x",
},
Key: "empty_map_of_sets",
Output: map[string]interface{}{},
},
}
for _, tc := range cases {
t.Run(tc.Key, func(t *testing.T) {
actual := Expand(tc.Map, tc.Key)
if !reflect.DeepEqual(actual, tc.Output) {
t.Errorf(
"Key: %v\nMap:\n\n%#v\n\nOutput:\n\n%#v\n\nExpected:\n\n%#v\n",
tc.Key,
tc.Map,
actual,
tc.Output)
}
})
}
}

View File

@ -1,71 +0,0 @@
package flatmap
import (
"fmt"
"reflect"
)
// Flatten takes a structure and turns into a flat map[string]string.
//
// Within the "thing" parameter, only primitive values are allowed. Structs are
// not supported. Therefore, it can only be slices, maps, primitives, and
// any combination of those together.
//
// See the tests for examples of what inputs are turned into.
func Flatten(thing map[string]interface{}) Map {
result := make(map[string]string)
for k, raw := range thing {
flatten(result, k, reflect.ValueOf(raw))
}
return Map(result)
}
func flatten(result map[string]string, prefix string, v reflect.Value) {
if v.Kind() == reflect.Interface {
v = v.Elem()
}
switch v.Kind() {
case reflect.Bool:
if v.Bool() {
result[prefix] = "true"
} else {
result[prefix] = "false"
}
case reflect.Int:
result[prefix] = fmt.Sprintf("%d", v.Int())
case reflect.Map:
flattenMap(result, prefix, v)
case reflect.Slice:
flattenSlice(result, prefix, v)
case reflect.String:
result[prefix] = v.String()
default:
panic(fmt.Sprintf("Unknown: %s", v))
}
}
func flattenMap(result map[string]string, prefix string, v reflect.Value) {
for _, k := range v.MapKeys() {
if k.Kind() == reflect.Interface {
k = k.Elem()
}
if k.Kind() != reflect.String {
panic(fmt.Sprintf("%s: map key is not string: %s", prefix, k))
}
flatten(result, fmt.Sprintf("%s.%s", prefix, k.String()), v.MapIndex(k))
}
}
func flattenSlice(result map[string]string, prefix string, v reflect.Value) {
prefix = prefix + "."
result[prefix+"#"] = fmt.Sprintf("%d", v.Len())
for i := 0; i < v.Len(); i++ {
flatten(result, fmt.Sprintf("%s%d", prefix, i), v.Index(i))
}
}

View File

@ -1,88 +0,0 @@
package flatmap
import (
"reflect"
"testing"
)
func TestFlatten(t *testing.T) {
cases := []struct {
Input map[string]interface{}
Output map[string]string
}{
{
Input: map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
Output: map[string]string{
"foo": "bar",
"bar": "baz",
},
},
{
Input: map[string]interface{}{
"foo": []string{
"one",
"two",
},
},
Output: map[string]string{
"foo.#": "2",
"foo.0": "one",
"foo.1": "two",
},
},
{
Input: map[string]interface{}{
"foo": []map[interface{}]interface{}{
map[interface{}]interface{}{
"name": "bar",
"port": 3000,
"enabled": true,
},
},
},
Output: map[string]string{
"foo.#": "1",
"foo.0.name": "bar",
"foo.0.port": "3000",
"foo.0.enabled": "true",
},
},
{
Input: map[string]interface{}{
"foo": []map[interface{}]interface{}{
map[interface{}]interface{}{
"name": "bar",
"ports": []string{
"1",
"2",
},
},
},
},
Output: map[string]string{
"foo.#": "1",
"foo.0.name": "bar",
"foo.0.ports.#": "2",
"foo.0.ports.0": "1",
"foo.0.ports.1": "2",
},
},
}
for _, tc := range cases {
actual := Flatten(tc.Input)
if !reflect.DeepEqual(actual, Map(tc.Output)) {
t.Fatalf(
"Input:\n\n%#v\n\nOutput:\n\n%#v\n\nExpected:\n\n%#v\n",
tc.Input,
actual,
tc.Output)
}
}
}

View File

@ -1,82 +0,0 @@
package flatmap
import (
"strings"
)
// Map is a wrapper around map[string]string that provides some helpers
// above it that assume the map is in the format that flatmap expects
// (the result of Flatten).
//
// All modifying functions such as Delete are done in-place unless
// otherwise noted.
type Map map[string]string
// Contains returns true if the map contains the given key.
func (m Map) Contains(key string) bool {
for _, k := range m.Keys() {
if k == key {
return true
}
}
return false
}
// Delete deletes a key out of the map with the given prefix.
func (m Map) Delete(prefix string) {
for k, _ := range m {
match := k == prefix
if !match {
if !strings.HasPrefix(k, prefix) {
continue
}
if k[len(prefix):len(prefix)+1] != "." {
continue
}
}
delete(m, k)
}
}
// Keys returns all of the top-level keys in this map
func (m Map) Keys() []string {
ks := make(map[string]struct{})
for k, _ := range m {
idx := strings.Index(k, ".")
if idx == -1 {
idx = len(k)
}
ks[k[:idx]] = struct{}{}
}
result := make([]string, 0, len(ks))
for k, _ := range ks {
result = append(result, k)
}
return result
}
// Merge merges the contents of the other Map into this one.
//
// This merge is smarter than a simple map iteration because it
// will fully replace arrays and other complex structures that
// are present in this map with the other map's. For example, if
// this map has a 3 element "foo" list, and m2 has a 2 element "foo"
// list, then the result will be that m has a 2 element "foo"
// list.
func (m Map) Merge(m2 Map) {
for _, prefix := range m2.Keys() {
m.Delete(prefix)
for k, v := range m2 {
if strings.HasPrefix(k, prefix) {
m[k] = v
}
}
}
}

View File

@ -1,120 +0,0 @@
package flatmap
import (
"reflect"
"sort"
"testing"
)
func TestMapContains(t *testing.T) {
cases := []struct {
Input map[string]string
Key string
Result bool
}{
{
Input: map[string]string{
"foo": "bar",
"bar": "nope",
},
Key: "foo",
Result: true,
},
{
Input: map[string]string{
"foo": "bar",
"bar": "nope",
},
Key: "baz",
Result: false,
},
}
for i, tc := range cases {
actual := Map(tc.Input).Contains(tc.Key)
if actual != tc.Result {
t.Fatalf("case %d bad: %#v", i, tc.Input)
}
}
}
func TestMapDelete(t *testing.T) {
m := Flatten(map[string]interface{}{
"foo": "bar",
"routes": []map[string]string{
map[string]string{
"foo": "bar",
},
},
})
m.Delete("routes")
expected := Map(map[string]string{"foo": "bar"})
if !reflect.DeepEqual(m, expected) {
t.Fatalf("bad: %#v", m)
}
}
func TestMapKeys(t *testing.T) {
cases := []struct {
Input map[string]string
Output []string
}{
{
Input: map[string]string{
"foo": "bar",
"bar.#": "bar",
"bar.0.foo": "bar",
"bar.0.baz": "bar",
},
Output: []string{
"bar",
"foo",
},
},
}
for _, tc := range cases {
actual := Map(tc.Input).Keys()
// Sort so we have a consistent view of the output
sort.Strings(actual)
if !reflect.DeepEqual(actual, tc.Output) {
t.Fatalf("input: %#v\n\nbad: %#v", tc.Input, actual)
}
}
}
func TestMapMerge(t *testing.T) {
cases := []struct {
One map[string]string
Two map[string]string
Result map[string]string
}{
{
One: map[string]string{
"foo": "bar",
"bar": "nope",
},
Two: map[string]string{
"bar": "baz",
"baz": "buz",
},
Result: map[string]string{
"foo": "bar",
"bar": "baz",
"baz": "buz",
},
},
}
for i, tc := range cases {
Map(tc.One).Merge(Map(tc.Two))
if !reflect.DeepEqual(tc.One, tc.Result) {
t.Fatalf("case %d bad: %#v", i, tc.One)
}
}
}