Skip to content

Commit

Permalink
Support map value groups
Browse files Browse the repository at this point in the history
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}.
  • Loading branch information
jquirke committed Mar 19, 2023
1 parent e781757 commit 23338b1
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 32 deletions.
4 changes: 3 additions & 1 deletion decorate.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ func findResultKeys(r resultList) ([]key, error) {
case resultSingle:
keys = append(keys, key{t: innerResult.Type, name: innerResult.Name})
case resultGrouped:
if innerResult.Type.Kind() != reflect.Slice {
isMap := innerResult.Type.Kind() == reflect.Map && innerResult.Type.Key().Kind() == reflect.String
isSlice := innerResult.Type.Kind() == reflect.Slice
if !isMap && !isSlice {
return nil, newErrInvalidInput("decorating a value group requires decorating the entire value group, not a single value", nil)
}
keys = append(keys, key{t: innerResult.Type.Elem(), group: innerResult.Group})
Expand Down
99 changes: 99 additions & 0 deletions decorate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,64 @@ func TestDecorateSuccess(t *testing.T) {
}))
})

t.Run("map is treated as an ordinary dependency without group tag, named or unnamed, and passes through multiple scopes", func(t *testing.T) {
type params struct {
dig.In

Strings1 map[string]string
Strings2 map[string]string `name:"strings2"`
}

type childResult struct {
dig.Out

Strings1 map[string]string
Strings2 map[string]string `name:"strings2"`
}

type A map[string]string
type B map[string]string

parent := digtest.New(t)
parent.RequireProvide(func() map[string]string { return map[string]string{"key1": "val1", "key2": "val2"} })
parent.RequireProvide(func() map[string]string { return map[string]string{"key1": "val21", "key2": "val22"} }, dig.Name("strings2"))

parent.RequireProvide(func(p params) A { return A(p.Strings1) })
parent.RequireProvide(func(p params) B { return B(p.Strings2) })

child := parent.Scope("child")

parent.RequireDecorate(func(p params) childResult {
res := childResult{Strings1: make(map[string]string, len(p.Strings1))}
for k, s := range p.Strings1 {
res.Strings1[k] = strings.ToUpper(s)
}
res.Strings2 = p.Strings2
return res
})

child.RequireDecorate(func(p params) childResult {
res := childResult{Strings2: make(map[string]string, len(p.Strings2))}
for k, s := range p.Strings2 {
res.Strings2[k] = strings.ToUpper(s)
}
res.Strings1 = p.Strings1
res.Strings1["key3"] = "newval"
return res
})

require.NoError(t, child.Invoke(func(p params) {
require.Len(t, p.Strings1, 3)
assert.Equal(t, "VAL1", p.Strings1["key1"])
assert.Equal(t, "VAL2", p.Strings1["key2"])
assert.Equal(t, "newval", p.Strings1["key3"])
require.Len(t, p.Strings2, 2)
assert.Equal(t, "VAL21", p.Strings2["key1"])
assert.Equal(t, "VAL22", p.Strings2["key2"])

}))

})
t.Run("decorate values in soft group", func(t *testing.T) {
type params struct {
dig.In
Expand Down Expand Up @@ -393,6 +451,46 @@ func TestDecorateSuccess(t *testing.T) {
assert.Equal(t, `[]string[group = "animals"]`, info.Inputs[0].String())
})

t.Run("decorate with map value groups", func(t *testing.T) {
type Params struct {
dig.In

Animals map[string]string `group:"animals"`
}

type Result struct {
dig.Out

Animals map[string]string `group:"animals"`
}

c := digtest.New(t)
c.RequireProvide(func() string { return "dog" }, dig.Name("animal1"), dig.Group("animals"))
c.RequireProvide(func() string { return "cat" }, dig.Name("animal2"), dig.Group("animals"))
c.RequireProvide(func() string { return "gopher" }, dig.Name("animal3"), dig.Group("animals"))

var info dig.DecorateInfo
c.RequireDecorate(func(p Params) Result {
animals := p.Animals
for k, v := range animals {
animals[k] = "good " + v
}
return Result{
Animals: animals,
}
}, dig.FillDecorateInfo(&info))

c.RequireInvoke(func(p Params) {
assert.Len(t, p.Animals, 3)
assert.Equal(t, "good dog", p.Animals["animal1"])
assert.Equal(t, "good cat", p.Animals["animal2"])
assert.Equal(t, "good gopher", p.Animals["animal3"])
})

require.Equal(t, 1, len(info.Inputs))
assert.Equal(t, `map[string]string[group = "animals"]`, info.Inputs[0].String())
})

t.Run("decorate with optional parameter", func(t *testing.T) {
c := digtest.New(t)

Expand Down Expand Up @@ -918,6 +1016,7 @@ func TestMultipleDecorates(t *testing.T) {
assert.ElementsMatch(t, []int{2, 3, 4}, a.Values)
})
})

}

func TestFillDecorateInfoString(t *testing.T) {
Expand Down
155 changes: 154 additions & 1 deletion dig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,27 @@ func TestGroups(t *testing.T) {
})
})

