Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions dict.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package sprig

import (
"fmt"
"reflect"

"dario.cat/mergo"
"github.com/mitchellh/copystructure"
)
Expand Down Expand Up @@ -37,14 +40,29 @@ func pluck(key string, d ...map[string]interface{}) []interface{} {
return res
}

func keys(dicts ...map[string]interface{}) []string {
// keys returns the string keys of one or more maps. Unlike most sprig dict
// functions, it accepts maps with any value type (e.g. map[string]string,
// map[string]*MyStruct), not just map[string]interface{}, since text/template
// performs static argument type-checking and would otherwise reject any map
// whose value type isn't exactly interface{}.
func keys(dicts ...interface{}) ([]string, error) {
k := []string{}
for _, dict := range dicts {
for key := range dict {
k = append(k, key)
v := reflect.ValueOf(dict)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Map {
return nil, fmt.Errorf("keys: argument must be a map, got %T", dict)
}
for _, mk := range v.MapKeys() {
if mk.Kind() != reflect.String {
return nil, fmt.Errorf("keys: map key type must be string, got %s", mk.Kind())
}
k = append(k, mk.String())
}
}
return k
return k, nil
}

func pick(dict map[string]interface{}, keys ...string) map[string]interface{} {
Expand Down
36 changes: 36 additions & 0 deletions dict_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,42 @@ func TestKeys(t *testing.T) {
}
}

func TestKeysNonInterfaceMaps(t *testing.T) {
// keys must accept maps whose value type isn't interface{} (e.g.
// map[string]string, map[string]int), since Go's text/template does
// static argument type-checking and would otherwise reject them before
// the function body ever runs. See #455.
tests := []struct {
tpl string
expect string
vars interface{}
}{
{
tpl: `{{ .M | keys | sortAlpha }}`,
expect: "[bar foo]",
vars: map[string]interface{}{"M": map[string]string{"foo": "1", "bar": "2"}},
},
{
tpl: `{{ .M | keys | sortAlpha }}`,
expect: "[bar foo]",
vars: map[string]interface{}{"M": map[string]int{"foo": 1, "bar": 2}},
},
}
for _, tt := range tests {
if err := runtv(tt.tpl, tt.expect, tt.vars); err != nil {
t.Error(err)
}
}
}

func TestKeysNonMapErrors(t *testing.T) {
tpl := `{{ .M | keys }}`
_, err := runRaw(tpl, map[string]interface{}{"M": "not a map"})
if err == nil {
t.Error("expected an error when keys is given a non-map argument")
}
}

func TestPick(t *testing.T) {
tests := map[string]string{
`{{- $d := dict "one" 1 "two" 222222 }}{{ pick $d "two" | len -}}`: "1",
Expand Down