diff --git a/flatmap/do.go b/flatmap/do.go new file mode 100644 index 00000000..e14de357 --- /dev/null +++ b/flatmap/do.go @@ -0,0 +1,99 @@ +package flatmap + +import ( + "fmt" + "reflect" +) + +/* +Do takes a nested map and flattens it into a single level map. The flattening +roughly follows the [JSONPath] standard. Please see the test function to +understand how the flattened output looks like. Here is an example that may +fall out of date, so be careful: + +If this is the nested input: + + map[string]any{ + "a": "foo", + "b": []any{ + map[string]any{ + "c": "bar", + "d": []any{ + map[string]any{ + "e": 2, + }, + true, + }, + }, + map[string]any{ + "c": "baz", + "d": []any{ + map[string]any{ + "e": 3, + }, + false, + }, + }, + }, + } + +You can expect this flattened output: + + map[string]any{ + ".a": "foo", + ".b[0].c": "bar", + ".b[0].d[0].e": 2, + ".b[0].d[1]": true, + ".b[1].c": "baz", + ".b[1].d[0].e": 3, + ".b[1].d[1]": false, + } + +[JSONPath]: https://goessner.net/articles/JsonPath/ +*/ +func Do(nested map[string]any) map[string]any { + flattened := map[string]any{} + for childKey, childValue := range nested { + setChildren(flattened, childKey, childValue) + } + + return flattened +} + +// setChildren is a helper function for flatten. It is invoked recursively on a +// child value. If the child is not a map or a slice, then the value is simply +// set on the flattened map. If the child is a map or a slice, then the +// function is invoked recursively on the child's values, until a +// non-map-non-slice value is hit. +func setChildren(flattened map[string]any, parentKey string, parentValue any) { + newKey := fmt.Sprintf(".%s", parentKey) + if reflect.TypeOf(parentValue) == nil { + flattened[newKey] = parentValue + return + } + + if reflect.TypeOf(parentValue).Kind() == reflect.Map { + children := parentValue.(map[string]any) + for childKey, childValue := range children { + newKey = fmt.Sprintf("%s.%s", parentKey, childKey) + setChildren(flattened, newKey, childValue) + } + return + } + + if reflect.TypeOf(parentValue).Kind() == reflect.Slice { + children := parentValue.([]any) + if len(children) == 0 { + flattened[newKey] = children + return + } + + for childIndex, childValue := range children { + newKey = fmt.Sprintf("%s[%v]", parentKey, childIndex) + setChildren(flattened, newKey, childValue) + } + return + } + + flattened[newKey] = parentValue +} diff --git a/flatmap/do_test.go b/flatmap/do_test.go new file mode 100644 index 00000000..96aac2d6 --- /dev/null +++ b/flatmap/do_test.go @@ -0,0 +1,186 @@ +package flatmap_test + +import ( + "reflect" + "testing" + + "github.com/nextmv-io/sdk/flatmap" +) + +func Test_Do(t *testing.T) { + type args struct { + nested map[string]any + } + tests := []struct { + name string + args args + want map[string]any + }{ + { + name: "flat", + args: args{ + nested: map[string]any{ + "a": "foo", + "b": 2, + "c": true, + }, + }, + want: map[string]any{ + ".a": "foo", + ".b": 2, + ".c": true, + }, + }, + { + name: "flat with nil", + args: args{ + nested: map[string]any{ + "a": "foo", + "b": nil, + "c": true, + }, + }, + want: map[string]any{ + ".a": "foo", + ".b": nil, + ".c": true, + }, + }, + { + name: "slice", + args: args{ + nested: map[string]any{ + "a": "foo", + "b": []any{ + "bar", + 2, + }, + }, + }, + want: map[string]any{ + ".a": "foo", + ".b[0]": "bar", + ".b[1]": 2, + }, + }, + { + name: "nested map", + args: args{ + nested: map[string]any{ + "a": "foo", + "b": map[string]any{ + "c": "bar", + "d": 2, + }, + }, + }, + want: map[string]any{ + ".a": "foo", + ".b.c": "bar", + ".b.d": 2, + }, + }, + { + name: "slice with nested maps", + args: args{ + nested: map[string]any{ + "a": "foo", + "b": []any{ + map[string]any{ + "c": "bar", + "d": 2, + }, + map[string]any{ + "c": "baz", + "d": 3, + }, + }, + }, + }, + want: map[string]any{ + ".a": "foo", + ".b[0].c": "bar", + ".b[0].d": 2, + ".b[1].c": "baz", + ".b[1].d": 3, + }, + }, + { + name: "slice with nested maps with nested slice", + args: args{ + nested: map[string]any{ + "a": "foo", + "b": []any{ + map[string]any{ + "c": "bar", + "d": []any{ + 2, + true, + }, + }, + map[string]any{ + "c": "baz", + "d": []any{ + 3, + false, + }, + }, + }, + }, + }, + want: map[string]any{ + ".a": "foo", + ".b[0].c": "bar", + ".b[0].d[0]": 2, + ".b[0].d[1]": true, + ".b[1].c": "baz", + ".b[1].d[0]": 3, + ".b[1].d[1]": false, + }, + }, + { + name: "slice with nested maps with nested slice with nested map", + args: args{ + nested: map[string]any{ + "a": "foo", + "b": []any{ + map[string]any{ + "c": "bar", + "d": []any{ + map[string]any{ + "e": 2, + }, + true, + }, + }, + map[string]any{ + "c": "baz", + "d": []any{ + map[string]any{ + "e": 3, + }, + false, + }, + }, + }, + }, + }, + want: map[string]any{ + ".a": "foo", + ".b[0].c": "bar", + ".b[0].d[0].e": 2, + ".b[0].d[1]": true, + ".b[1].c": "baz", + ".b[1].d[0].e": 3, + ".b[1].d[1]": false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := flatmap.Do(tt.args.nested); !reflect.DeepEqual(got, tt.want) { + t.Errorf("flatten() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/flatmap/doc.go b/flatmap/doc.go new file mode 100644 index 00000000..4088ef89 --- /dev/null +++ b/flatmap/doc.go @@ -0,0 +1,11 @@ +/* +Package flatmap contains functions to flatten and unflatten maps: + + - [Do] flattens a nested map into a flat map. + - [Undo] unflattens a flat map into a nested map. + +The flattening roughly follows the [JSONPath] standard. + +[JSONPath]: https://goessner.net/articles/JsonPath/ +*/ +package flatmap diff --git a/flatmap/undo.go b/flatmap/undo.go new file mode 100644 index 00000000..73291fea --- /dev/null +++ b/flatmap/undo.go @@ -0,0 +1,191 @@ +package flatmap + +import ( + "fmt" + "strconv" + "strings" +) + +/* +Undo takes a flattened map and nests it into a multi-level map. The flattened +map roughly follows the [JSONPath] standard. Please see test function to +understand how the nested output looks like. Here is an example that may fall +out of date, so be careful: + +If this is the flattened input: + + map[string]any{ + ".a": "foo", + ".b[0].c": "bar", + ".b[0].d[0].e": 2, + ".b[0].d[1]": true, + ".b[1].c": "baz", + ".b[1].d[0].e": 3, + ".b[1].d[1]": false, + } + +You can expect this nested output: + + map[string]any{ + "a": "foo", + "b": []any{ + map[string]any{ + "c": "bar", + "d": []any{ + map[string]any{ + "e": 2, + }, + true, + }, + }, + map[string]any{ + "c": "baz", + "d": []any{ + map[string]any{ + "e": 3, + }, + false, + }, + }, + }, + } + +[JSONPath]: https://goessner.net/articles/JsonPath/ +*/ +func Undo(flattened map[string]any) (map[string]any, error) { + // First, convert the flat map to a nested map. Then reshape the map into a + // slice where appropriate. + const magicSliceKey = "isSlice" + nested := make(map[string]any) + for key, value := range flattened { + p, err := pathFrom(key) + if err != nil { + return nil, err + } + + current := nested + for i, k := range p { + key := k.Key() + if k.IsSlice() { + current[magicSliceKey] = true + } + + isLast := i == len(p)-1 + if isLast { + current[key] = value + break + } + + if current[key] == nil { + current[key] = make(map[string]any) + } + + current = current[key].(map[string]any) + } + } + + // Convert maps to slices where appropriate using non recursive breadth + // first search. + queue := []map[string]any{nested} + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + for k, v := range current { + m, ok := v.(map[string]any) + if !ok { + // Not a map, we reached the end of the tree. + continue + } + + if m[magicSliceKey] == nil { + // Just a normal map, enqueue. + queue = append(queue, m) + continue + } + + // A map that needs to be converted to a slice. + delete(m, magicSliceKey) + slice, err := toSlice(m) + if err != nil { + return nil, err + } + + for _, x := range slice { + if _, ok := x.(map[string]any); ok { + // Enqueue all maps in the slice. + queue = append(queue, x.(map[string]any)) + } + } + current[k] = slice + } + } + return nested, nil +} + +func toSlice(x map[string]any) ([]any, error) { + slice := make([]any, len(x)) + for k, v := range x { + idx, err := strconv.Atoi(k) + if err != nil { + return nil, err + } + + if idx >= len(slice) || idx < 0 { + return nil, fmt.Errorf("index %d out of bounds", idx) + } + + slice[idx] = v + } + return slice, nil +} + +type pathKey struct { + name string + index int +} + +func (p pathKey) IsSlice() bool { + return p.index != -1 +} + +func (p pathKey) Key() string { + if p.IsSlice() { + return strconv.Itoa(p.index) + } + return p.name +} + +type path []pathKey + +func pathFrom(key string) (path, error) { + split := strings.Split(key[1:], ".") + p := make(path, 0, len(split)) + for _, s := range split { + stops, err := pathKeysFrom(s) + if err != nil { + return path{}, err + } + + p = append(p, stops...) + } + + return p, nil +} + +func pathKeysFrom(key string) ([]pathKey, error) { + if strings.Contains(key, "[") { + start := strings.Index(key, "[") + end := strings.Index(key, "]") + index, err := strconv.Atoi(key[start+1 : end]) + if err != nil { + return []pathKey{}, err + } + + return []pathKey{ + {name: key[:start], index: -1}, + {index: index}, + }, nil + } + + return []pathKey{{name: key, index: -1}}, nil +} diff --git a/flatmap/undo_test.go b/flatmap/undo_test.go new file mode 100644 index 00000000..b236f5af --- /dev/null +++ b/flatmap/undo_test.go @@ -0,0 +1,186 @@ +package flatmap_test + +import ( + "reflect" + "testing" + + "github.com/nextmv-io/sdk/flatmap" +) + +func Test_nest(t *testing.T) { + type args struct { + flattened map[string]any + } + tests := []struct { + name string + args args + want any + }{ + { + name: "flat", + args: args{ + flattened: map[string]any{ + ".a": "foo", + ".b": 2, + ".c": true, + }, + }, + want: map[string]any{ + "a": "foo", + "b": 2, + "c": true, + }, + }, + { + name: "flat with nil", + args: args{ + flattened: map[string]any{ + ".a": "foo", + ".b": nil, + ".c": true, + }, + }, + want: map[string]any{ + "a": "foo", + "b": nil, + "c": true, + }, + }, + { + name: "slice", + args: args{ + flattened: map[string]any{ + ".a": "foo", + ".b[0]": "bar", + ".b[1]": 2, + }, + }, + want: map[string]any{ + "a": "foo", + "b": []any{ + "bar", + 2, + }, + }, + }, + { + name: "nested map", + args: args{ + flattened: map[string]any{ + ".a": "foo", + ".b.c": "bar", + ".b.d": 2, + }, + }, + want: map[string]any{ + "a": "foo", + "b": map[string]any{ + "c": "bar", + "d": 2, + }, + }, + }, + { + name: "slice with nested maps", + args: args{ + flattened: map[string]any{ + ".a": "foo", + ".b[0].c": "bar", + ".b[0].d": 2, + ".b[1].c": "baz", + ".b[1].d": 3, + }, + }, + want: map[string]any{ + "a": "foo", + "b": []any{ + map[string]any{ + "c": "bar", + "d": 2, + }, + map[string]any{ + "c": "baz", + "d": 3, + }, + }, + }, + }, + { + name: "slice with nested maps with nested slice", + args: args{ + flattened: map[string]any{ + ".a": "foo", + ".b[0].c": "bar", + ".b[0].d[0]": 2, + ".b[0].d[1]": true, + ".b[1].c": "baz", + ".b[1].d[0]": 3, + ".b[1].d[1]": false, + }, + }, + want: map[string]any{ + "a": "foo", + "b": []any{ + map[string]any{ + "c": "bar", + "d": []any{ + 2, + true, + }, + }, + map[string]any{ + "c": "baz", + "d": []any{ + 3, + false, + }, + }, + }, + }, + }, + { + name: "slice with nested maps with nested slice with nested map", + args: args{ + flattened: map[string]any{ + ".a": "foo", + ".b[0].c": "bar", + ".b[0].d[0].e": 2, + ".b[0].d[1]": true, + ".b[1].c": "baz", + ".b[1].d[0].e": 3, + ".b[1].d[1]": false, + }, + }, + want: map[string]any{ + "a": "foo", + "b": []any{ + map[string]any{ + "c": "bar", + "d": []any{ + map[string]any{ + "e": 2, + }, + true, + }, + }, + map[string]any{ + "c": "baz", + "d": []any{ + map[string]any{ + "e": 3, + }, + false, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, _ := flatmap.Undo(tt.args.flattened); !reflect.DeepEqual(got, tt.want) { + t.Errorf("nest() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/golden/file.go b/golden/file.go index 358cb496..2868467a 100644 --- a/golden/file.go +++ b/golden/file.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/nextmv-io/sdk/flatmap" "github.com/xeipuuv/gojsonschema" ) @@ -171,13 +172,13 @@ func comparison( t.Fatal(err) } - flattenedOutput = flatten(output) + flattenedOutput = flatmap.Do(output) flattenedOutput = replaceTransient(flattenedOutput, config.TransientFields...) flattenedOutput, err = roundFields(flattenedOutput, config.OutputProcessConfig.RoundingConfig...) if err != nil { t.Fatal(err) } - nestedOutput, err := nest(flattenedOutput) + nestedOutput, err := flatmap.Undo(flattenedOutput) if err != nil { t.Fatal(err) } @@ -222,7 +223,7 @@ func comparison( t.Fatal(err) } - flattenedExpected := flatten(expected) + flattenedExpected := flatmap.Do(expected) if len(config.DedicatedComparison) > 0 { for _, key := range config.DedicatedComparison { diff --git a/golden/map.go b/golden/map.go index 43dd5a4a..42b2bd1c 100644 --- a/golden/map.go +++ b/golden/map.go @@ -3,10 +3,7 @@ package golden import ( "fmt" "math" - "reflect" "regexp" - "strconv" - "strings" "time" ) @@ -146,280 +143,3 @@ var keyIndexMatcher = regexp.MustCompile(`\[\d+\]`) func replaceIndicesInKeys(key string) string { return keyIndexMatcher.ReplaceAllString(key, "[]") } - -/* -flatten takes a nested map and flattens it into a single level map. The -flattening roughly follows the [JSONPath] standard. Please see test function to -understand how the flattened output looks like. Here is an example that may -fall out of date, so be careful: - -If this is the nested input: - - map[string]any{ - "a": "foo", - "b": []any{ - map[string]any{ - "c": "bar", - "d": []any{ - map[string]any{ - "e": 2, - }, - true, - }, - }, - map[string]any{ - "c": "baz", - "d": []any{ - map[string]any{ - "e": 3, - }, - false, - }, - }, - }, - } - -You can expect this flattened output: - - map[string]any{ - ".a": "foo", - ".b[0].c": "bar", - ".b[0].d[0].e": 2, - ".b[0].d[1]": true, - ".b[1].c": "baz", - ".b[1].d[0].e": 3, - ".b[1].d[1]": false, - } - -[JSONPath]: https://goessner.net/articles/JsonPath/ -*/ -func flatten(nested map[string]any) map[string]any { - flattened := map[string]any{} - for childKey, childValue := range nested { - setChildren(flattened, childKey, childValue) - } - - return flattened -} - -// setChildren is a helper function for flatten. It is invoked recursively on a -// child value. If the child is not a map or a slice, then the value is simply -// set on the flattened map. If the child is a map or a slice, then the -// function is invoked recursively on the child's values, until a -// non-map-non-slice value is hit. -func setChildren(flattened map[string]any, parentKey string, parentValue any) { - newKey := fmt.Sprintf(".%s", parentKey) - if reflect.TypeOf(parentValue) == nil { - flattened[newKey] = parentValue - return - } - - if reflect.TypeOf(parentValue).Kind() == reflect.Map { - children := parentValue.(map[string]any) - for childKey, childValue := range children { - newKey = fmt.Sprintf("%s.%s", parentKey, childKey) - setChildren(flattened, newKey, childValue) - } - return - } - - if reflect.TypeOf(parentValue).Kind() == reflect.Slice { - children := parentValue.([]any) - if len(children) == 0 { - flattened[newKey] = children - return - } - - for childIndex, childValue := range children { - newKey = fmt.Sprintf("%s[%v]", parentKey, childIndex) - setChildren(flattened, newKey, childValue) - } - return - } - - flattened[newKey] = parentValue -} - -/* -nest takes a flattened map and nests it into a multi-level map. The flattened -map roughly follows the [JSONPath] standard. Please see test function to -understand how the nested output looks like. Here is an example that may fall -out of date, so be careful: - -If this is the flattened input: - - map[string]any{ - ".a": "foo", - ".b[0].c": "bar", - ".b[0].d[0].e": 2, - ".b[0].d[1]": true, - ".b[1].c": "baz", - ".b[1].d[0].e": 3, - ".b[1].d[1]": false, - } - -You can expect this nested output: - - map[string]any{ - "a": "foo", - "b": []any{ - map[string]any{ - "c": "bar", - "d": []any{ - map[string]any{ - "e": 2, - }, - true, - }, - }, - map[string]any{ - "c": "baz", - "d": []any{ - map[string]any{ - "e": 3, - }, - false, - }, - }, - }, - } - -[JSONPath]: https://goessner.net/articles/JsonPath/ -*/ -func nest(flattened map[string]any) (map[string]any, error) { - // First, convert the flat map to a nested map. Then reshape the map into a - // slice where appropriate. - const magicSliceKey = "isSlice" - nested := make(map[string]any) - for key, value := range flattened { - p, err := pathFrom(key) - if err != nil { - return nil, err - } - - current := nested - for i, k := range p { - key := k.Key() - if k.IsSlice() { - current[magicSliceKey] = true - } - - isLast := i == len(p)-1 - if isLast { - current[key] = value - break - } - - if current[key] == nil { - current[key] = make(map[string]any) - } - - current = current[key].(map[string]any) - } - } - - // Convert maps to slices where appropriate using non recursive breadth - // first search. - queue := []map[string]any{nested} - for len(queue) > 0 { - current := queue[0] - queue = queue[1:] - for k, v := range current { - m, ok := v.(map[string]any) - if !ok { - // Not a map, we reached the end of the tree. - continue - } - - if m[magicSliceKey] == nil { - // Just a normal map, enqueue. - queue = append(queue, m) - continue - } - - // A map that needs to be converted to a slice. - delete(m, magicSliceKey) - slice, err := toSlice(m) - if err != nil { - return nil, err - } - - for _, x := range slice { - if _, ok := x.(map[string]any); ok { - // Enqueue all maps in the slice. - queue = append(queue, x.(map[string]any)) - } - } - current[k] = slice - } - } - return nested, nil -} - -func toSlice(x map[string]any) ([]any, error) { - slice := make([]any, len(x)) - for k, v := range x { - idx, err := strconv.Atoi(k) - if err != nil { - return nil, err - } - - if idx >= len(slice) || idx < 0 { - return nil, fmt.Errorf("index %d out of bounds", idx) - } - - slice[idx] = v - } - return slice, nil -} - -type pathKey struct { - name string - index int -} - -func (p pathKey) IsSlice() bool { - return p.index != -1 -} - -func (p pathKey) Key() string { - if p.IsSlice() { - return strconv.Itoa(p.index) - } - return p.name -} - -type path []pathKey - -func pathFrom(key string) (path, error) { - split := strings.Split(key[1:], ".") - p := make(path, 0, len(split)) - for _, s := range split { - stops, err := pathKeysFrom(s) - if err != nil { - return path{}, err - } - - p = append(p, stops...) - } - - return p, nil -} - -func pathKeysFrom(key string) ([]pathKey, error) { - if strings.Contains(key, "[") { - start := strings.Index(key, "[") - end := strings.Index(key, "]") - index, err := strconv.Atoi(key[start+1 : end]) - if err != nil { - return []pathKey{}, err - } - - return []pathKey{ - {name: key[:start], index: -1}, - {index: index}, - }, nil - } - - return []pathKey{{name: key, index: -1}}, nil -} diff --git a/golden/map_test.go b/golden/map_test.go index ac5d37d3..afbe6e8b 100644 --- a/golden/map_test.go +++ b/golden/map_test.go @@ -6,184 +6,6 @@ import ( "testing" ) -func Test_flatten(t *testing.T) { - type args struct { - nested map[string]any - } - tests := []struct { - name string - args args - want map[string]any - }{ - { - name: "flat", - args: args{ - nested: map[string]any{ - "a": "foo", - "b": 2, - "c": true, - }, - }, - want: map[string]any{ - ".a": "foo", - ".b": 2, - ".c": true, - }, - }, - { - name: "flat with nil", - args: args{ - nested: map[string]any{ - "a": "foo", - "b": nil, - "c": true, - }, - }, - want: map[string]any{ - ".a": "foo", - ".b": nil, - ".c": true, - }, - }, - { - name: "slice", - args: args{ - nested: map[string]any{ - "a": "foo", - "b": []any{ - "bar", - 2, - }, - }, - }, - want: map[string]any{ - ".a": "foo", - ".b[0]": "bar", - ".b[1]": 2, - }, - }, - { - name: "nested map", - args: args{ - nested: map[string]any{ - "a": "foo", - "b": map[string]any{ - "c": "bar", - "d": 2, - }, - }, - }, - want: map[string]any{ - ".a": "foo", - ".b.c": "bar", - ".b.d": 2, - }, - }, - { - name: "slice with nested maps", - args: args{ - nested: map[string]any{ - "a": "foo", - "b": []any{ - map[string]any{ - "c": "bar", - "d": 2, - }, - map[string]any{ - "c": "baz", - "d": 3, - }, - }, - }, - }, - want: map[string]any{ - ".a": "foo", - ".b[0].c": "bar", - ".b[0].d": 2, - ".b[1].c": "baz", - ".b[1].d": 3, - }, - }, - { - name: "slice with nested maps with nested slice", - args: args{ - nested: map[string]any{ - "a": "foo", - "b": []any{ - map[string]any{ - "c": "bar", - "d": []any{ - 2, - true, - }, - }, - map[string]any{ - "c": "baz", - "d": []any{ - 3, - false, - }, - }, - }, - }, - }, - want: map[string]any{ - ".a": "foo", - ".b[0].c": "bar", - ".b[0].d[0]": 2, - ".b[0].d[1]": true, - ".b[1].c": "baz", - ".b[1].d[0]": 3, - ".b[1].d[1]": false, - }, - }, - { - name: "slice with nested maps with nested slice with nested map", - args: args{ - nested: map[string]any{ - "a": "foo", - "b": []any{ - map[string]any{ - "c": "bar", - "d": []any{ - map[string]any{ - "e": 2, - }, - true, - }, - }, - map[string]any{ - "c": "baz", - "d": []any{ - map[string]any{ - "e": 3, - }, - false, - }, - }, - }, - }, - }, - want: map[string]any{ - ".a": "foo", - ".b[0].c": "bar", - ".b[0].d[0].e": 2, - ".b[0].d[1]": true, - ".b[1].c": "baz", - ".b[1].d[0].e": 3, - ".b[1].d[1]": false, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := flatten(tt.args.nested); !reflect.DeepEqual(got, tt.want) { - t.Errorf("flatten() = %v, want %v", got, tt.want) - } - }) - } -} - func Test_replaceTransient(t *testing.T) { type args struct { original map[string]any @@ -288,184 +110,6 @@ func Test_replaceTransient(t *testing.T) { } } -func Test_nest(t *testing.T) { - type args struct { - flattened map[string]any - } - tests := []struct { - name string - args args - want any - }{ - { - name: "flat", - args: args{ - flattened: map[string]any{ - ".a": "foo", - ".b": 2, - ".c": true, - }, - }, - want: map[string]any{ - "a": "foo", - "b": 2, - "c": true, - }, - }, - { - name: "flat with nil", - args: args{ - flattened: map[string]any{ - ".a": "foo", - ".b": nil, - ".c": true, - }, - }, - want: map[string]any{ - "a": "foo", - "b": nil, - "c": true, - }, - }, - { - name: "slice", - args: args{ - flattened: map[string]any{ - ".a": "foo", - ".b[0]": "bar", - ".b[1]": 2, - }, - }, - want: map[string]any{ - "a": "foo", - "b": []any{ - "bar", - 2, - }, - }, - }, - { - name: "nested map", - args: args{ - flattened: map[string]any{ - ".a": "foo", - ".b.c": "bar", - ".b.d": 2, - }, - }, - want: map[string]any{ - "a": "foo", - "b": map[string]any{ - "c": "bar", - "d": 2, - }, - }, - }, - { - name: "slice with nested maps", - args: args{ - flattened: map[string]any{ - ".a": "foo", - ".b[0].c": "bar", - ".b[0].d": 2, - ".b[1].c": "baz", - ".b[1].d": 3, - }, - }, - want: map[string]any{ - "a": "foo", - "b": []any{ - map[string]any{ - "c": "bar", - "d": 2, - }, - map[string]any{ - "c": "baz", - "d": 3, - }, - }, - }, - }, - { - name: "slice with nested maps with nested slice", - args: args{ - flattened: map[string]any{ - ".a": "foo", - ".b[0].c": "bar", - ".b[0].d[0]": 2, - ".b[0].d[1]": true, - ".b[1].c": "baz", - ".b[1].d[0]": 3, - ".b[1].d[1]": false, - }, - }, - want: map[string]any{ - "a": "foo", - "b": []any{ - map[string]any{ - "c": "bar", - "d": []any{ - 2, - true, - }, - }, - map[string]any{ - "c": "baz", - "d": []any{ - 3, - false, - }, - }, - }, - }, - }, - { - name: "slice with nested maps with nested slice with nested map", - args: args{ - flattened: map[string]any{ - ".a": "foo", - ".b[0].c": "bar", - ".b[0].d[0].e": 2, - ".b[0].d[1]": true, - ".b[1].c": "baz", - ".b[1].d[0].e": 3, - ".b[1].d[1]": false, - }, - }, - want: map[string]any{ - "a": "foo", - "b": []any{ - map[string]any{ - "c": "bar", - "d": []any{ - map[string]any{ - "e": 2, - }, - true, - }, - }, - map[string]any{ - "c": "baz", - "d": []any{ - map[string]any{ - "e": 3, - }, - false, - }, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got, _ := nest(tt.args.flattened); !reflect.DeepEqual(got, tt.want) { - t.Errorf("nest() = %v, want %v", got, tt.want) - } - }) - } -} - func Test_round(t *testing.T) { tests := []struct { num float64