diff --git a/flatmap/expand.go b/flatmap/expand.go new file mode 100644 index 000000000..2c0a9266b --- /dev/null +++ b/flatmap/expand.go @@ -0,0 +1,73 @@ +package flatmap + +import ( + "fmt" + "strconv" + "strings" +) + +// 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 num, err := strconv.ParseInt(v, 0, 0); err == nil { + return int(num) + } else 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 _, ok := m[key+".#"]; ok { + 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) + } + + result := make([]interface{}, num) + for i := 0; i < int(num); i++ { + result[i] = Expand(m, fmt.Sprintf("%s.%d", prefix, i)) + } + + return result +} + +func expandMap(m map[string]string, prefix string) 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 { + idx = len(k) + } + + // It contains a period, so it is a more complex structure + result[key] = Expand(m, k[:idx]) + } + + return result +} diff --git a/flatmap/expand_test.go b/flatmap/expand_test.go new file mode 100644 index 000000000..6ad54945e --- /dev/null +++ b/flatmap/expand_test.go @@ -0,0 +1,65 @@ +package flatmap + +import ( + "reflect" + "testing" +) + +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{ + "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, + }, + }, + }, + } + + for _, tc := range cases { + actual := Expand(tc.Map, tc.Key) + if !reflect.DeepEqual(actual, tc.Output) { + t.Fatalf( + "Key: %v\nMap:\n\n%#v\n\nOutput:\n\n%#v\n\nExpected:\n\n%#v\n", + tc.Key, + tc.Map, + actual, + tc.Output) + } + } +} diff --git a/flatmap/flatten.go b/flatmap/flatten.go new file mode 100644 index 000000000..71b249117 --- /dev/null +++ b/flatmap/flatten.go @@ -0,0 +1,71 @@ +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[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/flatten_test.go similarity index 100% rename from flatmap/map_test.go rename to flatmap/flatten_test.go diff --git a/flatmap/map.go b/flatmap/map.go index 204388fa7..5f3dd8cb3 100644 --- a/flatmap/map.go +++ b/flatmap/map.go @@ -1,73 +1,3 @@ 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)) - } -}