diff --git a/flatmap/map.go b/flatmap/map.go new file mode 100644 index 000000000..204388fa7 --- /dev/null +++ b/flatmap/map.go @@ -0,0 +1,73 @@ +package flatmap + +import ( + "fmt" + "reflect" +) + +type Map map[string]string + +// 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[string]string { + result := make(map[string]string) + + for k, raw := range thing { + flatten(result, k, reflect.ValueOf(raw)) + } + + return 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)) + } +} diff --git a/flatmap/map_test.go b/flatmap/map_test.go new file mode 100644 index 000000000..0bd15e882 --- /dev/null +++ b/flatmap/map_test.go @@ -0,0 +1,67 @@ +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", + }, + }, + } + + for _, tc := range cases { + actual := Flatten(tc.Input) + if !reflect.DeepEqual(actual, tc.Output) { + t.Fatalf( + "Input:\n\n%#v\n\nOutput:\n\n%#v\n\nExpected:\n\n%#v\n", + tc.Input, + actual, + tc.Output) + } + } +}