Repository: uber-go/fx
Pull request #837 (2022-02-11 18:35:53)

Add Fx.Replace
This adds `fx.Replace`, which replaces a value already provided in the graph with another value. This is similar to how `fx.Supply` provides an instantiated value to the graph.

For example the following code that uses fx.Decorate to modify the `*bytes.Buffer` type...
```go
fx.New(
  fx.Provide(func() *bytes.Buffer { ... }),
  fx.Decorate(func() *bytes.Buffer {
    ...
  }),
  fx.Invoke(func(b *bytes.Buffer) {
    // I get a modified buffer!
  }),
)
```

is the same as this version that uses fx.Replace:
```go
b := func() *bytes.Buffer { ... }

fx.New(
  fx.Provide(func() *bytes.Buffer { ... }),
  fx.Replace(b),
  fx.Invoke(func(b *bytes.Buffer) {
    // I get a modified buffer!
  }),
)
```

Implementation-wise, it is very similar to how fx.Supply creates a function that produces the given value using reflection and `dig.Provide`s it - fx.Replace creates a function that produces the given value with no argument using reflection, and `dig.Decorate` with that function.

codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/fx/pull/837?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
> Merging [#837](https://codecov.io/gh/uber-go/fx/pull/837?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (90c4f38) into [master](https://codecov.io/gh/uber-go/fx/commit/9173ccdc0e99ac9929831e8435b93501310d10cf?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (9173ccd) will **decrease** coverage by `0.14%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/uber-go/fx/pull/837/graphs/tree.svg?width=650&height=150&src=pr&token=PYY9PSfaC6&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)](https://codecov.io/gh/uber-go/fx/pull/837?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)

```diff
@@            Coverage Diff             @@
##           master     #837      +/-   ##
==========================================
- Coverage   98.56%   98.42%   -0.15%     
==========================================
  Files          29       30       +1     
  Lines        1043     1076      +33     
==========================================
+ Hits         1028     1059      +31     
- Misses         10       11       +1     
- Partials        5        6       +1     
```


| [Impacted Files](https://codecov.io/gh/uber-go/fx/pull/837?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [replace.go](https://codecov.io/gh/uber-go/fx/pull/837/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-cmVwbGFjZS5nbw==) | `100.00% <100.00%> (ø)` | |
| [app.go](https://codecov.io/gh/uber-go/fx/pull/837/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-YXBwLmdv) | `94.95% <0.00%> (-0.92%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/uber-go/fx/pull/837?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/uber-go/fx/pull/837?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go). Last update [9173ccd...90c4f38](https://codecov.io/gh/uber-go/fx/pull/837?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).

sywhang: @abg thanks for catching the issue with the test. Fixed that.
> Question: if I do this, what's the value in group:"t" at the top-level/what should it be?

I believe that should be `[]string{"d", "e", "f"}`? Unless that wasn't what you were thinking. Decoration at top-level should override anything provided by child modules. 


Patch:
diff --git a/replace.go b/replace.go
new file mode 100644
index 0000000..f7fa865
--- /dev/null
+++ b/replace.go
@@ -0,0 +1,103 @@
+// Copyright (c) 2022 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package fx
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+
+	"go.uber.org/fx/internal/fxreflect"
+)
+
+// Replace provides instantiated values for graph modification. Similar to
+// what fx.Supply is to fx.Provide, values provided by fx.Replace behaves
+// similarly to values produced by decorators specified with fx.Decorate.
+//
+// Refer to the documentation on fx.Decorate to see how graph modifications
+// work with fx.Module.
+//
+// Replace panics if a value (or annotation target) is an untyped nil or an error.
+func Replace(values ...interface{}) Option {
+	decorators := make([]interface{}, len(values)) // one function per value
+	types := make([]reflect.Type, len(values))
+	for i, value := range values {
+		switch value := value.(type) {
+		case annotated:
+			var typ reflect.Type
+			value.Target, typ = newReplaceDecorator(value.Target)
+			decorators[i] = value
+			types[i] = typ
+		default:
+			decorators[i], types[i] = newReplaceDecorator(value)
+		}
+	}
+
+	return replaceOption{
+		Targets: decorators,
+		Types:   types,
+		Stack:   fxreflect.CallerStack(1, 0),
+	}
+}
+
+type replaceOption struct {
+	Targets []interface{}
+	Types   []reflect.Type // type of value produced by constructor[i]
+	Stack   fxreflect.Stack
+}
+
+func (o replaceOption) apply(m *module) {
+	for _, target := range o.Targets {
+		m.decorators = append(m.decorators, decorator{
+			Target: target,
+			Stack:  o.Stack,
+		})
+	}
+}
+
+func (o replaceOption) String() string {
+	items := make([]string, 0, len(o.Targets))
+	for _, typ := range o.Types {
+		items = append(items, typ.String())
+	}
+	return fmt.Sprintf("fx.Replace(%s)", strings.Join(items, ", "))
+}
+
+// Returns a function that takes no parameters, and returns the given value.
+func newReplaceDecorator(value interface{}) (interface{}, reflect.Type) {
+	switch value.(type) {
+	case nil:
+		panic("untyped nil passed to fx.Replace")
+	case error:
+		panic("error value passed to fx.Replace")
+	}
+
+	typ := reflect.TypeOf(value)
+	returnTypes := []reflect.Type{typ}
+	returnValues := []reflect.Value{reflect.ValueOf(value)}
+
+	ft := reflect.FuncOf([]reflect.Type{}, returnTypes, false)
+	fv := reflect.MakeFunc(ft, func([]reflect.Value) []reflect.Value {
+		return returnValues
+	})
+
+	return fv.Interface(), typ
+}


Test patch:
diff --git a/app_test.go b/app_test.go
index ad8d095..48110d7 100644
--- a/app_test.go
+++ b/app_test.go
@@ -1698,6 +1698,11 @@ func TestOptionString(t *testing.T) {
 			give: Decorate(bytes.NewBufferString),
 			want: "fx.Decorate(bytes.NewBufferString())",
 		},
+		{
+			desc: "Replace",
+			give: Replace(bytes.NewReader(nil)),
+			want: "fx.Replace(*bytes.Reader)",
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/replace_test.go b/replace_test.go
new file mode 100644
index 0000000..98cec06
--- /dev/null
+++ b/replace_test.go
@@ -0,0 +1,207 @@
+// Copyright (c) 2022 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package fx_test
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.uber.org/fx"
+	"go.uber.org/fx/fxtest"
+)
+
+func TestReplaceSuccess(t *testing.T) {
+	t.Parallel()
+
+	t.Run("replace a value", func(t *testing.T) {
+		t.Parallel()
+		type A struct {
+			Value string
+		}
+		a := &A{Value: "a'"}
+		app := fxtest.New(t,
+			fx.Provide(func() *A {
+				return &A{
+					Value: "a",
+				}
+			}),
+			fx.Replace(a),
+			fx.Invoke(func(a *A) {
+				assert.Equal(t, "a'", a.Value)
+			}),
+		)
+		defer app.RequireStart().RequireStop()
+	})
+
+	t.Run("replace in a module", func(t *testing.T) {
+		t.Parallel()
+
+		type A struct {
+			Value string
+		}
+
+		a := &A{Value: "A"}
+
+		app := fxtest.New(t,
+			fx.Module("child",
+				fx.Replace(a),
+				fx.Invoke(func(a *A) {
+					assert.Equal(t, "A", a.Value)
+				}),
+			),
+			fx.Provide(func() *A {
+				return &A{
+					Value: "a",
+				}
+			}),
+		)
+		defer app.RequireStart().RequireStop()
+	})
+
+	t.Run("replace with annotate", func(t *testing.T) {
+		t.Parallel()
+
+		type A struct {
+			Value string
+		}
+
+		app := fxtest.New(t,
+			fx.Supply(
+				fx.Annotate(A{"A"}, fx.ResultTags(`name:"t"`)),
+			),
+			fx.Replace(
+				fx.Annotate(A{"B"}, fx.ResultTags(`name:"t"`)),
+			),
+			fx.Invoke(fx.Annotate(func(a A) {
+				assert.Equal(t, a.Value, "B")
+			}, fx.ParamTags(`name:"t"`))),
+		)
+		defer app.RequireStart().RequireStop()
+	})
+
+	t.Run("replace a value group with annotate", func(t *testing.T) {
+		t.Parallel()
+
+		app := fxtest.New(t,
+			fx.Supply(
+				fx.Annotate([]string{"A", "B", "C"}, fx.ResultTags(`group:"t,flatten"`)),
+			),
+			fx.Replace(fx.Annotate([]string{"a", "b", "c"}, fx.ResultTags(`group:"t"`))),
+			fx.Invoke(fx.Annotate(func(ss ...string) {
+				assert.ElementsMatch(t, []string{"a", "b", "c"}, ss)
+			}, fx.ParamTags(`group:"t"`))),
+		)
+		defer app.RequireStart().RequireStop()
+	})
+
+	t.Run("replace a value group supplied by a child module from root module", func(t *testing.T) {
+		t.Parallel()
+
+		foo := fx.Module("foo",
+			fx.Supply(
+				fx.Annotate([]string{"a", "b", "c"}, fx.ResultTags(`group:"t,flatten"`)),
+			),
+		)
+
+		fx.New(
+			fx.Module("wrapfoo",
+				foo,
+				fx.Replace(
+					fx.Annotate([]string{"d", "e", "f"}, fx.ResultTags(`group:"t"`)),
+				),
+				fx.Invoke(fx.Annotate(func(ss []string) {
+					assert.ElementsMatch(t, []string{"d", "e", "f"}, ss)
+				}, fx.ParamTags(`group:"t"`))),
+			),
+		)
+	})
+}
+
+func TestReplaceFailure(t *testing.T) {
+	t.Parallel()
+
+	t.Run("replace same value twice", func(t *testing.T) {
+		t.Parallel()
+
+		type A struct {
+			Value string
+		}
+		a := &A{Value: "A"}
+		app := NewForTest(t,
+			fx.Provide(func() *A {
+				return &A{Value: "a"}
+			}),
+			fx.Module("child",
+				fx.Replace(a),
+				fx.Replace(a),
+				fx.Invoke(func(a *A) {
+					assert.Fail(t, "this should never run")
+				}),
+			),
+		)
+		err := app.Err()
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "*fx_test.A already decorated")
+	})
+
+	t.Run("replace a value that wasn't provided", func(t *testing.T) {
+		t.Parallel()
+
+		type A struct{}
+
+		app := NewForTest(t,
+			fx.Replace(A{}),
+			fx.Invoke(func(a *A) {
+			}),
+		)
+		err := app.Err()
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "missing type: *fx_test.A")
+	})
+
+	t.Run("replace panics on invalid values", func(t *testing.T) {
+		t.Parallel()
+
+		type A struct{}
+		type B struct{}
+
+		require.PanicsWithValuef(
+			t,
+			"untyped nil passed to fx.Replace",
+			func() { fx.Replace(A{}, nil) },
+			"a naked nil should panic",
+		)
+
+		require.PanicsWithValuef(
+			t,
+			"error value passed to fx.Replace",
+			func() { fx.Replace(A{}, errors.New("some error")) },
+			"replacing with an error should panic",
+		)
+
+		require.NotPanicsf(
+			t,
+			func() { fx.Replace(A{}, (*B)(nil)) },
+			"a wrapped nil should not panic")
+	})
+}