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 f2aaba0
Show file tree
Hide file tree
Showing 10 changed files with 703 additions and 639 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ problems. Please find the following packages:
output.
- [measure][measure]: measures for various distances between locations.
- [golden][golden]: tools for running tests with golden files.
- [flatmap][flatmap]: functionality for flattening and unflattening maps.

Please visit the official [Nextmv docs][docs] for comprehensive information.

Expand All @@ -22,3 +23,4 @@ behaviors when we have a good reason to.
[measure]: ./measure
[golden]: ./golden
[docs]: https://docs.nextmv.io
[flatmap]: ./flatmap
60 changes: 60 additions & 0 deletions flatmap/do.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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 example to understand
how the flattened output looks like.
[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
102 changes: 102 additions & 0 deletions flatmap/example_flatmap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package flatmap_test

import (
"encoding/json"
"fmt"

"github.com/nextmv-io/sdk/flatmap"
)

func ExampleDo() {
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,
},
},
},
}

flattened := flatmap.Do(nested)

b, err := json.MarshalIndent(flattened, "", " ")
if err != nil {
panic(err)
}

fmt.Println(string(b))

// Output:
// {
// ".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
// }
}

func ExampleUndo() {
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,
}

nested, err := flatmap.Undo(flattened)
if err != nil {
panic(err)
}

b, err := json.MarshalIndent(nested, "", " ")
if err != nil {
panic(err)
}

fmt.Println(string(b))
// Output:
// {
// "a": "foo",
// "b": [
// {
// "c": "bar",
// "d": [
// {
// "e": 2
// },
// true
// ]
// },
// {
// "c": "baz",
// "d": [
// {
// "e": 3
// },
// false
// ]
// }
// ]
// }
}
Loading

0 comments on commit f2aaba0

Please sign in to comment.