Skip to content

Commit f3d07ad

Browse files
committed
Support map value groups
This revision allows dig to specify value groups of map type. For example: ``` type Params struct { dig.In Things []int `group:"foogroup"` MapOfThings map[string]int `group:"foogroup"` } type Result struct { dig.Out Int1 int `name:"foo1" group:"foogroup"` Int2 int `name:"foo2" group:"foogroup"` Int3 int `name:"foo3" group:"foogroup"` } c.Provide(func() Result { return Result{Int1: 1, Int2: 2, Int3: 3} }) c.Invoke(func(p Params) { }) ``` p.Things will be a value group slice as per usual, containing the elements {1,2,3} in an arbitrary order. p.MapOfThings will be a key-value pairing of {"foo1":1, "foo2":2, "foo3":3}.
1 parent d86944e commit f3d07ad

File tree

4 files changed

+141
-29
lines changed

4 files changed

+141
-29
lines changed

dig_test.go

+79
Original file line numberDiff line numberDiff line change
@@ -1691,6 +1691,85 @@ func TestGroups(t *testing.T) {
16911691
assert.ElementsMatch(t, []string{"a"}, param.Value)
16921692
})
16931693
})
1694+
/* map tests */
1695+
t.Run("empty map received without provides", func(t *testing.T) {
1696+
c := digtest.New(t)
1697+
1698+
type in struct {
1699+
dig.In
1700+
1701+
Values map[string]int `group:"foo"`
1702+
}
1703+
1704+
c.RequireInvoke(func(i in) {
1705+
require.Empty(t, i.Values)
1706+
})
1707+
})
1708+
t.Run("values are provided, map and name and slice", func(t *testing.T) {
1709+
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))
1710+
1711+
type out struct {
1712+
dig.Out
1713+
1714+
Value1 int `name:"value1" group:"val"`
1715+
Value2 int `name:"value2" group:"val"`
1716+
Value3 int `name:"value3" group:"val"`
1717+
}
1718+
1719+
c.RequireProvide(func() out {
1720+
return out{Value1: 1, Value2: 2, Value3: 3}
1721+
})
1722+
1723+
type in struct {
1724+
dig.In
1725+
1726+
Value1 int `name:"value1"`
1727+
Value2 int `name:"value2"`
1728+
Value3 int `name:"value3"`
1729+
Values []int `group:"val"`
1730+
ValueMap map[string]int `group:"val"`
1731+
}
1732+
1733+
c.RequireInvoke(func(i in) {
1734+
assert.Equal(t, []int{2, 3, 1}, i.Values)
1735+
assert.Equal(t, i.ValueMap["value1"], 1)
1736+
assert.Equal(t, i.ValueMap["value2"], 2)
1737+
assert.Equal(t, i.ValueMap["value3"], 3)
1738+
assert.Equal(t, i.Value1, 1)
1739+
assert.Equal(t, i.Value2, 2)
1740+
assert.Equal(t, i.Value3, 3)
1741+
})
1742+
})
1743+
1744+
t.Run("Every item used in a map must have a named key", func(t *testing.T) {
1745+
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))
1746+
1747+
type out struct {
1748+
dig.Out
1749+
1750+
Value1 int `name:"value1" group:"val"`
1751+
Value2 int `name:"value2" group:"val"`
1752+
Value3 int `group:"val"`
1753+
}
1754+
1755+
c.RequireProvide(func() out {
1756+
return out{Value1: 1, Value2: 2, Value3: 3}
1757+
})
1758+
1759+
type in struct {
1760+
dig.In
1761+
1762+
ValueMap map[string]int `group:"val"`
1763+
}
1764+
var called = false
1765+
err := c.Invoke(func(i in) { called = true })
1766+
dig.AssertErrorMatches(t, err,
1767+
`could not build arguments for function "go.uber.org/dig_test".TestGroups\S+`,
1768+
`dig_test.go:\d+`, // file:line
1769+
`every entry in a map value groups must have a name, group "val" is missing a name`)
1770+
assert.False(t, called, "shouldn't call invoked function when deps aren't available")
1771+
})
1772+
16941773
}
16951774

