-
Notifications
You must be signed in to change notification settings - Fork 290
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
1a0d80d
commit d3293a9
Showing
3 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
}) | ||
} |