summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coll/coll.go28
-rw-r--r--coll/coll_test.go110
-rw-r--r--docs-src/content/functions/coll.yml22
-rw-r--r--docs/content/functions/coll.md36
-rw-r--r--funcs/coll.go15
-rw-r--r--funcs/coll_test.go25
-rw-r--r--go.sum1
-rw-r--r--tests/integration/collection_test.go19
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)
+}
diff --git a/go.sum b/go.sum
index 54755b8c..943466f3 100644
--- a/go.sum
+++ b/go.sum
@@ -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]"})
+}