diff --git a/dict.go b/dict.go index 4315b35..73193b9 100644 --- a/dict.go +++ b/dict.go @@ -1,6 +1,9 @@ package sprig import ( + "fmt" + "reflect" + "dario.cat/mergo" "github.com/mitchellh/copystructure" ) @@ -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{} { diff --git a/dict_test.go b/dict_test.go index c829daa..604b9f8 100644 --- a/dict_test.go +++ b/dict_test.go @@ -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",