From 1c1632a0c437341aa160fc0f39455a8c2aef9b09 Mon Sep 17 00:00:00 2001 From: Sung Yoon Whang Date: Wed, 9 Feb 2022 00:20:16 -0800 Subject: [PATCH 1/8] Add fx.Decorate --- app.go | 7 ++ decorate.go | 88 ++++++++++++++++++++++ decorate_test.go | 185 +++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- module.go | 30 ++++++-- 6 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 decorate.go create mode 100644 decorate_test.go diff --git a/app.go b/app.go index 0942d9870..0598891c2 100644 --- a/app.go +++ b/app.go @@ -518,6 +518,13 @@ func New(opts ...Option) *App { } } + // Run decorators before executing any Invokes. + if err := app.root.decorate(); err != nil { + app.err = err + + return app + } + // This error might have come from the provide loop above. We've // already flushed to the custom logger, so we can return. if app.err != nil { diff --git a/decorate.go b/decorate.go new file mode 100644 index 000000000..2e792acbc --- /dev/null +++ b/decorate.go @@ -0,0 +1,88 @@ +// 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" + "strings" + + "go.uber.org/dig" + "go.uber.org/fx/internal/fxreflect" +) + +func Decorate(decorators ...interface{}) Option { + return decorateOption{ + Targets: decorators, + Stack: fxreflect.CallerStack(1, 0), + } +} + +type decorateOption struct { + Targets []interface{} + Stack fxreflect.Stack +} + +func (o decorateOption) apply(mod *module) { + for _, target := range o.Targets { + mod.decorators = append(mod.decorators, decorator{ + Target: target, + Stack: o.Stack, + }) + } +} + +func (o decorateOption) String() string { + items := make([]string, len(o.Targets)) + for i, f := range o.Targets { + items[i] = fxreflect.FuncName(f) + } + return fmt.Sprintf("fx.Decorate(%s)", strings.Join(items, ", ")) +} + +// provide is a single decorators used in Fx. +type decorator struct { + // Constructor provided to Fx. This may be an fx.Annotated. + Target interface{} + + // Stack trace of where this provide was made. + Stack fxreflect.Stack +} + +func runDecorator(c container, d decorator, opts ...dig.DecorateOption) error { + decorator := d.Target + + switch decorator := decorator.(type) { + case annotated: + dcor, err := decorator.Build() + if err != nil { + return fmt.Errorf("fx.Decorate(%v) from:\n%+vFailed: %v", decorator, d.Stack, err) + } + + if err := c.Decorate(dcor, opts...); err != nil { + return fmt.Errorf("fx.Decorate(%v) from:\n%+vFailed: %v", decorator, d.Stack, err) + } + default: + if err := c.Decorate(decorator, opts...); err != nil { + return fmt.Errorf("fx.Decorate(%v) from:\n%+vFailed: %v", decorator, d.Stack, err) + } + } + return nil +} diff --git a/decorate_test.go b/decorate_test.go new file mode 100644 index 000000000..9b968c12e --- /dev/null +++ b/decorate_test.go @@ -0,0 +1,185 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" +) + +func TestDecorateSuccess(t *testing.T) { + type Logger struct { + Name string + } + + t.Run("decorate something from Module", func(t *testing.T) { + redis := fx.Module("redis", + fx.Provide(func() *Logger { + return &Logger{Name: "redis"} + }), + ) + + testRedis := fx.Module("testRedis", + redis, + fx.Decorate(func() *Logger { + return &Logger{Name: "testRedis"} + }), + fx.Invoke(func(l *Logger) { + assert.Equal(t, "testRedis", l.Name) + }), + ) + + app := fxtest.New(t, + testRedis, + fx.Invoke(func(l *Logger) { + assert.Equal(t, "redis", l.Name) + }), + ) + defer app.RequireStart().RequireStop() + }) + + t.Run("decorate a dependency from root", func(t *testing.T) { + redis := fx.Module("redis", + fx.Decorate(func() *Logger { + return &Logger{Name: "redis"} + }), + fx.Invoke(func(l *Logger) { + assert.Equal(t, "redis", l.Name) + }), + ) + app := fxtest.New(t, + redis, + fx.Provide(func() *Logger { + assert.Fail(t, "should not run this") + return &Logger{Name: "root"} + }), + ) + defer app.RequireStart().RequireStop() + }) + + t.Run("use Decorate in root", func(t *testing.T) { + redis := fx.Module("redis", + fx.Invoke(func(l *Logger) { + assert.Equal(t, "decorated logger", l.Name) + }), + ) + logger := fx.Module("logger", + fx.Provide(func() *Logger { + return &Logger{Name: "logger"} + }), + ) + app := fxtest.New(t, + redis, + logger, + fx.Decorate(func(l *Logger) *Logger { + return &Logger{Name: "decorated " + l.Name} + }), + ) + defer app.RequireStart().RequireStop() + }) + + t.Run("use Decorate with Annotate", func(t *testing.T) { + type Coffee struct { + Name string + Price int + } + + cafe := fx.Module("cafe", + fx.Provide(fx.Annotate(func() *Coffee { + return &Coffee{Name: "Americano", Price: 3} + }, fx.ResultTags(`group:"coffee"`))), + fx.Provide(fx.Annotate(func() *Coffee { + return &Coffee{Name: "Cappucino", Price: 4} + }, fx.ResultTags(`group:"coffee"`))), + fx.Provide(fx.Annotate(func() *Coffee { + return &Coffee{Name: "Cold Brew", Price: 4} + }, fx.ResultTags(`group:"coffee"`))), + ) + + takeout := fx.Module("takeout", + cafe, + fx.Decorate(fx.Annotate(func(coffee []*Coffee) []*Coffee { + var newC []*Coffee + for _, c := range coffee { + newC = append(newC, &Coffee{ + Name: c.Name, + Price: c.Price + 1, + }) + } + return newC + }, fx.ParamTags(`group:"coffee"`), fx.ResultTags(`group:"coffee"`))), + fx.Invoke(fx.Annotate(func(coffee []*Coffee) { + assert.Equal(t, 3, len(coffee)) + totalPrice := 0 + for _, c := range coffee { + totalPrice += c.Price + } + assert.Equal(t, 4+5+5, totalPrice) + }, fx.ParamTags(`group:"coffee"`))), + ) + + app := fxtest.New(t, + takeout, + ) + defer app.RequireStart().RequireStop() + }) + + t.Run("use Decorate with parameter/result struct", func(t *testing.T) { + type Logger struct { + Name string + } + type A struct { + fx.In + + Log *Logger + Version int `name:"versionNum"` + } + type B struct { + fx.Out + + Log *Logger + Version int `name:"versionNum"` + } + app := fxtest.New(t, + fx.Provide( + fx.Annotate(func() int { return 1 }, + fx.ResultTags(`name:"versionNum"`)), + func() *Logger { + return &Logger{Name: "logger"} + }, + ), + fx.Decorate(func(a A) B { + return B{ + Log: &Logger{Name: a.Log.Name + " decorated"}, + Version: a.Version + 1, + } + }), + fx.Invoke(fx.Annotate(func(l *Logger, ver int) { + assert.Equal(t, "logger decorated", l.Name) + assert.Equal(t, 2, ver) + }, fx.ParamTags(``, `name:"versionNum"`))), + ) + defer app.RequireStart().RequireStop() + }) +} diff --git a/go.mod b/go.mod index eac0e7f1f..d53179e07 100644 --- a/go.mod +++ b/go.mod @@ -12,4 +12,4 @@ require ( golang.org/x/sys v0.0.0-20210903071746-97244b99971b ) -replace go.uber.org/dig => github.com/uber-go/dig v1.13.1-0.20220106194054-29dd17211ed4 +replace go.uber.org/dig => github.com/uber-go/dig v1.13.1-0.20220208182428-8193c7fedade diff --git a/go.sum b/go.sum index 613bd4598..067f9b9d2 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/uber-go/dig v1.13.1-0.20220106194054-29dd17211ed4 h1:lTskNvD4R8nSk2vH8AwwU7N1ZpFE7AzBv2CGTfCAxlw= -github.com/uber-go/dig v1.13.1-0.20220106194054-29dd17211ed4/go.mod h1:jHAn/z1Ld1luVVyGKOAIFYz/uBFqKjjEEdIqVAqfQ2o= +github.com/uber-go/dig v1.13.1-0.20220208182428-8193c7fedade h1:q2FxCCuMWy3p904qjF7NyaFj4EP/G5T7eACVKLvMgrU= +github.com/uber-go/dig v1.13.1-0.20220208182428-8193c7fedade/go.mod h1:jHAn/z1Ld1luVVyGKOAIFYz/uBFqKjjEEdIqVAqfQ2o= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= diff --git a/module.go b/module.go index 47d99ecea..b07bb5a5b 100644 --- a/module.go +++ b/module.go @@ -36,6 +36,7 @@ import ( type container interface { Invoke(interface{}, ...dig.InvokeOption) error Provide(interface{}, ...dig.ProvideOption) error + Decorate(interface{}, ...dig.DecorateOption) error } // Module is a named group of zero or more fx.Options. @@ -76,13 +77,14 @@ func (o moduleOption) apply(mod *module) { } type module struct { - parent *module - name string - scope *dig.Scope - provides []provide - invokes []invoke - modules []*module - app *App + parent *module + name string + scope *dig.Scope + provides []provide + invokes []invoke + decorators []decorator + modules []*module + app *App } // builds the Scopes using the App's Container. Note that this happens @@ -174,3 +176,17 @@ func (m *module) executeInvoke(i invoke) (err error) { }) return err } + +func (m *module) decorate() (err error) { + for _, decorator := range m.decorators { + if err := runDecorator(m.scope, decorator); err != nil { + return err + } + } + for _, m := range m.modules { + if err := m.decorate(); err != nil { + return err + } + } + return nil +} From 3f9783736583be7b0d82644d2a07578dfc2e2b88 Mon Sep 17 00:00:00 2001 From: Sung Yoon Whang Date: Wed, 9 Feb 2022 11:02:02 -0800 Subject: [PATCH 2/8] add more failure case tests --- decorate_test.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/decorate_test.go b/decorate_test.go index 9b968c12e..523118bb9 100644 --- a/decorate_test.go +++ b/decorate_test.go @@ -21,9 +21,11 @@ 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" ) @@ -183,3 +185,116 @@ func TestDecorateSuccess(t *testing.T) { defer app.RequireStart().RequireStop() }) } + +func TestDecorateFailure(t *testing.T) { + t.Run("decorator returns an error", func(t *testing.T) { + type Logger struct { + Name string + } + + app := NewForTest(t, + fx.Provide(func() *Logger { + return &Logger{Name: "root"} + }), + fx.Decorate(func(l *Logger) (*Logger, error) { + return &Logger{Name: l.Name + "decorated"}, errors.New("minor sadness") + }), + fx.Invoke(func(l *Logger) { + assert.Fail(t, "this should not be executed") + }), + ) + + err := app.Err() + require.Error(t, err) + assert.Contains(t, err.Error(), "minor sadness") + }) + + t.Run("decorate the same type twice from the same Module", func(t *testing.T) { + type Logger struct { + Name string + } + + app := NewForTest(t, + fx.Provide(func() *Logger { + return &Logger{Name: "root"} + }), + fx.Decorate(func(l *Logger) *Logger { + return &Logger{Name: "dec1 " + l.Name} + }), + fx.Decorate(func(l *Logger) *Logger { + return &Logger{Name: "dec2 " + l.Name} + }), + ) + + err := app.Err() + require.Error(t, err) + assert.Contains(t, err.Error(), "*fx_test.Logger already decorated") + }) + + t.Run("annotated decorator returns an error", func(t *testing.T) { + type Logger struct { + Name string + } + + tag := `name:"decoratedLogger"` + app := NewForTest(t, + fx.Provide(fx.Annotate(func() *Logger { + return &Logger{Name: "root"} + }, fx.ResultTags(tag))), + fx.Decorate(fx.Annotate(func(l *Logger) (*Logger, error) { + return &Logger{Name: "dec1 " + l.Name}, errors.New("major sadness") + }, fx.ParamTags(tag), fx.ResultTags(tag))), + fx.Invoke(fx.Annotate(func(l *Logger) { + assert.Fail(t, "this should never run") + }, fx.ParamTags(tag))), + ) + + err := app.Err() + require.Error(t, err) + assert.Contains(t, err.Error(), "major sadness") + }) + + t.Run("decorator missing a dependency", func(t *testing.T) { + type Logger struct { + Name string + } + type Config struct { + Name string + } + + app := NewForTest(t, + fx.Provide(func() *Logger { + return &Logger{Name: "logger"} + }), + fx.Decorate(func(l *Logger, c *Config) *Logger { + return &Logger{Name: l.Name + c.Name} + }), + fx.Invoke(func(l *Logger) { + assert.Fail(t, "this should never run") + }), + ) + + err := app.Err() + require.Error(t, err) + assert.Contains(t, err.Error(), "missing dependencies") + }) + + t.Run("use a decorator like a provider", func(t *testing.T) { + type Logger struct { + Name string + } + + app := NewForTest(t, + fx.Decorate(func() *Logger { + return &Logger{Name: "decorator"} + }), + fx.Invoke(func(l *Logger) { + assert.Fail(t, "this should never run") + }), + ) + + err := app.Err() + require.Error(t, err) + assert.Contains(t, err.Error(), "missing dependencies") + }) +} From 3d6178ccd740db504fee388c9040e15fd0b9bd8f Mon Sep 17 00:00:00 2001 From: Sung Yoon Whang Date: Wed, 9 Feb 2022 12:04:54 -0800 Subject: [PATCH 3/8] Add docs --- decorate.go | 21 +++++++++++++++++++++ decorate_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/decorate.go b/decorate.go index 2e792acbc..2690d49a3 100644 --- a/decorate.go +++ b/decorate.go @@ -28,6 +28,27 @@ import ( "go.uber.org/fx/internal/fxreflect" ) +// Decorate specifies one or more decorator functions to an fx application. +// Decorator functions let users augment objects in the graph. They can take in +// zero or more dependencies that must be provided to the application with fx.Provide, +// and produce one or more values that can be used by other invoked values. +// +// An example decorator is the following function which accepts a value, augments that value, +// and returns the replacement value. +// +// fx.Decorate(func(log *zap.Logger) *zap.Logger { +// return log.Named("myapp") +// }) +// +// The following decorator accepts multiple dependencies from the graph, augments and returns +// one of them. +// +// fx.Decorate(func(log *zap.Logger, cfg *Config) *zap.Logger { +// return log.Named(cfg.Name) +// }) +// +// All modifications in the object graph due to a decorator are scoped to the fx.Module it was +// specified from. func Decorate(decorators ...interface{}) Option { return decorateOption{ Targets: decorators, diff --git a/decorate_test.go b/decorate_test.go index 523118bb9..62d848adf 100644 --- a/decorate_test.go +++ b/decorate_test.go @@ -184,6 +184,35 @@ func TestDecorateSuccess(t *testing.T) { ) defer app.RequireStart().RequireStop() }) + + t.Run("decorator with optional parameter", func(t *testing.T) { + type Config struct { + Name string + } + type Logger struct { + Name string + } + type DecoratorParam struct { + fx.In + + Cfg *Config `optional:"true"` + Log *Logger + } + + app := fxtest.New(t, + fx.Provide(func() *Logger { return &Logger{Name: "log"} }), + fx.Decorate(func(p DecoratorParam) *Logger { + if p.Cfg != nil { + return &Logger{Name: p.Cfg.Name} + } + return &Logger{Name: p.Log.Name} + }), + fx.Invoke(func(l *Logger) { + assert.Equal(t, l.Name, "log") + }), + ) + defer app.RequireStart().RequireStop() + }) } func TestDecorateFailure(t *testing.T) { From 8de312c1f59f0d7ccf2a894ba3f4a5b1fc1562f6 Mon Sep 17 00:00:00 2001 From: Sung Yoon Whang Date: Wed, 9 Feb 2022 12:17:56 -0800 Subject: [PATCH 4/8] cleanup runDecorator --- decorate.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/decorate.go b/decorate.go index 2690d49a3..4ba533dd7 100644 --- a/decorate.go +++ b/decorate.go @@ -87,23 +87,21 @@ type decorator struct { Stack fxreflect.Stack } -func runDecorator(c container, d decorator, opts ...dig.DecorateOption) error { +func runDecorator(c container, d decorator, opts ...dig.DecorateOption) (err error) { decorator := d.Target - - switch decorator := decorator.(type) { - case annotated: - dcor, err := decorator.Build() + defer func() { if err != nil { - return fmt.Errorf("fx.Decorate(%v) from:\n%+vFailed: %v", decorator, d.Stack, err) + err = fmt.Errorf("fx.Decorate(%v) from:\n%+vFailed: %v", decorator, d.Stack, err) } + }() - if err := c.Decorate(dcor, opts...); err != nil { - return fmt.Errorf("fx.Decorate(%v) from:\n%+vFailed: %v", decorator, d.Stack, err) + switch decorator := decorator.(type) { + case annotated: + if dcor, err := decorator.Build(); err == nil { + err = c.Decorate(dcor, opts...) } default: - if err := c.Decorate(decorator, opts...); err != nil { - return fmt.Errorf("fx.Decorate(%v) from:\n%+vFailed: %v", decorator, d.Stack, err) - } + err = c.Decorate(decorator, opts...) } - return nil + return } From fd2dfb1acbf9f4a99eabae7576ffb08a3debe051 Mon Sep 17 00:00:00 2001 From: Sung Yoon Whang Date: Wed, 9 Feb 2022 12:22:21 -0800 Subject: [PATCH 5/8] linter --- decorate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decorate.go b/decorate.go index 4ba533dd7..6316a8205 100644 --- a/decorate.go +++ b/decorate.go @@ -97,7 +97,7 @@ func runDecorator(c container, d decorator, opts ...dig.DecorateOption) (err err switch decorator := decorator.(type) { case annotated: - if dcor, err := decorator.Build(); err == nil { + if dcor, derr := decorator.Build(); derr == nil { err = c.Decorate(dcor, opts...) } default: From aca21bf8aa8aaebf98ebbcc69c46b2d19ae8af4f Mon Sep 17 00:00:00 2001 From: Sung Yoon Whang Date: Wed, 9 Feb 2022 17:21:06 -0800 Subject: [PATCH 6/8] address code review feedback --- app_test.go | 5 +++++ decorate.go | 38 ++++++++++++++++++++++++++++++++++---- decorate_test.go | 26 +++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/app_test.go b/app_test.go index 128c53542..ad8d09582 100644 --- a/app_test.go +++ b/app_test.go @@ -1693,6 +1693,11 @@ func TestOptionString(t *testing.T) { give: Supply(Annotated{Target: bytes.NewReader(nil)}), want: "fx.Supply(*bytes.Reader)", }, + { + desc: "Decorate", + give: Decorate(bytes.NewBufferString), + want: "fx.Decorate(bytes.NewBufferString())", + }, } for _, tt := range tests { diff --git a/decorate.go b/decorate.go index 6316a8205..777887e1e 100644 --- a/decorate.go +++ b/decorate.go @@ -28,7 +28,7 @@ import ( "go.uber.org/fx/internal/fxreflect" ) -// Decorate specifies one or more decorator functions to an fx application. +// Decorate specifies one or more decorator functions to an Fx application. // Decorator functions let users augment objects in the graph. They can take in // zero or more dependencies that must be provided to the application with fx.Provide, // and produce one or more values that can be used by other invoked values. @@ -47,8 +47,38 @@ import ( // return log.Named(cfg.Name) // }) // +// Similar to fx.Provide, functions passed to fx.Decorate may optionally return an error +// as their last result. If a decorator returns a non-nil error, it will halt application startup. +// // All modifications in the object graph due to a decorator are scoped to the fx.Module it was -// specified from. +// specified from. Decorations specified in the top-level fx.New call apply across the application. +// +// Decorators can be annotated using fx.Annotate, but not with fx.Annotated. Refer to documentation +// on fx.Annotate() to learn how to use it for annotating functions. +// +// Decorators support fx.In and fx.Out structs, similar to how fx.Provide and fx.Invoke does. +// +// Decorators support value groups as well. For example, the following code shows a decorator +// which takes in a value group using fx.In struct, and returns another value group. +// +// type HandlerParam struct { +// fx.In +// +// Handlers []Handler `group:"server" +// } +// +// type HandlerResult struct { +// fx.Out +// +// Handlers []Handler `group:"server" +// } +// +// fx.New( +// // ... +// fx.Decorate(func(p HandlerParam) HandlerResult { +// // ... +// }), +// ) func Decorate(decorators ...interface{}) Option { return decorateOption{ Targets: decorators, @@ -78,9 +108,9 @@ func (o decorateOption) String() string { return fmt.Sprintf("fx.Decorate(%s)", strings.Join(items, ", ")) } -// provide is a single decorators used in Fx. +// decorator is a single decorator used in Fx. type decorator struct { - // Constructor provided to Fx. This may be an fx.Annotated. + // Decorator provided to Fx. Target interface{} // Stack trace of where this provide was made. diff --git a/decorate_test.go b/decorate_test.go index 62d848adf..edf5fd06e 100644 --- a/decorate_test.go +++ b/decorate_test.go @@ -61,7 +61,7 @@ func TestDecorateSuccess(t *testing.T) { defer app.RequireStart().RequireStop() }) - t.Run("decorate a dependency from root", func(t *testing.T) { + t.Run("decorate a dependency in child module", func(t *testing.T) { redis := fx.Module("redis", fx.Decorate(func() *Logger { return &Logger{Name: "redis"} @@ -238,6 +238,30 @@ func TestDecorateFailure(t *testing.T) { assert.Contains(t, err.Error(), "minor sadness") }) + t.Run("decorator in a nested module returns an error", func(t *testing.T) { + type Logger struct { + Name string + } + + app := NewForTest(t, + fx.Provide(func() *Logger { + return &Logger{Name: "root"} + }), + fx.Module("child", + fx.Decorate(func(l *Logger) (*Logger, error) { + return &Logger{Name: l.Name + "decorated"}, errors.New("minor sadness") + }), + fx.Invoke(func(l *Logger) { + assert.Fail(t, "this should not be executed") + }), + ), + ) + + err := app.Err() + require.Error(t, err) + assert.Contains(t, err.Error(), "minor sadness") + }) + t.Run("decorate the same type twice from the same Module", func(t *testing.T) { type Logger struct { Name string From 3d678d38960dd69b73652df868d96ddb01f26439 Mon Sep 17 00:00:00 2001 From: Sung Yoon Whang Date: Wed, 9 Feb 2022 23:39:12 -0800 Subject: [PATCH 7/8] Apply suggestions from code review Co-authored-by: r-hang <42982339+r-hang@users.noreply.github.com> --- decorate.go | 2 +- decorate_test.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/decorate.go b/decorate.go index 777887e1e..1e863d391 100644 --- a/decorate.go +++ b/decorate.go @@ -56,7 +56,7 @@ import ( // Decorators can be annotated using fx.Annotate, but not with fx.Annotated. Refer to documentation // on fx.Annotate() to learn how to use it for annotating functions. // -// Decorators support fx.In and fx.Out structs, similar to how fx.Provide and fx.Invoke does. +// Decorators support fx.In and fx.Out structs, similar to fx.Provide and fx.Invoke. // // Decorators support value groups as well. For example, the following code shows a decorator // which takes in a value group using fx.In struct, and returns another value group. diff --git a/decorate_test.go b/decorate_test.go index edf5fd06e..6f3238f56 100644 --- a/decorate_test.go +++ b/decorate_test.go @@ -35,7 +35,7 @@ func TestDecorateSuccess(t *testing.T) { Name string } - t.Run("decorate something from Module", func(t *testing.T) { + t.Run("objects provided by other modules are decorated", func(t *testing.T) { redis := fx.Module("redis", fx.Provide(func() *Logger { return &Logger{Name: "redis"} @@ -61,7 +61,7 @@ func TestDecorateSuccess(t *testing.T) { defer app.RequireStart().RequireStop() }) - t.Run("decorate a dependency in child module", func(t *testing.T) { + t.Run("objects in child modules are decorated.", func(t *testing.T) { redis := fx.Module("redis", fx.Decorate(func() *Logger { return &Logger{Name: "redis"} @@ -80,7 +80,7 @@ func TestDecorateSuccess(t *testing.T) { defer app.RequireStart().RequireStop() }) - t.Run("use Decorate in root", func(t *testing.T) { + t.Run("root decoration applies to all modules", func(t *testing.T) { redis := fx.Module("redis", fx.Invoke(func(l *Logger) { assert.Equal(t, "decorated logger", l.Name) @@ -262,7 +262,7 @@ func TestDecorateFailure(t *testing.T) { assert.Contains(t, err.Error(), "minor sadness") }) - t.Run("decorate the same type twice from the same Module", func(t *testing.T) { + t.Run("decorating a type more than once in the same Module errors", func(t *testing.T) { type Logger struct { Name string } @@ -307,7 +307,7 @@ func TestDecorateFailure(t *testing.T) { assert.Contains(t, err.Error(), "major sadness") }) - t.Run("decorator missing a dependency", func(t *testing.T) { + t.Run("all decorator dependencies must be provided", func(t *testing.T) { type Logger struct { Name string } @@ -332,7 +332,7 @@ func TestDecorateFailure(t *testing.T) { assert.Contains(t, err.Error(), "missing dependencies") }) - t.Run("use a decorator like a provider", func(t *testing.T) { + t.Run("decorate cannot provide a non-existent type", func(t *testing.T) { type Logger struct { Name string } From 5a386a07ccc9264d44773ad556279cf994e6a2c9 Mon Sep 17 00:00:00 2001 From: Sung Yoon Whang Date: Wed, 9 Feb 2022 23:46:04 -0800 Subject: [PATCH 8/8] coverage --- decorate_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/decorate_test.go b/decorate_test.go index 6f3238f56..cf3f49446 100644 --- a/decorate_test.go +++ b/decorate_test.go @@ -248,8 +248,11 @@ func TestDecorateFailure(t *testing.T) { return &Logger{Name: "root"} }), fx.Module("child", - fx.Decorate(func(l *Logger) (*Logger, error) { - return &Logger{Name: l.Name + "decorated"}, errors.New("minor sadness") + fx.Decorate(func(l *Logger) *Logger { + return &Logger{Name: l.Name + "decorated"} + }), + fx.Decorate(func(l *Logger) *Logger { + return &Logger{Name: l.Name + "decorated"} }), fx.Invoke(func(l *Logger) { assert.Fail(t, "this should not be executed") @@ -259,7 +262,7 @@ func TestDecorateFailure(t *testing.T) { err := app.Err() require.Error(t, err) - assert.Contains(t, err.Error(), "minor sadness") + assert.Contains(t, err.Error(), "*fx_test.Logger already decorated") }) t.Run("decorating a type more than once in the same Module errors", func(t *testing.T) {