Skip to content

Commit

Permalink
Create the flatmap package
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastian-quintero committed Aug 8, 2024
1 parent 4134c6f commit 9faa6a0
Show file tree
Hide file tree
Showing 8 changed files with 677 additions and 639 deletions.
99 changes: 99 additions & 0 deletions flatmap/do.go
Original file line number Diff line number Diff line change
@@ -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
}
186 changes: 186 additions & 0 deletions flatmap/do_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
11 changes: 11 additions & 0 deletions flatmap/doc.go
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 9faa6a0

Please sign in to comment.