Skip to content

Commit 9424885

Browse files
Merge pull request #458 from nextmv-io/feature/flatmap
Create the flatmap package
2 parents 4134c6f + f2aaba0 commit 9424885

File tree

10 files changed

+703
-639
lines changed

10 files changed

+703
-639
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ problems. Please find the following packages:
77
output.
88
- [measure][measure]: measures for various distances between locations.
99
- [golden][golden]: tools for running tests with golden files.
10+
- [flatmap][flatmap]: functionality for flattening and unflattening maps.
1011

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

@@ -22,3 +23,4 @@ behaviors when we have a good reason to.
2223
[measure]: ./measure
2324
[golden]: ./golden
2425
[docs]: https://docs.nextmv.io
26+
[flatmap]: ./flatmap

flatmap/do.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package flatmap
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
)
7+
8+
/*
9+
Do takes a nested map and flattens it into a single level map. The flattening
10+
roughly follows the [JSONPath] standard. Please see the example to understand
11+
how the flattened output looks like.
12+
13+
[JSONPath]: https://goessner.net/articles/JsonPath/
14+
*/
15+
func Do(nested map[string]any) map[string]any {
16+
flattened := map[string]any{}
17+
for childKey, childValue := range nested {
18+
setChildren(flattened, childKey, childValue)
19+
}
20+
21+
return flattened
22+
}
23+
24+
// setChildren is a helper function for flatten. It is invoked recursively on a
25+
// child value. If the child is not a map or a slice, then the value is simply
26+
// set on the flattened map. If the child is a map or a slice, then the
27+
// function is invoked recursively on the child's values, until a
28+
// non-map-non-slice value is hit.
29+
func setChildren(flattened map[string]any, parentKey string, parentValue any) {
30+
newKey := fmt.Sprintf(".%s", parentKey)
31+
if reflect.TypeOf(parentValue) == nil {
32+
flattened[newKey] = parentValue
33+
return
34+
}
35+
36+
if reflect.TypeOf(parentValue).Kind() == reflect.Map {
37+
children := parentValue.(map[string]any)
38+
for childKey, childValue := range children {
39+
newKey = fmt.Sprintf("%s.%s", parentKey, childKey)
40+
setChildren(flattened, newKey, childValue)
41+
}
42+
return
43+
}
44+
45+
if reflect.TypeOf(parentValue).Kind() == reflect.Slice {
46+
children := parentValue.([]any)
47+
if len(children) == 0 {
48+
flattened[newKey] = children
49+
return
50+
}
51+
52+
for childIndex, childValue := range children {
53+
newKey = fmt.Sprintf("%s[%v]", parentKey, childIndex)
54+
setChildren(flattened, newKey, childValue)
55+
}
56+
return
57+
}
58+
59+
flattened[newKey] = parentValue
60+
}

flatmap/do_test.go

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package flatmap_test
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/nextmv-io/sdk/flatmap"
8+
)
9+
10+
func Test_Do(t *testing.T) {
11+
type args struct {
12+
nested map[string]any
13+
}
14+
tests := []struct {
15+
name string
16+
args args
17+
want map[string]any
18+
}{
19+
{
20+
name: "flat",
21+
args: args{
22+
nested: map[string]any{
23+
"a": "foo",
24+
"b": 2,
25+
"c": true,
26+
},
27+
},
28+
want: map[string]any{
29+
".a": "foo",
30+
".b": 2,
31+
".c": true,
32+
},
33+
},
34+
{
35+
name: "flat with nil",
36+
args: args{
37+
nested: map[string]any{
38+
"a": "foo",
39+
"b": nil,
40+
"c": true,
41+
},
42+
},
43+
want: map[string]any{
44+
".a": "foo",
45+
".b": nil,
46+
".c": true,
47+
},
48+
},
49+
{
50+
name: "slice",
51+
args: args{
52+
nested: map[string]any{
53+
"a": "foo",
54+
"b": []any{
55+
"bar",
56+
2,
57+
},
58+
},
59+
},
60+
want: map[string]any{
61+
".a": "foo",
62+
".b[0]": "bar",
63+
".b[1]": 2,
64+
},
65+
},
66+
{
67+
name: "nested map",
68+
args: args{
69+
nested: map[string]any{
70+
"a": "foo",
71+
"b": map[string]any{
72+
"c": "bar",
73+
"d": 2,
74+
},
75+
},
76+
},
77+
want: map[string]any{
78+
".a": "foo",
79+
".b.c": "bar",
80+
".b.d": 2,
81+
},
82+
},
83+
{
84+
name: "slice with nested maps",
85+
args: args{
86+
nested: map[string]any{
87+
"a": "foo",
88+
"b": []any{
89+
map[string]any{
90+
"c": "bar",
91+
"d": 2,
92+
},
93+
map[string]any{
94+
"c": "baz",
95+
"d": 3,
96+
},
97+
},
98+
},
99+
},
100+
want: map[string]any{
101+
".a": "foo",
102+
".b[0].c": "bar",
103+
".b[0].d": 2,
104+
".b[1].c": "baz",
105+
".b[1].d": 3,
106+
},
107+
},
108+
{
109+
name: "slice with nested maps with nested slice",
110+
args: args{
111+
nested: map[string]any{
112+
"a": "foo",
113+
"b": []any{
114+
map[string]any{
115+
"c": "bar",
116+
"d": []any{
117+
2,
118+
true,
119+
},
120+
},
121+
map[string]any{
122+
"c": "baz",
123+
"d": []any{
124+
3,
125+
false,
126+
},
127+
},
128+
},
129+
},
130+
},
131+
want: map[string]any{
132+
".a": "foo",
133+
".b[0].c": "bar",
134+
".b[0].d[0]": 2,
135+
".b[0].d[1]": true,
136+
".b[1].c": "baz",
137+
".b[1].d[0]": 3,
138+
".b[1].d[1]": false,
139+
},
140+
},
141+
{
142+
name: "slice with nested maps with nested slice with nested map",
143+
args: args{
144+
nested: map[string]any{
145+
"a": "foo",
146+
"b": []any{
147+
map[string]any{
148+
"c": "bar",
149+
"d": []any{
150+
map[string]any{
151+
"e": 2,
152+
},
153+
true,
154+
},
155+
},
156+
map[string]any{
157+
"c": "baz",
158+
"d": []any{
159+
map[string]any{
160+
"e": 3,
161+
},
162+
false,
163+
},
164+
},
165+
},
166+
},
167+
},
168+
want: map[string]any{
169+
".a": "foo",
170+
".b[0].c": "bar",
171+
".b[0].d[0].e": 2,
172+
".b[0].d[1]": true,
173+
".b[1].c": "baz",
174+
".b[1].d[0].e": 3,
175+
".b[1].d[1]": false,
176+
},
177+
},
178+
}
179+
for _, tt := range tests {
180+
t.Run(tt.name, func(t *testing.T) {
181+
if got := flatmap.Do(tt.args.nested); !reflect.DeepEqual(got, tt.want) {
182+
t.Errorf("flatten() = %v, want %v", got, tt.want)
183+
}
184+
})
185+
}
186+
}

