Skip to content

Commit

Permalink
Add Fx.Replace (#837)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sywhang authored and josephinedotlee committed Feb 18, 2022
1 parent 1a0d80d commit d3293a9
Show file tree
Hide file tree
Showing 3 changed files with 315 additions and 0 deletions.
5 changes: 5 additions & 0 deletions app_test.go
Expand Up @@ -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 {
Expand Down
103 changes: 103 additions & 0 deletions 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
}
207 changes: 207 additions & 0 deletions 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")
})
}

0 comments on commit d3293a9

Please sign in to comment.