From a2670bf460e10ed5de69f90abbe7c4e2b32068cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 11 Nov 2019 14:37:37 +0100 Subject: [PATCH] tpl/collections: Allow dict to create nested structures Fixes #6497 --- docs/content/en/functions/dict.md | 6 ++++++ tpl/collections/collections.go | 29 ++++++++++++++++++++++++----- tpl/collections/collections_test.go | 25 ++++++++++++++++--------- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/docs/content/en/functions/dict.md b/docs/content/en/functions/dict.md index 007cc30c5..76fdde284 100644 --- a/docs/content/en/functions/dict.md +++ b/docs/content/en/functions/dict.md @@ -21,6 +21,12 @@ aliases: [] `dict` is especially useful for passing more than one value to a partial template. +Note that the `key` can be either a `string` or a `string slice`. The latter is useful to create a deply nested structure, e.g.: + +```go-text-template +{{ $m := dict (slice "a" "b" "c") "value" }} +``` + ## Example: Using `dict` to pass multiple values to a `partial` diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go index 0f0fd7ffb..8d89d6255 100644 --- a/tpl/collections/collections.go +++ b/tpl/collections/collections.go @@ -145,22 +145,41 @@ func (ns *Namespace) Delimit(seq, delimiter interface{}, last ...interface{}) (t // Dictionary creates a map[string]interface{} from the given parameters by // walking the parameters and treating them as key-value pairs. The number // of parameters must be even. +// The keys can be string slices, which will create the needed nested structure. func (ns *Namespace) Dictionary(values ...interface{}) (map[string]interface{}, error) { if len(values)%2 != 0 { return nil, errors.New("invalid dictionary call") } - dict := make(map[string]interface{}, len(values)/2) + root := make(map[string]interface{}) for i := 0; i < len(values); i += 2 { - key, ok := values[i].(string) - if !ok { - return nil, errors.New("dictionary keys must be strings") + dict := root + var key string + switch v := values[i].(type) { + case string: + key = v + case []string: + for i := 0; i < len(v)-1; i++ { + key = v[i] + var m map[string]interface{} + v, found := dict[key] + if found { + m = v.(map[string]interface{}) + } else { + m = make(map[string]interface{}) + dict[key] = m + } + dict = m + } + key = v[len(v)-1] + default: + return nil, errors.New("invalid dictionary key") } dict[key] = values[i+1] } - return dict, nil + return root, nil } // EchoParam returns a given value if it is set; otherwise, it returns an diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go index 3830a7505..cfbcd312b 100644 --- a/tpl/collections/collections_test.go +++ b/tpl/collections/collections_test.go @@ -182,7 +182,6 @@ func TestDelimit(t *testing.T) { } func TestDictionary(t *testing.T) { - t.Parallel() c := qt.New(t) ns := New(&deps.Deps{}) @@ -192,22 +191,30 @@ func TestDictionary(t *testing.T) { expect interface{} }{ {[]interface{}{"a", "b"}, map[string]interface{}{"a": "b"}}, + {[]interface{}{[]string{"a", "b"}, "c"}, map[string]interface{}{"a": map[string]interface{}{"b": "c"}}}, + {[]interface{}{[]string{"a", "b"}, "c", []string{"a", "b2"}, "c2", "b", "c"}, + map[string]interface{}{"a": map[string]interface{}{"b": "c", "b2": "c2"}, "b": "c"}}, {[]interface{}{"a", 12, "b", []int{4}}, map[string]interface{}{"a": 12, "b": []int{4}}}, // errors {[]interface{}{5, "b"}, false}, {[]interface{}{"a", "b", "c"}, false}, } { - errMsg := qt.Commentf("[%d] %v", i, test.values) + i := i + test := test + c.Run(fmt.Sprint(i), func(c *qt.C) { + c.Parallel() + errMsg := qt.Commentf("[%d] %v", i, test.values) - result, err := ns.Dictionary(test.values...) + result, err := ns.Dictionary(test.values...) - if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil), errMsg) - continue - } + if b, ok := test.expect.(bool); ok && !b { + c.Assert(err, qt.Not(qt.IsNil), errMsg) + return + } - c.Assert(err, qt.IsNil, errMsg) - c.Assert(result, qt.DeepEquals, test.expect, errMsg) + c.Assert(err, qt.IsNil, errMsg) + c.Assert(result, qt.DeepEquals, test.expect, qt.Commentf(fmt.Sprint(result))) + }) } }