tpl/collections: Allow dict to create nested structures
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 11 Nov 2019 13:37:37 +0000 (14:37 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 11 Nov 2019 20:55:16 +0000 (21:55 +0100)
Fixes #6497

docs/content/en/functions/dict.md
tpl/collections/collections.go
tpl/collections/collections_test.go

index 007cc30c532acc0524e88d9653e433e96457d02a..76fdde2843baf75fea74b891f3993b8beb8afbd9 100644 (file)
@@ -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`
 
index 0f0fd7ffb645c5b13a3b5846b10dc7e42b835cdc..8d89d6255b8d470152d6c408e29383bdaf5b592d 100644 (file)
@@ -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
index 3830a750553a5f02b51be65dbc7067b73cb5d3fe..cfbcd312bc324df7eeb7b89a64a951e0dc5d2d33 100644 (file)
@@ -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)
-
-               result, err := ns.Dictionary(test.values...)
-
-               if b, ok := test.expect.(bool); ok && !b {
-                       c.Assert(err, qt.Not(qt.IsNil), errMsg)
-                       continue
-               }
-
-               c.Assert(err, qt.IsNil, errMsg)
-               c.Assert(result, qt.DeepEquals, test.expect, errMsg)
+               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...)
+
+                       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, qt.Commentf(fmt.Sprint(result)))
+               })
        }
 }