t.Run("provide multiple with the same name and group but different type", func(t *testing.T) {
c := digtest.New(t)
type A struct{}
type B struct{}
type ret1 struct {
dig.Out
*A `name:"foo" group:"foos"`
}
type ret2 struct {
dig.Out
*B `name:"foo" group:"foos"`
}
c.RequireProvide(func() ret1 {
return ret1{A: &A{}}
})

c.RequireProvide(func() ret2 {
return ret2{B: &B{}}
})
})

t.Run("different types may be grouped", func(t *testing.T) {
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))

Expand Down Expand Up @@ -1745,6 +1766,118 @@ func TestGroups(t *testing.T) {
assert.ElementsMatch(t, []string{"a"}, param.Value)
})
})
/* map tests */
t.Run("empty map received without provides", func(t *testing.T) {
c := digtest.New(t)

type in struct {
dig.In

Values map[string]int `group:"foo"`
}

c.RequireInvoke(func(i in) {
require.Empty(t, i.Values)
})
})

t.Run("map value group using dig.Name and dig.Group", func(t *testing.T) {
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))

c.RequireProvide(func() int {
return 1
}, dig.Name("value1"), dig.Group("val"))
c.RequireProvide(func() int {
return 2
}, dig.Name("value2"), dig.Group("val"))
c.RequireProvide(func() int {
return 3
}, dig.Name("value3"), dig.Group("val"))

type in struct {
dig.In

Value1 int `name:"value1"`
Value2 int `name:"value2"`
Value3 int `name:"value3"`
Values []int `group:"val"`
ValueMap map[string]int `group:"val"`
}

c.RequireInvoke(func(i in) {
assert.Equal(t, []int{2, 3, 1}, i.Values)
assert.Equal(t, i.ValueMap["value1"], 1)
assert.Equal(t, i.ValueMap["value2"], 2)
assert.Equal(t, i.ValueMap["value3"], 3)
assert.Equal(t, i.Value1, 1)
assert.Equal(t, i.Value2, 2)
assert.Equal(t, i.Value3, 3)
})
})
t.Run("values are provided, map and name and slice", func(t *testing.T) {
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))
type out struct {
dig.Out

Value1 int `name:"value1" group:"val"`
Value2 int `name:"value2" group:"val"`
Value3 int `name:"value3" group:"val"`
}

c.RequireProvide(func() out {
return out{Value1: 1, Value2: 2, Value3: 3}
})

type in struct {
dig.In

Value1 int `name:"value1"`
Value2 int `name:"value2"`
Value3 int `name:"value3"`
Values []int `group:"val"`
ValueMap map[string]int `group:"val"`
}

c.RequireInvoke(func(i in) {
assert.Equal(t, []int{2, 3, 1}, i.Values)
assert.Equal(t, i.ValueMap["value1"], 1)
assert.Equal(t, i.ValueMap["value2"], 2)
assert.Equal(t, i.ValueMap["value3"], 3)
assert.Equal(t, i.Value1, 1)
assert.Equal(t, i.Value2, 2)
assert.Equal(t, i.Value3, 3)
})
})

t.Run("Every item used in a map must have a named key", func(t *testing.T) {
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))

type out struct {
dig.Out

Value1 int `name:"value1" group:"val"`
Value2 int `name:"value2" group:"val"`
Value3 int `group:"val"`
}

c.RequireProvide(func() out {
return out{Value1: 1, Value2: 2, Value3: 3}
})

type in struct {
dig.In

ValueMap map[string]int `group:"val"`
}
var called = false
err := c.Invoke(func(i in) { called = true })
dig.AssertErrorMatches(t, err,
`could not build arguments for function "go.uber.org/dig_test".TestGroups\S+`,
`dig_test.go:\d+`, // file:line
`every entry in a map value groups must have a name, group "val" is missing a name`)
assert.False(t, called, "shouldn't call invoked function when deps aren't available")
})

}

// --- END OF END TO END TESTS
Expand Down Expand Up @@ -2753,7 +2886,27 @@ func testProvideFailures(t *testing.T, dryRun bool) {
)
})

t.Run("provide multiple instances with the same name but different group", func(t *testing.T) {
t.Run("provide multiple instances with the same name and same group using options", func(t *testing.T) {
c := digtest.New(t, dig.DryRun(dryRun))
type A struct{}

c.RequireProvide(func() *A {
return &A{}
}, dig.Group("foos"), dig.Name("foo"))

err := c.Provide(func() *A {
return &A{}
}, dig.Group("foos"), dig.Name("foo"))
require.Error(t, err, "expected error on the second provide")
dig.AssertErrorMatches(t, err,
`cannot provide function "go.uber.org/dig_test".testProvideFailures\S+`,
`dig_test.go:\d+`, // file:line
`cannot provide \*dig_test.A\[name="foo"\] from \[1\]:`,
`already provided by "go.uber.org/dig_test".testProvideFailures\S+`,
)
})

t.Run("provide multiple instances with the same name and type but different group", func(t *testing.T) {
c := digtest.New(t, dig.DryRun(dryRun))
type A struct{}
type ret1 struct {
Expand Down
4 changes: 2 additions & 2 deletions graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type graphNode struct {
}

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

0 comments on commit 23338b1

Please sign in to comment.