Repository: uber-go/dig
Pull request #375 (2023-01-25 17:38:31)

Allow using dig.As with dig.Group
This PR adds the ability to use dig.As with dig.Group.
When dig.As is used with dig.Group, the value produced
by the constructor will be provided to the specified group
as the type specified by dig.As.

```
 c.Provide(newBuffer, dig.As(new(io.Reader)), dig.Group("readers"))
```

For example, the above code is equivalent to the following.

```
 c.Provide(func(...) io.Reader {
   b := newBuffer(...)
   return b
 }, dig.Group("readers"))
```

Fixes #341 

codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/dig/pull/375?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
> Merging [#375](https://codecov.io/gh/uber-go/dig/pull/375?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (9b5e6dc) into [master](https://codecov.io/gh/uber-go/dig/commit/5ae7b1f2652f264e42174773de26dbeea0cc8199?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (5ae7b1f) will **decrease** coverage by `0.53%`.
> The diff coverage is `68.00%`.

```diff
@@            Coverage Diff             @@
##           master     #375      +/-   ##
==========================================
- Coverage   98.33%   97.80%   -0.53%     
==========================================
  Files          21       21              
  Lines        1441     1458      +17     
==========================================
+ Hits         1417     1426       +9     
- Misses         15       21       +6     
- Partials        9       11       +2     
```


| [Impacted Files](https://codecov.io/gh/uber-go/dig/pull/375?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [result.go](https://codecov.io/gh/uber-go/dig/pull/375?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-cmVzdWx0Lmdv) | `95.09% <65.21%> (-3.83%)` | :arrow_down: |
| [provide.go](https://codecov.io/gh/uber-go/dig/pull/375?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-cHJvdmlkZS5nbw==) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)

sywhang: looks like our GH account has some billing issues, OSPO is already working on fixing that. 

Patch:
diff --git a/provide.go b/provide.go
index 905c7e9..91a4a92 100644
--- a/provide.go
+++ b/provide.go
@@ -51,10 +51,6 @@ func (o *provideOptions) Validate() error {
 			return newErrInvalidInput(
 				fmt.Sprintf("cannot use named values with value groups: name:%q provided with group:%q", o.Name, o.Group), nil)
 		}
-		if len(o.As) > 0 {
-			return newErrInvalidInput(
-				fmt.Sprintf("cannot use dig.As with value groups: dig.As provided with group:%q", o.Group), nil)
-		}
 	}
 
 	// Names must be representable inside a backquoted string. The only
@@ -639,6 +635,10 @@ func (cv connectionVisitor) Visit(res result) resultVisitor {
 		// value there.
 		k := key{group: r.Group, t: r.Type}
 		cv.keyPaths[k] = path
+		for _, asType := range r.As {
+			k := key{group: r.Group, t: asType}
+			cv.keyPaths[k] = path
+		}
 	}
 
 	return cv
diff --git a/result.go b/result.go
index 2e07d8c..369cd21 100644
--- a/result.go
+++ b/result.go
@@ -88,6 +88,24 @@ func newResult(t reflect.Type, opts resultOptions) (result, error) {
 				fmt.Sprintf("cannot parse group %q", opts.Group), err)
 		}
 		rg := resultGrouped{Type: t, Group: g.Name, Flatten: g.Flatten}
+		if len(opts.As) > 0 {
+			var asTypes []reflect.Type
+			for _, as := range opts.As {
+				ifaceType := reflect.TypeOf(as).Elem()
+				if ifaceType == t {
+					continue
+				}
+				if !t.Implements(ifaceType) {
+					return nil, newErrInvalidInput(
+						fmt.Sprintf("invalid dig.As: %v does not implement %v", t, ifaceType), nil)
+				}
+				asTypes = append(asTypes, ifaceType)
+			}
+			if len(asTypes) > 0 {
+				rg.Type = asTypes[0]
+				rg.As = asTypes[1:]
+			}
+		}
 		if g.Soft {
 			return nil, newErrInvalidInput(fmt.Sprintf(
 				"cannot use soft with result value groups: soft was used with group:%q", g.Name), nil)
@@ -441,17 +459,27 @@ type resultGrouped struct {
 	// as a group. Requires the value's slice to be a group. If set, Type will be
 	// the type of individual elements rather than the group.
 	Flatten bool
+
+	// If specified, this is a list of types which the value will be made
+	// available as, in addition to its own type.
+	As []reflect.Type
 }
 
 func (rt resultGrouped) DotResult() []*dot.Result {
-	return []*dot.Result{
-		{
-			Node: &dot.Node{
-				Type:  rt.Type,
-				Group: rt.Group,
-			},
+	dotResults := make([]*dot.Result, 0, len(rt.As)+1)
+	dotResults = append(dotResults, &dot.Result{
+		Node: &dot.Node{
+			Type:  rt.Type,
+			Group: rt.Group,
 		},
+	})
+
+	for _, asType := range rt.As {
+		dotResults = append(dotResults, &dot.Result{
+			Node: &dot.Node{Type: asType, Group: rt.Group},
+		})
 	}
+	return dotResults
 }
 
 // newResultGrouped(f) builds a new resultGrouped from the provided field.
@@ -491,6 +519,9 @@ func (rt resultGrouped) Extract(cw containerWriter, decorated bool, v reflect.Va
 	// Decorated values are always flattened.
 	if !decorated && !rt.Flatten {
 		cw.submitGroupedValue(rt.Group, rt.Type, v)
+		for _, asType := range rt.As {
+			cw.submitGroupedValue(rt.Group, asType, v)
+		}
 		return
 	}
 


Test patch:
diff --git a/decorate_test.go b/decorate_test.go
index 26bd86c..721ba80 100644
--- a/decorate_test.go
+++ b/decorate_test.go
@@ -32,6 +32,27 @@ import (
 	"go.uber.org/dig/internal/digtest"
 )
 
+type myInt interface {
+	String() string
+	Increment()
+}
+type someInt int
+
+var _ myInt = (*someInt)(nil)
+
+func newSomeInt(i int) *someInt {
+	v := someInt(i)
+	return &v
+}
+
+func (i *someInt) String() string {
+	return fmt.Sprintf("%d", i)
+}
+
+func (i *someInt) Increment() {
+	*i += 1
+}
+
 func TestDecorateSuccess(t *testing.T) {
 	t.Run("simple decorate without names or groups", func(t *testing.T) {
 		t.Parallel()
@@ -124,6 +145,36 @@ func TestDecorateSuccess(t *testing.T) {
 		})
 	})
 
+	t.Run("decorate grouped values provided as", func(t *testing.T) {
+		t.Parallel()
+
+		type A struct {
+			dig.In
+			Values []myInt `group:"values"`
+		}
+
+		type B struct {
+			dig.Out
+			Values []myInt `group:"values"`
+		}
+
+		c := digtest.New(t)
+
+		for i := range make([]int, 3) {
+			i := i
+			c.RequireProvide(func() *someInt { return newSomeInt(i) }, dig.Group("values"), dig.As(new(myInt)))
+		}
+		c.Decorate(func(in A) B {
+			for _, v := range in.Values {
+				v.Increment()
+			}
+			return B{Values: in.Values}
+		})
+		c.RequireInvoke(func(a A) {
+			assert.ElementsMatch(t, []myInt{newSomeInt(1), newSomeInt(2), newSomeInt(3)}, a.Values)
+		})
+	})
+
 	t.Run("value groups with multiple decorations", func(t *testing.T) {
 		type params struct {
 			dig.In
diff --git a/dig_test.go b/dig_test.go
index 5841c9e..69b10f9 100644
--- a/dig_test.go
+++ b/dig_test.go
@@ -687,17 +687,89 @@ func TestEndToEndSuccess(t *testing.T) {
 		}))
 	})
 
+	t.Run("As with Group", func(t *testing.T) {
+		c := digtest.New(t)
+		expectedStrs := []string{"foo", "bar"}
+		for _, s := range expectedStrs {
+			s := s
+			c.RequireProvide(func() *bytes.Buffer {
+				return bytes.NewBufferString(s)
+			}, dig.Group("readers"), dig.As(new(io.Reader)))
+		}
+
+		type in struct {
+			dig.In
+			Readers []io.Reader `group:"readers"`
+		}
+
+		var actualStrs []string
+
+		c.RequireInvoke(func(got in) {
+			require.Len(t, got.Readers, 2)
+			for _, r := range got.Readers {
+				buf := make([]byte, 3)
+				_, err := r.Read(buf)
+				require.NoError(t, err)
+				actualStrs = append(actualStrs, string(buf))
+			}
+		})
+
+		assert.ElementsMatch(t, actualStrs, expectedStrs, "list of strings provided must match")
+	})
+
+	t.Run("multiple As with Group", func(t *testing.T) {
+		c := digtest.New(t)
+		expectedStrs := []string{"foo", "bar"}
+		for _, s := range expectedStrs {
+			s := s
+			c.RequireProvide(func() *bytes.Buffer {
+				return bytes.NewBufferString(s)
+			}, dig.Group("buffs"), dig.As(new(io.Reader), new(io.Writer)))
+		}
+
+		type in struct {
+			dig.In
+			Readers []io.Reader `group:"buffs"`
+			Writers []io.Writer `group:"buffs"`
+		}
+
+		var actualStrs []string
+
+		c.RequireInvoke(func(got in) {
+			require.Len(t, got.Readers, 2)
+			for _, r := range got.Readers {
+				buf := make([]byte, 3)
+				_, err := r.Read(buf)
+				require.NoError(t, err)
+				actualStrs = append(actualStrs, string(buf))
+			}
+			require.Len(t, got.Writers, 2)
+		})
+
+		assert.ElementsMatch(t, actualStrs, expectedStrs, "list of strings provided must match")
+	})
+
 	t.Run("As same interface", func(t *testing.T) {
 		c := digtest.New(t)
 		c.RequireProvide(func() io.Reader {
-			panic("this function should not be called")
+			t.Fatal("this function should not be called")
+			return nil
 		}, dig.As(new(io.Reader)))
 	})
 
+	t.Run("As same interface with Group", func(t *testing.T) {
+		c := digtest.New(t)
+		c.RequireProvide(func() io.Reader {
+			t.Fatal("this function should not be called")
+			return nil
+		}, dig.As(new(io.Reader)), dig.Group("readers"))
+	})
+
 	t.Run("As different interface", func(t *testing.T) {
 		c := digtest.New(t)
 		c.RequireProvide(func() io.ReadCloser {
-			panic("this function should not be called")
+			t.Fatal("this function should not be called")
+			return nil
 		}, dig.As(new(io.Reader), new(io.Closer)))
 	})
 
@@ -919,7 +991,8 @@ func TestEndToEndSuccess(t *testing.T) {
 		})
 
 		c.RequireProvide(func() []*A {
-			panic("[]*A constructor must not be called.")
+			t.Fatal("[]*A constructor must not be called.")
+			return nil
 		})
 
 		c.RequireInvoke(func(a *A, as ...*A) {
@@ -942,7 +1015,8 @@ func TestEndToEndSuccess(t *testing.T) {
 		})
 
 		c.RequireProvide(func() []*A {
-			panic("[]*A constructor must not be called.")
+			t.Fatal("[]*A constructor must not be called.")
+			return nil
 		})
 
 		var gaveB *B
@@ -1494,7 +1568,6 @@ func TestGroups(t *testing.T) {
 // --- END OF END TO END TESTS
 
 func TestRecoverFromPanic(t *testing.T) {
-
 	tests := []struct {
 		name    string
 		setup   func(*digtest.Container)
@@ -1542,7 +1615,6 @@ func TestRecoverFromPanic(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-
 			t.Run("without option", func(t *testing.T) {
 				c := digtest.New(t)
 				tt.setup(c)
@@ -1781,7 +1853,8 @@ func TestProvideInvalidName(t *testing.T) {
 
 	c := digtest.New(t)
 	err := c.Provide(func() io.Reader {
-		panic("this function must not be called")
+		t.Fatal("this function must not be called")
+		return nil
 	}, dig.Name("foo`bar"))
 	require.Error(t, err, "Provide must fail")
 	assert.Contains(t, err.Error(), "invalid dig.Name(\"foo`bar\"): names cannot contain backquotes")
@@ -1792,13 +1865,15 @@ func TestProvideInvalidGroup(t *testing.T) {
 
 	c := digtest.New(t)
 	err := c.Provide(func() io.Reader {
-		panic("this function must not be called")
+		t.Fatal("this function must not be called")
+		return nil
 	}, dig.Group("foo`bar"))
 	require.Error(t, err, "Provide must fail")
 	assert.Contains(t, err.Error(), "invalid dig.Group(\"foo`bar\"): group names cannot contain backquotes")
 
 	err = c.Provide(func() io.Reader {
-		panic("this function must not be called")
+		t.Fatal("this function must not be called")
+		return nil
 	}, dig.Group("foo,bar"))
 	require.Error(t, err, "Provide must fail")
 	assert.Contains(t, err.Error(), `cannot parse group "foo,bar": invalid option "bar"`)
@@ -1821,6 +1896,7 @@ func TestProvideInvalidAs(t *testing.T) {
 		name        string
 		param       interface{}
 		expectedErr string
+		addlOption  dig.ProvideOption
 	}{
 		{
 			name:        "as param is not an type interface",
@@ -1852,6 +1928,17 @@ func TestProvideInvalidAs(t *testing.T) {
 			param:       func() *out { return &out{name: "example"} },
 			expectedErr: "invalid dig.As(func() *dig_test.out): argument must be a pointer to an interface",
 		},
+		{
+			name:        "as param is not implemented by provided type",
+			param:       new(io.ReadCloser),
+			expectedErr: "invalid dig.As: *bytes.Buffer does not implement io.ReadCloser",
+		},
+		{
+			name:        "as param is not implemented by provided type",
+			param:       new(io.ReadCloser),
+			expectedErr: "invalid dig.As: *bytes.Buffer does not implement io.ReadCloser",
+			addlOption:  dig.Group("readclosers"),
+		},
 	}
 	for _, tt := range tests {
 		tt := tt
@@ -1859,13 +1946,25 @@ func TestProvideInvalidAs(t *testing.T) {
 			t.Parallel()
 
 			c := digtest.New(t)
-			err := c.Provide(
-				func() *bytes.Buffer {
-					var buf bytes.Buffer
-					return &buf
-				},
-				dig.As(tt.param),
-			)
+			var err error
+			if tt.addlOption == nil {
+				err = c.Provide(
+					func() *bytes.Buffer {
+						var buf bytes.Buffer
+						return &buf
+					},
+					dig.As(tt.param),
+				)
+			} else {
+				err = c.Provide(
+					func() *bytes.Buffer {
+						var buf bytes.Buffer
+						return &buf
+					},
+					dig.As(tt.param),
+					tt.addlOption,
+				)
+			}
 
 			require.Error(t, err, "provide must fail")
 			assert.Contains(t, err.Error(), tt.expectedErr)
@@ -1905,22 +2004,13 @@ func TestProvideIncompatibleOptions(t *testing.T) {
 	t.Run("group and name", func(t *testing.T) {
 		c := digtest.New(t)
 		err := c.Provide(func() io.Reader {
-			panic("this function must not be called")
+			t.Fatal("this function must not be called")
+			return nil
 		}, dig.Group("foo"), dig.Name("bar"))
 		require.Error(t, err)
 		assert.Contains(t, err.Error(), "cannot use named values with value groups: "+
 			`name:"bar" provided with group:"foo"`)
 	})
-
-	t.Run("group and As", func(t *testing.T) {
-		c := digtest.New(t)
-		err := c.Provide(func() *bytes.Buffer {
-			panic("this function must not be called")
-		}, dig.Group("foo"), dig.As(new(io.Reader)))
-		require.Error(t, err)
-		assert.Contains(t, err.Error(), "cannot use dig.As with value groups: "+
-			`dig.As provided with group:"foo"`)
-	})
 }
 
 type testStruct struct{}
@@ -2688,7 +2778,8 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 		c := digtest.New(t, dig.DryRun(dryRun))
 
 		c.RequireProvide(func(p param) *type3 {
-			panic("function must not be called")
+			t.Fatal("function must not be called")
+			return nil
 		})
 
 		err := c.Invoke(func(*type3) {
@@ -2716,11 +2807,13 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 		c := digtest.New(t, dig.DryRun(dryRun))
 
 		c.RequireProvide(func() type2 {
-			panic("function must not be called")
+			t.Fatal("function must not be called")
+			return type2{}
 		})
 
 		c.RequireProvide(func(type1, *type2) type3 {
-			panic("function must not be called")
+			t.Fatal("function must not be called")
+			return type3{}
 		})
 
 		err := c.Invoke(func(type3) {
@@ -2776,7 +2869,8 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 
 		c := digtest.New(t, dig.DryRun(dryRun))
 		err := c.Provide(func(a args) *type1 {
-			panic("function must not be called")
+			t.Fatal("function must not be called")
+			return nil
 		})
 
 		require.Error(t, err, "expected provide error")
@@ -2805,7 +2899,8 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 		// Container has a constructor for *dep, but that constructor has unmet
 		// dependencies.
 		c.RequireProvide(func(missing) *dep {
-			panic("constructor for *dep should not be called")
+			t.Fatal("constructor for *dep should not be called")
+			return nil
 		})
 
 		// Should still be able to invoke a function that takes params, since *dep
@@ -2836,13 +2931,14 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 		})
 
 		c.RequireProvide(func(*failed) *dep {
-			panic("constructor for *dep should not be called")
+			t.Fatal("constructor for *dep should not be called")
+			return nil
 		})
 
 		// Should still be able to invoke a function that takes params, since *dep
 		// is optional.
 		err := c.Invoke(func(p params) {
-			panic("shouldn't execute invoked function")
+			t.Fatal("shouldn't execute invoked function")
 		})
 		require.Error(t, err, "expected invoke error")
 		dig.AssertErrorMatches(t, err,
@@ -3200,7 +3296,7 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 			return A{}, errors.New("great sadness")
 		})
 
-		err := c.Invoke(func(A) { panic("impossible") })
+		err := c.Invoke(func(A) { t.Fatal("invoke function should not be called") })
 
 		require.Error(t, err, "expected Invoke error")
 		dig.AssertErrorMatches(t, err,
@@ -3225,7 +3321,7 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 			return B{}, nil
 		})
 
-		err := c.Invoke(func(B) { panic("impossible") })
+		err := c.Invoke(func(B) { t.Fatal("invoke function should not be called") })
 
 		require.Error(t, err, "expected Invoke error")
 		dig.AssertErrorMatches(t, err,
@@ -3255,7 +3351,7 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 			A A
 		}
 
-		err := c.Invoke(func(params) { panic("impossible") })
+		err := c.Invoke(func(params) { t.Fatal("invoke function should not be called") })
 
 		require.Error(t, err, "expected Invoke error")
 		dig.AssertErrorMatches(t, err,
@@ -3288,7 +3384,7 @@ func testInvokeFailures(t *testing.T, dryRun bool) {
 			return B{}, nil
 		})
 
-		err := c.Invoke(func(B) { panic("impossible") })
+		err := c.Invoke(func(B) { t.Fatal("invoke function should not be called") })
 
 		require.Error(t, err, "expected Invoke error")
 		dig.AssertErrorMatches(t, err,
diff --git a/visualize_test.go b/visualize_test.go
index de5b8a8..07d8e5a 100644
--- a/visualize_test.go
+++ b/visualize_test.go
@@ -70,6 +70,7 @@ func TestDotGraph(t *testing.T) {
 	type4 := reflect.TypeOf(t4{})
 	type5 := reflect.TypeOf(t5{})
 	type6 := reflect.Indirect(reflect.ValueOf(new(io.Reader))).Type()
+	type7 := reflect.Indirect(reflect.ValueOf(new(io.Writer))).Type()
 
 	p1 := tparam(type1, "", "", false)
 	p2 := tparam(type2, "", "", false)
@@ -302,6 +303,30 @@ func TestDotGraph(t *testing.T) {
 		assertCtorsEqual(t, expected, dg.Ctors)
 	})
 
+	t.Run("value groups as", func(t *testing.T) {
+		c := digtest.New(t)
+		c.Provide(
+			func() *bytes.Buffer { return bytes.NewBufferString("foo") },
+			dig.As(new(io.Reader), new(io.Writer)),
+			dig.Group("buffs"),
+		)
+		c.Provide(
+			func() *bytes.Buffer { return bytes.NewBufferString("bar") },
+			dig.As(new(io.Reader), new(io.Writer)),
+			dig.Group("buffs"),
+		)
+		expected := []*dot.Ctor{
+			{
+				Results: []*dot.Result{tresult(type6, "", "buffs", 0), tresult(type7, "", "buffs", 0)},
+			},
+			{
+				Results: []*dot.Result{tresult(type6, "", "buffs", 1), tresult(type7, "", "buffs", 1)},
+			},
+		}
+		dg := c.CreateGraph()
+		assertCtorsEqual(t, expected, dg.Ctors)
+	})
+
 	t.Run("named values", func(t *testing.T) {
 		type in struct {
 			dig.In
@@ -518,7 +543,6 @@ func TestVisualize(t *testing.T) {
 		dig.VerifyVisualization(t, "error", c.Container, dig.VisualizeError(err))
 
 		t.Run("non-failing graph nodes are pruned", func(t *testing.T) {
-
 			t.Run("prune non-failing constructor result", func(t *testing.T) {
 				c := digtest.New(t)
 				c.Provide(func(in1) out1 { return out1{} })