flatmap/doc.go

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
Package flatmap contains functions to flatten and unflatten maps:
3+
4+
- [Do] flattens a nested map into a flat map.
5+
- [Undo] unflattens a flat map into a nested map.
6+
7+
The flattening roughly follows the [JSONPath] standard.
8+
9+
[JSONPath]: https://goessner.net/articles/JsonPath/
10+
*/
11+
package flatmap

flatmap/example_flatmap_test.go

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package flatmap_test
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/nextmv-io/sdk/flatmap"
8+
)
9+
10+
func ExampleDo() {
11+
nested := map[string]any{
12+
"a": "foo",
13+
"b": []any{
14+
map[string]any{
15+
"c": "bar",
16+
"d": []any{
17+
map[string]any{
18+
"e": 2,
19+
},
20+
true,
21+
},
22+
},
23+
map[string]any{
24+
"c": "baz",
25+
"d": []any{
26+
map[string]any{
27+
"e": 3,
28+
},
29+
false,
30+
},
31+
},
32+
},
33+
}
34+
35+
flattened := flatmap.Do(nested)
36+
37+
b, err := json.MarshalIndent(flattened, "", " ")
38+
if err != nil {
39+
panic(err)
40+
}
41+
42+
fmt.Println(string(b))
43+
44+
// Output:
45+
// {
46+
// ".a": "foo",
47+
// ".b[0].c": "bar",
48+
// ".b[0].d[0].e": 2,
49+
// ".b[0].d[1]": true,
50+
// ".b[1].c": "baz",
51+
// ".b[1].d[0].e": 3,
52+
// ".b[1].d[1]": false
53+
// }
54+
}
55+
56+
func ExampleUndo() {
57+
flattened := map[string]any{
58+
".a": "foo",
59+
".b[0].c": "bar",
60+
".b[0].d[0].e": 2,
61+
".b[0].d[1]": true,
62+
".b[1].c": "baz",
63+
".b[1].d[0].e": 3,
64+
".b[1].d[1]": false,
65+
}
66+
67+
nested, err := flatmap.Undo(flattened)
68+
if err != nil {
69+
panic(err)
70+
}
71+
72+
b, err := json.MarshalIndent(nested, "", " ")
73+
if err != nil {
74+
panic(err)
75+
}
76+
77+
fmt.Println(string(b))
78+
// Output:
79+
// {
80+
// "a": "foo",
81+
// "b": [
82+
// {
83+
// "c": "bar",
84+
// "d": [
85+
// {
86+
// "e": 2
87+
// },
88+
// true
89+
// ]
90+
// },
91+
// {
92+
// "c": "baz",
93+
// "d": [
94+
// {
95+
// "e": 3
96+
// },
97+
// false
98+
// ]
99+
// }
100+
// ]
101+
// }
102+
}

0 commit comments

Comments
 (0)