Repository: uber-go/fx
Pull request #1089 (2023-05-30 15:01:41)

Make fx.Populate work with fx.Annotate
This PR makes fx.Populate work with fx.Annotate. Previously, to fx.Populate a
named dependency, users would have to create a struct that embeds fx.In and has
a field with a tag and pass a pointer to that struct. This could be cumbersome
in cases where the user is intersted in only one named value. With this PR,
users will be able to use fx.Annotate to get the same effect:

```
var a A
...
fx.Populate(
  fx.Annotate(
    &a,
    fx.ParamTags(`name:"A"`),
  ),
)
```



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index adc6371..c9af1b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - No changes yet.
 
 ## [1.19.3](https://github.com/uber-go/fx/compare/v1.19.2...v1.19.3) - 2023-04-17
+
 ### Changed
 - Fixed several typos in docs.
 - WASM build support.
diff --git a/populate.go b/populate.go
index 4195e80..09148b2 100644
--- a/populate.go
+++ b/populate.go
@@ -30,22 +30,69 @@ import (
 // values that must be populated. Pointers to structs that embed In are
 // supported, which can be used to populate multiple values in a struct.
 //
+// Annotating each pointer with ParamTags is also supported as a shorthand
+// to passing a pointer to a struct that embeds In with field tags. For example:
+//
+//	 var a A
+//	 var b B
+//	 fx.Populate(
+//		fx.Annotate(
+//				&a,
+//				fx.ParamTags(`name:"A"`)
+//	 	),
+//		fx.Annotate(
+//				&b,
+//				fx.ParamTags(`name:"B"`)
+//	 	)
+//	 )
+//
+// Code above is equivalent to the following:
+//
+//	type Target struct {
+//		fx.In
+//
+//		a A `name:"A"`
+//		b B `name:"B"`
+//	}
+//	var target Target
+//	...
+//	fx.Populate(&target)
+//
 // This is most helpful in unit tests: it lets tests leverage Fx's automatic
 // constructor wiring to build a few structs, but then extract those structs
 // for further testing.
 func Populate(targets ...interface{}) Option {
 	// Validate all targets are non-nil pointers.
-	targetTypes := make([]reflect.Type, len(targets))
+	fields := make([]reflect.StructField, len(targets)+1)
+	fields[0] = reflect.StructField{
+		Name:      "In",
+		Type:      reflect.TypeOf(In{}),
+		Anonymous: true,
+	}
 	for i, t := range targets {
 		if t == nil {
 			return Error(fmt.Errorf("failed to Populate: target %v is nil", i+1))
 		}
-		rt := reflect.TypeOf(t)
+		var (
+			rt  reflect.Type
+			tag reflect.StructTag
+		)
+		switch t := t.(type) {
+		case annotated:
+			rt = reflect.TypeOf(t.Target)
+			tag = reflect.StructTag(t.ParamTags[0])
+			targets[i] = t.Target
+		default:
+			rt = reflect.TypeOf(t)
+		}
 		if rt.Kind() != reflect.Ptr {
 			return Error(fmt.Errorf("failed to Populate: target %v is not a pointer type, got %T", i+1, t))
 		}
-
-		targetTypes[i] = reflect.TypeOf(t).Elem()
+		fields[i+1] = reflect.StructField{
+			Name: fmt.Sprintf("Field%d", i),
+			Type: rt.Elem(),
+			Tag:  tag,
+		}
 	}
 
 	// Build a function that looks like:
@@ -56,10 +103,11 @@ func Populate(targets ...interface{}) Option {
 	//   [...]
 	// }
 	//
-	fnType := reflect.FuncOf(targetTypes, nil, false /* variadic */)
+	fnType := reflect.FuncOf([]reflect.Type{reflect.StructOf(fields)}, nil, false /* variadic */)
 	fn := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value {
-		for i, arg := range args {
-			reflect.ValueOf(targets[i]).Elem().Set(arg)
+		arg := args[0]
+		for i, target := range targets {
+			reflect.ValueOf(target).Elem().Set(arg.Field(i + 1))
 		}
 		return nil
 	})


Test patch:
diff --git a/populate_test.go b/populate_test.go
index 05a5e98..14e88b7 100644
--- a/populate_test.go
+++ b/populate_test.go
@@ -27,7 +27,6 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-
 	. "go.uber.org/fx"
 	"go.uber.org/fx/fxtest"
 )
@@ -158,6 +157,73 @@ func TestPopulate(t *testing.T) {
 		assert.False(t, targets.V1 == targets.V2, "fields should be different")
 	})
 
+	t.Run("annotated populate", func(t *testing.T) {
+		t.Parallel()
+
+		type result struct {
+			Out
+
+			V1 *t1 `name:"n1"`
+			V2 *t1 `name:"n2"`
+		}
+		var v1, v2 *t1
+		app := fxtest.New(t,
+			Provide(func() result {
+				return result{
+					V1: &t1{},
+					V2: &t1{},
+				}
+			}),
+			Populate(
+				Annotate(
+					&v1,
+					ParamTags(`name:"n1"`),
+				),
+				Annotate(
+					&v2,
+					ParamTags(`name:"n2"`),
+				),
+			),
+		)
+		app.RequireStart().RequireStop()
+
+		require.NotNil(t, v1, "did not populate argument 1")
+		require.NotNil(t, v2, "did not populate argument 2")
+		// Cannot use assert.Equal here as we want to compare pointers.
+		assert.False(t, v1 == v2, "values should be different")
+	})
+
+	t.Run("annotated populate with annotated provide", func(t *testing.T) {
+		t.Parallel()
+		var v1, v2 *t1
+		app := fxtest.New(t,
+			Provide(
+				Annotate(
+					func() (*t1, *t1) {
+						return &t1{}, &t1{}
+					},
+					ResultTags(`name:"n1"`, `name:"n2"`),
+				),
+			),
+			Populate(
+				Annotate(
+					&v1,
+					ParamTags(`name:"n1"`),
+				),
+				Annotate(
+					&v2,
+					ParamTags(`name:"n2"`),
+				),
+			),
+		)
+		app.RequireStart().RequireStop()
+
+		require.NotNil(t, v1, "did not populate argument 1")
+		require.NotNil(t, v2, "did not populate argument 2")
+		// Cannot use assert.Equal here as we want to compare pointers.
+		assert.False(t, v1 == v2, "values should be different")
+	})
+
 	t.Run("populate group", func(t *testing.T) {
 		t.Parallel()