diff options
| -rw-r--r-- | coll/coll.go | 28 | ||||
| -rw-r--r-- | coll/coll_test.go | 110 | ||||
| -rw-r--r-- | docs-src/content/functions/coll.yml | 22 | ||||
| -rw-r--r-- | docs/content/functions/coll.md | 36 | ||||
| -rw-r--r-- | funcs/coll.go | 15 | ||||
| -rw-r--r-- | funcs/coll_test.go | 25 | ||||
| -rw-r--r-- | go.sum | 1 | ||||
| -rw-r--r-- | tests/integration/collection_test.go | 19 |
8 files changed, 255 insertions, 1 deletions
diff --git a/coll/coll.go b/coll/coll.go index 99911d0a..8e41ed91 100644 --- a/coll/coll.go +++ b/coll/coll.go @@ -292,3 +292,31 @@ func sameTypes(a []interface{}) bool { } return true } + +// Flatten a nested array or slice to at most 'depth' levels. Use depth of -1 +// to completely flatten the input. +// Returns a new slice without modifying the input. +func Flatten(list interface{}, depth int) (out []interface{}, err error) { + l, err := interfaceSlice(list) + if err != nil { + return nil, err + } + if depth == 0 { + return l, nil + } + for _, v := range l { + s := reflect.ValueOf(v) + kind := s.Kind() + switch kind { + case reflect.Slice, reflect.Array: + vl, err := Flatten(v, depth-1) + if err != nil { + return nil, err + } + out = append(out, vl...) + default: + out = append(out, v) + } + } + return out, nil +} diff --git a/coll/coll_test.go b/coll/coll_test.go index a4c34a5c..6110a78d 100644 --- a/coll/coll_test.go +++ b/coll/coll_test.go @@ -15,7 +15,6 @@ func TestSlice(t *testing.T) { } func TestHas(t *testing.T) { - in := map[string]interface{}{ "foo": "bar", "baz": map[string]interface{}{ @@ -457,3 +456,112 @@ func TestSort(t *testing.T) { }) } } + +func TestFlatten(t *testing.T) { + data := []struct { + depth int + in interface{} + expected []interface{} + }{ + {0, []int{1, 2, 3}, []interface{}{1, 2, 3}}, + {0, [3]int{1, 2, 3}, []interface{}{1, 2, 3}}, + {0, + []interface{}{[]string{}, []int{1, 2}, 3}, + []interface{}{[]string{}, []int{1, 2}, 3}, + }, + {0, + []interface{}{[]string{"one"}, [][]int{{1, 2}}, 3}, + []interface{}{[]string{"one"}, [][]int{{1, 2}}, 3}, + }, + + {1, []int{1, 2, 3}, []interface{}{1, 2, 3}}, + {1, [3]int{1, 2, 3}, []interface{}{1, 2, 3}}, + {1, []interface{}{[]string{}, []int{1, 2}, 3}, []interface{}{1, 2, 3}}, + {1, + []interface{}{[]string{"one"}, [][]int{{1, 2}}, 3}, + []interface{}{"one", []int{1, 2}, 3}, + }, + + {2, []int{1, 2, 3}, []interface{}{1, 2, 3}}, + {2, [3]int{1, 2, 3}, []interface{}{1, 2, 3}}, + {2, []interface{}{[]string{}, []int{1, 2}, 3}, []interface{}{1, 2, 3}}, + {2, + []interface{}{[]string{"one"}, [][]int{{1, 2}}, 3}, + []interface{}{"one", 1, 2, 3}, + }, + {2, + []interface{}{ + []string{"one"}, + []interface{}{ + []interface{}{ + []int{1}, + []interface{}{2, []int{3}}, + }, + []int{4, 5}, + }, + 6, + }, + []interface{}{"one", []int{1}, []interface{}{2, []int{3}}, 4, 5, 6}, + }, + + {-1, []int{1, 2, 3}, []interface{}{1, 2, 3}}, + {-1, [3]int{1, 2, 3}, []interface{}{1, 2, 3}}, + {-1, []interface{}{[]string{}, []int{1, 2}, 3}, []interface{}{1, 2, 3}}, + {-1, + []interface{}{[]string{"one"}, [][]int{{1, 2}}, 3}, + []interface{}{"one", 1, 2, 3}, + }, + {-1, + []interface{}{ + []string{"one"}, + []interface{}{ + []interface{}{ + []int{1}, + []interface{}{2, []int{3}}, + }, + []int{4, 5}, + }, + 6, + }, + []interface{}{"one", 1, 2, 3, 4, 5, 6}, + }, + } + + for _, d := range data { + out, err := Flatten(d.in, d.depth) + assert.NoError(t, err) + assert.EqualValues(t, d.expected, out) + } + + _, err := Flatten(42, -1) + assert.Error(t, err) +} + +func BenchmarkFlatten(b *testing.B) { + data := []interface{}{ + []int{1, 2, 3}, + [3]int{1, 2, 3}, + []interface{}{[]string{}, []int{1, 2}, 3}, + []interface{}{[]string{"one"}, [][]int{{1, 2}}, 3}, + []interface{}{ + []string{"one"}, + []interface{}{ + []interface{}{ + []int{1}, + []interface{}{2, []int{3}}, + }, + []int{4, 5}, + }, + 6, + }, + } + for depth := -1; depth <= 2; depth++ { + for _, d := range data { + b.Run(fmt.Sprintf("depth%d %T(%v)", depth, d, d), func(b *testing.B) { + for i := 0; i < b.N; i++ { + Flatten(d, depth) + } + }) + } + } +} diff --git a/docs-src/content/functions/coll.yml b/docs-src/content/functions/coll.yml index 62d3cbcc..8a9917f1 100644 --- a/docs-src/content/functions/coll.yml +++ b/docs-src/content/functions/coll.yml @@ -200,6 +200,28 @@ funcs: - | $ gomplate -i '{{ slice 1 2 3 2 3 4 1 5 | uniq }}' [1 2 3 4 5] + - name: coll.Flatten + alias: flatten + description: | + Flatten a nested list. Defaults to completely flattening all nested lists, + but can be limited with `depth`. + + _Note that this function does not change the given list; it always produces a new one._ + pipeline: true + arguments: + - name: depth + required: false + description: maximum depth of nested lists to flatten. Omit or set to `-1` for infinite depth. + - name: list + required: true + description: the input list + examples: + - | + $ gomplate -i '{{ "[[1,2],[],[[3,4],[[[5],6],7]]]" | jsonArray | flatten }}' + [1 2 3 4 5 6 7] + - | + $ gomplate -i '{{ coll.Flatten 2 ("[[1,2],[],[[3,4],[[[5],6],7]]]" | jsonArray) }}' + [1 2 3 4 [[5] 6] 7] - name: coll.Reverse alias: reverse description: | diff --git a/docs/content/functions/coll.md b/docs/content/functions/coll.md index 869d95a7..6ce9645b 100644 --- a/docs/content/functions/coll.md +++ b/docs/content/functions/coll.md @@ -326,6 +326,42 @@ $ gomplate -i '{{ slice 1 2 3 2 3 4 1 5 | uniq }}' [1 2 3 4 5] ``` +## `coll.Flatten` + +**Alias:** `flatten` + +Flatten a nested list. Defaults to completely flattening all nested lists, +but can be limited with `depth`. + +_Note that this function does not change the given list; it always produces a new one._ + +### Usage + +```go +coll.Flatten [depth] list +``` +```go +list | coll.Flatten [depth] +``` + +### Arguments + +| name | description | +|------|-------------| +| `depth` | _(optional)_ maximum depth of nested lists to flatten. Omit or set to `-1` for infinite depth. | +| `list` | _(required)_ the input list | + +### Examples + +```console +$ gomplate -i '{{ "[[1,2],[],[[3,4],[[[5],6],7]]]" | jsonArray | flatten }}' +[1 2 3 4 5 6 7] +``` +```console +$ gomplate -i '{{ coll.Flatten 2 ("[[1,2],[],[[3,4],[[[5],6],7]]]" | jsonArray) }}' +[1 2 3 4 [[5] 6] 7] +``` + ## `coll.Reverse` **Alias:** `reverse` diff --git a/funcs/coll.go b/funcs/coll.go index 26d2ed05..62ebed61 100644 --- a/funcs/coll.go +++ b/funcs/coll.go @@ -36,6 +36,7 @@ func AddCollFuncs(f map[string]interface{}) { f["merge"] = CollNS().Merge f["sort"] = CollNS().Sort f["jsonpath"] = CollNS().JSONPath + f["flatten"] = CollNS().Flatten } // CollFuncs - @@ -114,3 +115,17 @@ func (f *CollFuncs) Sort(args ...interface{}) ([]interface{}, error) { func (f *CollFuncs) JSONPath(p string, in interface{}) (interface{}, error) { return coll.JSONPath(p, in) } + +// Flatten - +func (f *CollFuncs) Flatten(args ...interface{}) ([]interface{}, error) { + if len(args) == 0 || len(args) > 2 { + return nil, errors.Errorf("wrong number of args: wanted 1 or 2, got %d", len(args)) + } + list := args[0] + depth := -1 + if len(args) == 2 { + depth = conv.ToInt(args[0]) + list = args[1] + } + return coll.Flatten(list, depth) +} diff --git a/funcs/coll_test.go b/funcs/coll_test.go new file mode 100644 index 00000000..135859ba --- /dev/null +++ b/funcs/coll_test.go @@ -0,0 +1,25 @@ +package funcs + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFlatten(t *testing.T) { + c := CollNS() + + _, err := c.Flatten() + assert.Error(t, err) + + _, err = c.Flatten(42) + assert.Error(t, err) + + out, err := c.Flatten([]interface{}{1, []interface{}{[]int{2}, 3}}) + assert.NoError(t, err) + assert.EqualValues(t, []interface{}{1, 2, 3}, out) + + out, err = c.Flatten(1, []interface{}{1, []interface{}{[]int{2}, 3}}) + assert.NoError(t, err) + assert.EqualValues(t, []interface{}{1, []int{2}, 3}, out) +} @@ -216,6 +216,7 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= diff --git a/tests/integration/collection_test.go b/tests/integration/collection_test.go index 58b9e8d6..f593777a 100644 --- a/tests/integration/collection_test.go +++ b/tests/integration/collection_test.go @@ -99,3 +99,22 @@ func (s *CollSuite) TestJSONPath(c *C) { "-i", `{{ .config | coll.JSONPath ".values..a" }}`)) result.Assert(c, icmd.Expected{ExitCode: 0, Out: `eh?`}) } + +func (s *CollSuite) TestFlatten(c *C) { + in := "[[1,2],[],[[3,4],[[[5],6],7]]]" + result := icmd.RunCmd(icmd.Command(GomplateBin, + "-i", "{{ `"+in+"` | jsonArray | coll.Flatten | toJSON }}")) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "[1,2,3,4,5,6,7]"}) + + result = icmd.RunCmd(icmd.Command(GomplateBin, + "-i", "{{ `"+in+"` | jsonArray | flatten 0 | toJSON }}")) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: in}) + + result = icmd.RunCmd(icmd.Command(GomplateBin, + "-i", "{{ coll.Flatten 1 (`"+in+"` | jsonArray) | toJSON }}")) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "[1,2,[3,4],[[[5],6],7]]"}) + + result = icmd.RunCmd(icmd.Command(GomplateBin, + "-i", "{{ `"+in+"` | jsonArray | coll.Flatten 2 | toJSON }}")) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "[1,2,3,4,[[5],6],7]"}) +} |
