Repository: uber-go/fx
Pull request #990 (2022-11-16 07:31:34)

Add fx.From annotation
open a new PR for https://github.com/uber-go/fx/pull/938,
because long time no progress update. 😭

kkHAIKE: @sywhang sure
kkHAIKE: ping @sywhang 😊
codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/fx/pull/990?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
> Merging [#990](https://codecov.io/gh/uber-go/fx/pull/990?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (de5acec) into [master](https://codecov.io/gh/uber-go/fx/commit/dfa20606070cc53dc05a0d0fbded1ca02e0d2c33?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (dfa2060) will **increase** coverage by `0.03%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master     #990      +/-   ##
==========================================
+ Coverage   98.67%   98.70%   +0.03%     
==========================================
  Files          39       33       -6     
  Lines        1806     1780      -26     
==========================================
- Hits         1782     1757      -25     
+ Misses         17       16       -1     
  Partials        7        7              
```


| [Impacted Files](https://codecov.io/gh/uber-go/fx/pull/990?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [annotated.go](https://codecov.io/gh/uber-go/fx/pull/990/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-YW5ub3RhdGVkLmdv) | `99.00% <100.00%> (+0.11%)` | :arrow_up: |
| [docs/internal/exectest/cmd.go](https://codecov.io/gh/uber-go/fx/pull/990/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9leGVjdGVzdC9jbWQuZ28=) | | |
| [docs/internal/exectest/output.go](https://codecov.io/gh/uber-go/fx/pull/990/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9leGVjdGVzdC9vdXRwdXQuZ28=) | | |
| [docs/internal/test/fake.go](https://codecov.io/gh/uber-go/fx/pull/990/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC90ZXN0L2Zha2UuZ28=) | | |
| [docs/internal/iotest/read.go](https://codecov.io/gh/uber-go/fx/pull/990/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9pb3Rlc3QvcmVhZC5nbw==) | | |
| [docs/internal/apptest/run.go](https://codecov.io/gh/uber-go/fx/pull/990/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9hcHB0ZXN0L3J1bi5nbw==) | | |
| [docs/internal/httptest/http.go](https://codecov.io/gh/uber-go/fx/pull/990/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9odHRwdGVzdC9odHRwLmdv) | | |

: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: Thanks for doing this work!

Patch:
diff --git a/annotated.go b/annotated.go
index fda4572..d8a17e4 100644
--- a/annotated.go
+++ b/annotated.go
@@ -1176,12 +1176,193 @@ func extractResultFields(types []reflect.Type) ([]reflect.StructField, func(int,
 	}
 }
 
+type fromAnnotation struct {
+	targets []interface{}
+	types   []reflect.Type
+}
+
+var _ Annotation = (*fromAnnotation)(nil)
+
+// From is an [Annotation] that annotates the parameter(s) for a function (i.e. a
+// constructor) to be accepted from other provided types. It is analogous to the
+// [As] for parameter types to the constructor.
+//
+// For example,
+//
+//	type Runner interface { Run() }
+//	func NewFooRunner() *FooRunner // implements Runner
+//	func NewRunnerWrap(r Runner) *RunnerWrap
+//
+//	fx.Provide(
+//	  fx.Annotate(
+//	    NewRunnerWrap,
+//	    fx.From(new(*FooRunner)),
+//	  ),
+//	)
+//
+// Is equivalent to,
+//
+//	fx.Provide(func(r *FooRunner) *RunnerWrap {
+//	  // need *FooRunner instead of Runner
+//	  return NewRunnerWrap(r)
+//	})
+//
+// When the annotated function takes in multiple parameters, each type gets
+// mapped to corresponding positional parameter of the annotated function
+//
+// For example,
+//
+//	func NewBarRunner() *BarRunner // implements Runner
+//	func NewRunnerWraps(r1 Runner, r2 Runner) *RunnerWraps
+//
+//	fx.Provide(
+//	  fx.Annotate(
+//	    NewRunnerWraps,
+//	    fx.From(new(*FooRunner), new(*BarRunner)),
+//	  ),
+//	)
+//
+// Is equivalent to,
+//
+//	fx.Provide(func(r1 *FooRunner, r2 *BarRunner) *RunnerWraps {
+//	  return NewRunnerWraps(r1, r2)
+//	})
+func From(interfaces ...interface{}) Annotation {
+	return &fromAnnotation{targets: interfaces}
+}
+
+func (fr *fromAnnotation) apply(ann *annotated) error {
+	if len(ann.From) > 0 {
+		return errors.New("cannot apply more than one line of From")
+	}
+	ft := reflect.TypeOf(ann.Target)
+	fr.types = make([]reflect.Type, len(fr.targets))
+	for i, typ := range fr.targets {
+		if ft.IsVariadic() && i == ft.NumIn()-1 {
+			return errors.New("fx.From: cannot annotate a variadic argument")
+		}
+		t := reflect.TypeOf(typ)
+		if t == nil || t.Kind() != reflect.Ptr {
+			return fmt.Errorf("fx.From: argument must be a pointer to a type that implements some interface: got %v", t)
+		}
+		fr.types[i] = t.Elem()
+	}
+	ann.From = fr.types
+	return nil
+}
+
+// build builds and returns a constructor after applying a From annotation
+func (fr *fromAnnotation) build(ann *annotated) (interface{}, error) {
+	paramTypes, remap, err := fr.parameters(ann)
+	if err != nil {
+		return nil, err
+	}
+	resultTypes, _ := ann.currentResultTypes()
+
+	origFn := reflect.ValueOf(ann.Target)
+	newFnType := reflect.FuncOf(paramTypes, resultTypes, false)
+	newFn := reflect.MakeFunc(newFnType, func(args []reflect.Value) []reflect.Value {
+		args = remap(args)
+		return origFn.Call(args)
+	})
+	return newFn.Interface(), nil
+}
+
+// parameters returns the type for the parameters of the annotated function,
+// and a function that maps the arguments of the annotated function
+// back to the arguments of the target function.
+func (fr *fromAnnotation) parameters(ann *annotated) (
+	types []reflect.Type,
+	remap func([]reflect.Value) []reflect.Value,
+	err error,
+) {
+	ft := reflect.TypeOf(ann.Target)
+	types = make([]reflect.Type, ft.NumIn())
+	for i := 0; i < ft.NumIn(); i++ {
+		types[i] = ft.In(i)
+	}
+
+	// No parameter annotations. Return the original types
+	// and an identity function.
+	if len(fr.targets) == 0 {
+		return types, func(args []reflect.Value) []reflect.Value {
+			return args
+		}, nil
+	}
+
+	// Turn parameters into an fx.In struct.
+	inFields := []reflect.StructField{_inAnnotationField}
+
+	// The following situations may occur:
+	// 1. there was a variadic argument, so it was pre-transformed.
+	// 2. another parameter annotation was transformed (ex: ParamTags).
+	// so need to visit fields of the fx.In struct.
+	if len(types) > 0 && isIn(types[0]) {
+		paramType := types[0]
+
+		for i := 1; i < paramType.NumField(); i++ {
+			origField := paramType.Field(i)
+			field := reflect.StructField{
+				Name: origField.Name,
+				Type: origField.Type,
+				Tag:  origField.Tag,
+			}
+			if i-1 < len(fr.types) {
+				t := fr.types[i-1]
+				if !t.Implements(field.Type) {
+					return nil, nil, fmt.Errorf("invalid fx.From: %v does not implement %v", t, field.Type)
+				}
+				field.Type = t
+			}
+
+			inFields = append(inFields, field)
+		}
+
+		types = []reflect.Type{reflect.StructOf(inFields)}
+		return types, func(args []reflect.Value) []reflect.Value {
+			param := args[0]
+			args[0] = reflect.New(paramType).Elem()
+			for i := 1; i < paramType.NumField(); i++ {
+				args[0].Field(i).Set(param.Field(i))
+			}
+			return args
+		}, nil
+	}
+
+	for i, t := range types {
+		field := reflect.StructField{
+			Name: fmt.Sprintf("Field%d", i),
+			Type: t,
+		}
+		if i < len(fr.types) {
+			t := fr.types[i]
+			if !t.Implements(field.Type) {
+				return nil, nil, fmt.Errorf("invalid fx.From: %v does not implement %v", t, field.Type)
+			}
+			field.Type = t
+		}
+
+		inFields = append(inFields, field)
+	}
+
+	types = []reflect.Type{reflect.StructOf(inFields)}
+	return types, func(args []reflect.Value) []reflect.Value {
+		params := args[0]
+		args = args[:0]
+		for i := 0; i < ft.NumIn(); i++ {
+			args = append(args, params.Field(i+1))
+		}
+		return args
+	}, nil
+}
+
 type annotated struct {
 	Target      interface{}
 	Annotations []Annotation
 	ParamTags   []string
 	ResultTags  []string
 	As          [][]reflect.Type
+	From        []reflect.Type
 	FuncPtr     uintptr
 	Hooks       []*lifecycleHookAnnotation
 	// container is used to build private scopes for lifecycle hook functions
@@ -1202,6 +1383,9 @@ func (ann annotated) String() string {
 	if as := ann.As; len(as) > 0 {
 		fmt.Fprintf(&sb, ", fx.As(%v)", as)
 	}
+	if from := ann.From; len(from) > 0 {
+		fmt.Fprintf(&sb, ", fx.From(%v)", from)
+	}
 	return sb.String()
 }
 


Test patch:
diff --git a/annotated_test.go b/annotated_test.go
index 784e8e6..a45fd8b 100644
--- a/annotated_test.go
+++ b/annotated_test.go
@@ -70,6 +70,14 @@ func TestAnnotated(t *testing.T) {
 	})
 }
 
+type fromStringer struct {
+	name string
+}
+
+func (from *fromStringer) String() string {
+	return from.name
+}
+
 type asStringer struct {
 	name string
 }
@@ -86,6 +94,337 @@ func (a anotherStringer) String() string {
 	return a.name
 }
 
+func TestAnnotatedFrom(t *testing.T) {
+	t.Parallel()
+	type myStringer interface {
+		String() string
+	}
+
+	newFromStringer := func() *fromStringer {
+		return &fromStringer{
+			name: "a good stringer",
+		}
+	}
+
+	tests := []struct {
+		desc    string
+		provide fx.Option
+		invoke  interface{}
+	}{
+		{
+			desc: "provide a good stringer",
+			provide: fx.Provide(
+				newFromStringer,
+				fx.Annotate(
+					func(myStringer myStringer) fmt.Stringer {
+						return &fromStringer{
+							name: myStringer.String(),
+						}
+					},
+					fx.From(new(*fromStringer)),
+				),
+			),
+			invoke: func(s fmt.Stringer) {
+				assert.Equal(t, s.String(), "a good stringer")
+			},
+		},
+		{
+			desc: "value type implementing interface",
+			provide: fx.Provide(
+				func() anotherStringer {
+					return anotherStringer{
+						"another stringer",
+					}
+				},
+				fx.Annotate(
+					func(myStringer myStringer) fmt.Stringer {
+						return &fromStringer{
+							name: myStringer.String(),
+						}
+					},
+					fx.From(new(anotherStringer)),
+				),
+			),
+			invoke: func(s fmt.Stringer) {
+				assert.Equal(t, s.String(), "another stringer")
+			},
+		},
+		{
+			desc: "provide with multiple types From",
+			provide: fx.Provide(
+				newFromStringer,
+				func() anotherStringer {
+					return anotherStringer{
+						"another stringer",
+					}
+				},
+				fx.Annotate(
+					func(myStringer1 myStringer, myStringer2 myStringer) fmt.Stringer {
+						return &fromStringer{
+							name: myStringer1.String() + " and " + myStringer2.String(),
+						}
+					},
+					fx.From(new(anotherStringer), new(*fromStringer)),
+				),
+			),
+			invoke: func(s fmt.Stringer) {
+				assert.Equal(t, s.String(), "another stringer and a good stringer")
+			},
+		},
+		{
+			desc: "provide from with param annotation",
+			provide: fx.Provide(
+				fx.Annotate(
+					newFromStringer,
+					fx.ResultTags(`name:"struct1"`),
+				),
+				fx.Annotate(
+					func(myStringer myStringer) fmt.Stringer {
+						return &fromStringer{
+							name: myStringer.String(),
+						}
+					},
+					fx.From(new(*fromStringer)),
+					fx.ParamTags(`name:"struct1"`),
+				),
+			),
+			invoke: func(s fmt.Stringer) {
+				assert.Equal(t, s.String(), "a good stringer")
+			},
+		},
+		{
+			// same as the test above, except now we annotate
+			// it in a different order.
+			desc: "provide from with param annotation, in different order",
+			provide: fx.Provide(
+				fx.Annotate(
+					newFromStringer,
+					fx.ResultTags(`name:"struct1"`),
+				),
+				fx.Annotate(
+					func(myStringer myStringer) fmt.Stringer {
+						return &fromStringer{
+							name: myStringer.String(),
+						}
+					},
+					fx.ParamTags(`name:"struct1"`),
+					fx.From(new(*fromStringer)),
+				),
+			),
+			invoke: func(s fmt.Stringer) {
+				assert.Equal(t, s.String(), "a good stringer")
+			},
+		},
+		{
+			desc: "annotate fewer items than required for constructor",
+			provide: fx.Provide(
+				newFromStringer,
+				func() anotherStringer {
+					return anotherStringer{
+						"another stringer",
+					}
+				},
+				fx.Annotate(
+					func(myStringer1 myStringer, fromStringer2 *fromStringer) fmt.Stringer {
+						return &fromStringer{
+							name: myStringer1.String() + " and " + fromStringer2.String(),
+						}
+					},
+					fx.From(new(anotherStringer)),
+				),
+			),
+			invoke: func(s fmt.Stringer) {
+				assert.Equal(t, s.String(), "another stringer and a good stringer")
+			},
+		},
+		{
+			desc: "Provide with empty From type",
+			provide: fx.Provide(
+				newFromStringer,
+				fx.Annotate(
+					func(myStringer *fromStringer) fmt.Stringer {
+						return &fromStringer{
+							name: myStringer.String(),
+						}
+					},
+					fx.From(),
+				),
+			),
+			invoke: func(s fmt.Stringer) {
+				assert.Equal(t, s.String(), "a good stringer")
+			},
+		},
+		{
+			desc: "Provide with variadic function",
+			provide: fx.Provide(
+				newFromStringer,
+				fx.Annotate(
+					func(myStringer myStringer, x ...int) fmt.Stringer {
+						return &fromStringer{
+							name: myStringer.String(),
+						}
+					},
+					fx.From(new(*fromStringer)),
+				),
+			),
+			invoke: func(s fmt.Stringer) {
+				assert.Equal(t, s.String(), "a good stringer")
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.desc, func(t *testing.T) {
+			t.Parallel()
+
+			app := NewForTest(t,
+				fx.WithLogger(func() fxevent.Logger {
+					return fxtest.NewTestLogger(t)
+				}),
+				tt.provide,
+				fx.Invoke(tt.invoke),
+			)
+			require.NoError(t, app.Err())
+		})
+	}
+}
+
+func TestAnnotatedFromFailures(t *testing.T) {
+	t.Parallel()
+	type myStringer interface {
+		String() string
+	}
+
+	newFromStringer := func() *fromStringer {
+		return &fromStringer{name: "stringer"}
+	}
+
+	tests := []struct {
+		desc          string
+		provide       fx.Option
+		invoke        interface{}
+		errorContains string
+	}{
+		{
+			desc: "provide when an illegal type From",
+			provide: fx.Provide(
+				fx.Annotate(
+					func(writer io.Writer) fmt.Stringer {
+						return &fromStringer{}
+					},
+					fx.From(new(*fromStringer)),
+				),
+			),
+			invoke: func(stringer fmt.Stringer) {
+				fmt.Println(stringer.String())
+			},
+			errorContains: "*fx_test.fromStringer does not implement io.Writer",
+		},
+		{
+			desc: "provide with variadic function and an illegal type From",
+			provide: fx.Provide(
+				fx.Annotate(
+					func(writer io.Writer, x ...int) fmt.Stringer {
+						return &fromStringer{}
+					},
+					fx.From(new(*fromStringer)),
+				),
+			),
+			invoke: func(stringer fmt.Stringer) {
+				fmt.Println(stringer.String())
+			},
+			errorContains: "*fx_test.fromStringer does not implement io.Writer",
+		},
+		{
+			desc: "don't provide original type using From",
+			provide: fx.Provide(
+				fx.Annotate(
+					func(myStringer myStringer) fmt.Stringer {
+						return &fromStringer{
+							name: myStringer.String(),
+						}
+					},
+					fx.From(new(*fromStringer)),
+				),
+			),
+			invoke: func(stringer fmt.Stringer) {
+				fmt.Println(stringer.String())
+			},
+			errorContains: "missing type: *fx_test.fromStringer",
+		},
+		{
+			desc: "fail to provide with name annotation",
+			provide: fx.Provide(
+				fx.Annotate(
+					newFromStringer,
+				),
+				fx.Annotate(
+					func(myStringer myStringer) fmt.Stringer {
+						return &fromStringer{
+							name: myStringer.String(),
+						}
+					},
+					fx.From(new(*fromStringer)),
+					fx.ParamTags(`name:"struct1"`),
+				),
+			),
+			invoke: func(s fmt.Stringer) {
+				assert.Equal(t, s.String(), "a good stringer")
+			},
+			errorContains: `missing type: *fx_test.fromStringer[name="struct1"]`,
+		},
+		{
+			desc: "non-pointer argument to From",
+			provide: fx.Provide(
+				fx.Annotate(
+					newFromStringer,
+					fx.From("foo"),
+				),
+			),
+			errorContains: "argument must be a pointer",
+		},
+		{
+			desc: "multiple from annotations",
+			provide: fx.Provide(
+				fx.Annotate(
+					newFromStringer,
+					fx.From(new(asStringer)),
+					fx.From(new(asStringer)),
+				),
+			),
+			errorContains: "cannot apply more than one line of From",
+		},
+		{
+			desc: "variadic argument",
+			provide: fx.Provide(
+				fx.Annotate(
+					func(ss ...myStringer) {},
+					fx.From(new(asStringer)),
+				),
+			),
+			errorContains: "cannot annotate a variadic argument",
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.desc, func(t *testing.T) {
+			t.Parallel()
+			app := NewForTest(t,
+				fx.WithLogger(func() fxevent.Logger {
+					return fxtest.NewTestLogger(t)
+				}),
+				tt.provide,
+				fx.Invoke(tt.invoke),
+			)
+			err := app.Err()
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), tt.errorContains)
+		})
+	}
+}
+
 func TestAnnotatedAs(t *testing.T) {
 	t.Parallel()
 	type in struct {