16961775
// --- END OF END TO END TESTS

graph.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type graphNode struct {
2828
}
2929

3030
// graphHolder is the dependency graph of the container.
31-
// It saves constructorNodes and paramGroupedSlice (value groups)
31+
// It saves constructorNodes and paramGroupedCollection (value groups)
3232
// as nodes in the graph.
3333
// It implements the graph interface defined by internal/graph.
3434
// It has 1-1 correspondence with the Scope whose graph it represents.
@@ -68,7 +68,7 @@ func (gh *graphHolder) EdgesFrom(u int) []int {
6868
for _, param := range w.paramList.Params {
6969
orders = append(orders, getParamOrder(gh, param)...)
7070
}
71-
case *paramGroupedSlice:
71+
case *paramGroupedCollection:
7272
providers := gh.s.getAllGroupProviders(w.Group, w.Type.Elem())
7373
for _, provider := range providers {
7474
orders = append(orders, provider.Order(gh.s))

param.go

+46-23
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ import (
3838
// paramSingle An explicitly requested type.
3939
// paramObject dig.In struct where each field in the struct can be another
4040
// param.
41-
// paramGroupedSlice
42-
// A slice consuming a value group. This will receive all
41+
// paramGroupedCollection
42+
// A slice or map consuming a value group. This will receive all
4343
// values produced with a `group:".."` tag with the same name
44-
// as a slice.
44+
// as a slice or map. For a map, every value produced with the
45+
// same group name MUST have a name which will form the map key.
46+
4547
type param interface {
4648
fmt.Stringer
4749

@@ -59,7 +61,7 @@ var (
5961
_ param = paramSingle{}
6062
_ param = paramObject{}
6163
_ param = paramList{}
62-
_ param = paramGroupedSlice{}
64+
_ param = paramGroupedCollection{}
6365
)
6466

6567
// newParam builds a param from the given type. If the provided type is a
@@ -342,7 +344,7 @@ func getParamOrder(gh *graphHolder, param param) []int {
342344
for _, provider := range providers {
343345
orders = append(orders, provider.Order(gh.s))
344346
}
345-
case paramGroupedSlice:
347+
case paramGroupedCollection:
346348
// value group parameters have nodes of their own.
347349
// We can directly return that here.
348350
orders = append(orders, p.orders[gh.s])
@@ -401,7 +403,7 @@ func (po paramObject) Build(c containerStore) (reflect.Value, error) {
401403
var softGroupsQueue []paramObjectField
402404
var fields []paramObjectField
403405
for _, f := range po.Fields {
404-
if p, ok := f.Param.(paramGroupedSlice); ok && p.Soft {
406+
if p, ok := f.Param.(paramGroupedCollection); ok && p.Soft {
405407
softGroupsQueue = append(softGroupsQueue, f)
406408
continue
407409
}
@@ -451,7 +453,7 @@ func newParamObjectField(idx int, f reflect.StructField, c containerStore) (para
451453

452454
case f.Tag.Get(_groupTag) != "":
453455
var err error
454-
p, err = newParamGroupedSlice(f, c)
456+
p, err = newParamGroupedCollection(f, c)
455457
if err != nil {
456458
return pof, err
457459
}
@@ -488,29 +490,31 @@ func (pof paramObjectField) Build(c containerStore) (reflect.Value, error) {
488490
return v, nil
489491
}
490492

491-
// paramGroupedSlice is a param which produces a slice of values with the same
493+
// paramGroupedCollection is a param which produces a slice or map of values with the same
492494
// group name.
493-
type paramGroupedSlice struct {
495+
type paramGroupedCollection struct {
494496
// Name of the group as specified in the `group:".."` tag.
495497
Group string
496498

497-
// Type of the slice.
499+
// Type of the map or slice.
498500
Type reflect.Type
499501

500502
// Soft is used to denote a soft dependency between this param and its
501503
// constructors, if it's true its constructors are only called if they
502504
// provide another value requested in the graph
503505
Soft bool
504506

507+
isMap bool
505508
orders map[*Scope]int
506509
}
507510

508-
func (pt paramGroupedSlice) String() string {
511+
func (pt paramGroupedCollection) String() string {
509512
// io.Reader[group="foo"] refers to a group of io.Readers called 'foo'
510513
return fmt.Sprintf("%v[group=%q]", pt.Type.Elem(), pt.Group)
514+
// JQTODO, different string for map
511515
}
512516

513-
func (pt paramGroupedSlice) DotParam() []*dot.Param {
517+
func (pt paramGroupedCollection) DotParam() []*dot.Param {
514518
return []*dot.Param{
515519
{
516520
Node: &dot.Node{
@@ -521,28 +525,31 @@ func (pt paramGroupedSlice) DotParam() []*dot.Param {
521525
}
522526
}
523527

524-
// newParamGroupedSlice builds a paramGroupedSlice from the provided type with
528+
// newParamGroupedCollection builds a paramGroupedCollection from the provided type with
525529
// the given name.
526530
//
527-
// The type MUST be a slice type.
528-
func newParamGroupedSlice(f reflect.StructField, c containerStore) (paramGroupedSlice, error) {
531+
// The type MUST be a slice or map[string]T type.
532+
func newParamGroupedCollection(f reflect.StructField, c containerStore) (paramGroupedCollection, error) {
529533
g, err := parseGroupString(f.Tag.Get(_groupTag))
530534
if err != nil {
531-
return paramGroupedSlice{}, err
535+
return paramGroupedCollection{}, err
532536
}
533-
pg := paramGroupedSlice{
537+
isMap := f.Type.Kind() == reflect.Map && f.Type.Key().Kind() == reflect.String
538+
isSlice := f.Type.Kind() == reflect.Slice
539+
pg := paramGroupedCollection{
534540
Group: g.Name,
535541
Type: f.Type,
542+
isMap: isMap,
536543
orders: make(map[*Scope]int),
537544
Soft: g.Soft,
538545
}
539546

540547
name := f.Tag.Get(_nameTag)
541548
optional, _ := isFieldOptional(f)
542549
switch {
543-
case f.Type.Kind() != reflect.Slice:
550+
case !isMap && !isSlice:
544551
return pg, newErrInvalidInput(
545-
fmt.Sprintf("value groups may be consumed as slices only: field %q (%v) is not a slice", f.Name, f.Type), nil)
552+
fmt.Sprintf("value groups may be consumed as slices or string-keyed maps only: field %q (%v) is not a slice or string-keyed map", f.Name, f.Type), nil)
546553
case g.Flatten:
547554
return pg, newErrInvalidInput(
548555
fmt.Sprintf("cannot use flatten in parameter value groups: field %q (%v) specifies flatten", f.Name, f.Type), nil)
@@ -560,7 +567,7 @@ func newParamGroupedSlice(f reflect.StructField, c containerStore) (paramGrouped
560567
// any of the parent Scopes. In the case where there are multiple scopes that
561568
// are decorating the same type, the closest scope in effect will be replacing
562569
// any decorated value groups provided in further scopes.
563-
func (pt paramGroupedSlice) getDecoratedValues(c containerStore) (reflect.Value, bool) {
570+
func (pt paramGroupedCollection) getDecoratedValues(c containerStore) (reflect.Value, bool) {
564571
for _, c := range c.storesToRoot() {
565572
if items, ok := c.getDecoratedValueGroup(pt.Group, pt.Type); ok {
566573
return items, true
@@ -575,7 +582,7 @@ func (pt paramGroupedSlice) getDecoratedValues(c containerStore) (reflect.Value,
575582
// The order in which the decorators are invoked is from the top level scope to
576583
// the current scope, to account for decorators that decorate values that were
577584
// already decorated.
578-
func (pt paramGroupedSlice) callGroupDecorators(c containerStore) error {
585+
func (pt paramGroupedCollection) callGroupDecorators(c containerStore) error {
579586
stores := c.storesToRoot()
580587
for i := len(stores) - 1; i >= 0; i-- {
581588
c := stores[i]
@@ -600,7 +607,7 @@ func (pt paramGroupedSlice) callGroupDecorators(c containerStore) error {
600607
// search the given container and its parent for matching group providers and
601608
// call them to commit values. If an error is encountered, return the number
602609
// of providers called and a non-nil error from the first provided.
603-
func (pt paramGroupedSlice) callGroupProviders(c containerStore) (int, error) {
610+
func (pt paramGroupedCollection) callGroupProviders(c containerStore) (int, error) {
604611
itemCount := 0
605612
for _, c := range c.storesToRoot() {
606613
providers := c.getGroupProviders(pt.Group, pt.Type.Elem())
@@ -618,7 +625,7 @@ func (pt paramGroupedSlice) callGroupProviders(c containerStore) (int, error) {
618625
return itemCount, nil
619626
}
620627

621-
func (pt paramGroupedSlice) Build(c containerStore) (reflect.Value, error) {
628+
func (pt paramGroupedCollection) Build(c containerStore) (reflect.Value, error) {
622629
// do not call this if we are already inside a decorator since
623630
// it will result in an infinite recursion. (i.e. decorate -> params.BuildList() -> Decorate -> params.BuildList...)
624631
// this is safe since a value can be decorated at most once in a given scope.
@@ -644,6 +651,22 @@ func (pt paramGroupedSlice) Build(c containerStore) (reflect.Value, error) {
644651
}
645652

646653
stores := c.storesToRoot()
654+
if pt.isMap {
655+
result := reflect.MakeMapWithSize(pt.Type, itemCount)
656+
for _, c := range stores {
657+
kgvs := c.getValueGroup(pt.Group, pt.Type.Elem())
658+
for _, kgv := range kgvs {
659+
if kgv.key == "" {
660+
return _noValue, newErrInvalidInput(
661+
fmt.Sprintf("every entry in a map value groups must have a name, group \"%v\" is missing a name", pt.Group),
662+
nil,
663+
)
664+
}
665+
result.SetMapIndex(reflect.ValueOf(kgv.key), kgv.value)
666+
}
667+
}
668+
return result, nil
669+
}
647670
result := reflect.MakeSlice(pt.Type, 0, itemCount)
648671
for _, c := range stores {
649672
kgvs := c.getValueGroup(pt.Group, pt.Type.Elem())

param_test.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -179,21 +179,31 @@ func TestParamObjectFailure(t *testing.T) {
179179
})
180180
}
181181

182-
func TestParamGroupSliceErrors(t *testing.T) {
182+
func TestParamGroupCollectionErrors(t *testing.T) {
183183
tests := []struct {
184184
desc string
185185
shape interface{}
186186
wantErr string
187187
}{
188188
{
189-
desc: "non-slice type are disallowed",
189+
desc: "non-slice or string-keyed map type are disallowed (slice)",
190190
shape: struct {
191191
In
192192

193193
Foo string `group:"foo"`
194194
}{},
195-
wantErr: "value groups may be consumed as slices only: " +
196-
`field "Foo" (string) is not a slice`,
195+
wantErr: "value groups may be consumed as slices or string-keyed maps only: " +
196+
`field "Foo" (string) is not a slice or string-keyed map`,
197+
},
198+
{
199+
desc: "non-slice or string-keyed map type are disallowed (string-keyed map)",
200+
shape: struct {
201+
In
202+
203+
Foo map[int]int `group:"foo"`
204+
}{},
205+
wantErr: "value groups may be consumed as slices or string-keyed maps only: " +
206+
`field "Foo" (map[int]int) is not a slice or string-keyed map`,
197207
},
198208
{
199209
desc: "cannot provide name for a group",

0 commit comments

Comments
 (0)