Skip to content

Commit

Permalink
Fix decorator not being applied to transient dependencies (uber-go#941)
Browse files Browse the repository at this point in the history
There is a bug that only manifests when decorating objects in Fx
where transient dependency types are not properly decorated. For
example, if type A depends on type B, and type B depends on type C
which has a decorator, it fails to recognize that.

This only occurs when all constructors and the decorators for all
these types are provided at the "root" level fx.App.

This happens because fx injects a fake "root" Scope between the
actual root Container (which is where constructors are provided to
by default).

Fixed by making fx stop create this fake root Scope and use the
dig.Container directly.

Fixes uber-go#940.
  • Loading branch information
sywhang committed Oct 11, 2022
1 parent 11939cd commit dfe3ea3
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 4 deletions.
33 changes: 33 additions & 0 deletions decorate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,39 @@ func TestDecorateSuccess(t *testing.T) {
)
defer app.RequireStart().RequireStop()
})

t.Run("decoration must execute when required by a member of group", func(t *testing.T) {
type Drinks interface {
}
type Coffee struct {
Type string
Name string
Price int
}
type PriceService struct {
DefaultPrice int
}
app := fxtest.New(t,
fx.Provide(func() *PriceService {
return &PriceService{DefaultPrice: 3}
}),
fx.Decorate(func(service *PriceService) *PriceService {
service.DefaultPrice = 10
return service
}),
fx.Provide(fx.Annotate(func(service *PriceService) Drinks {
assert.Equal(t, 10, service.DefaultPrice)
return &Coffee{Type: "coffee", Name: "Americano", Price: service.DefaultPrice}
}, fx.ResultTags(`group:"drinks"`))),
fx.Provide(fx.Annotated{Group: "drinks", Target: func() Drinks {
return &Coffee{Type: "coffee", Name: "Cold Brew", Price: 4}
}}),
fx.Invoke(fx.Annotate(func(drinks []Drinks) {
assert.Len(t, drinks, 2)
}, fx.ParamTags(`group:"drinks"`))),
)
defer app.RequireStart().RequireStop()
})
}

func TestDecorateFailure(t *testing.T) {
Expand Down
17 changes: 13 additions & 4 deletions module.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,32 @@ func (o moduleOption) apply(mod *module) {
type module struct {
parent *module
name string
scope *dig.Scope
scope scope
provides []provide
invokes []invoke
decorators []decorator
modules []*module
app *App
}

// scope is a private wrapper interface for dig.Container and dig.Scope.
// We can consider moving this into Fx using type constraints after Go 1.20
// is released and 1.17 is deprecated.
type scope interface {
Decorate(f interface{}, opts ...dig.DecorateOption) error
Invoke(f interface{}, opts ...dig.InvokeOption) error
Provide(f interface{}, opts ...dig.ProvideOption) error
Scope(name string, opts ...dig.ScopeOption) *dig.Scope
String() string
}

// builds the Scopes using the App's Container. Note that this happens
// after applyModules' are called because the App's Container needs to
// be built for any Scopes to be initialized, and applys' should be called
// before the Container can get initialized.
func (m *module) build(app *App, root *dig.Container) {
if m.parent == nil {
m.scope = root.Scope(m.name)
// TODO: Once fx.Decorate is in-place,
// use the root container instead of subscope.
m.scope = root
} else {
parentScope := m.parent.scope
m.scope = parentScope.Scope(m.name)
Expand Down

0 comments on commit dfe3ea3

Please sign in to comment.