fx.Decorate does not add a missing object to the module
Closed this issue · 3 comments
Describe the bug
If I add an fx.Decorate
to my module that returns a type that doesn't exist in the container otherwise, the value isn't made available to the module.
To Reproduce
- Define a type and a constructor for it that returns named values only:
type DBConn struct{ name string }
// Note that a naked DBConn will not be added to the container.
// Only named variants.
func NewDB() (out struct {
fx.Out
RW *DBConn `name:"rw"`
RO *DBConn `name:"ro"`
}) {
out.RW = &DBConn{name: "rw"}
out.RO = &DBConn{name: "ro"}
return
}
- Add helper functions that a module can use to pick from one of the named variants.
// UseRW opts the current module to use the rw connection.
func UseRW() fx.Option {
return fx.Decorate(func(i struct {
fx.In
RW *DBConn `name:"rw"`
}) *DBConn {
return i.RW
})
}
// UseRO opts the current module to use the ro connection.
func UseRO() fx.Option {
return fx.Decorate(func(i struct {
fx.In
RO *DBConn `name:"ro"`
}) *DBConn {
return i.RO
})
}
- Attempt to use these in main.
func main() {
fx.New(
fx.Provide(
NewDB,
),
fx.Module("uses-rw",
UseRW(),
fx.Invoke(func(conn *DBConn) {
println("uses-rw:", conn.name)
}),
),
fx.Module("uses-ro",
UseRO(),
fx.Invoke(func(conn *DBConn) {
println("uses-ro:", conn.name)
}),
),
).Run()
}
Full program
package main
import "go.uber.org/fx"
type DBConn struct{ name string }
// Note that a naked DBConn will not be added to the container.
// Only named variants.
func NewDB() (out struct {
fx.Out
RW *DBConn `name:"rw"`
RO *DBConn `name:"ro"`
}) {
out.RW = &DBConn{name: "rw"}
out.RO = &DBConn{name: "ro"}
return
}
// Helpers that use fx.Decorate to pick one of the named variants
// but only within the current module's scope.
// UseRW opts the current module to use the rw connection.
func UseRW() fx.Option {
return fx.Decorate(func(i struct {
fx.In
RW *DBConn `name:"rw"`
}) *DBConn {
return i.RW
})
}
// UseRO opts the current module to use the ro connection.
func UseRO() fx.Option {
return fx.Decorate(func(i struct {
fx.In
RO *DBConn `name:"ro"`
}) *DBConn {
return i.RO
})
}
func main() {
fx.New(
fx.Provide(
NewDB,
),
fx.Module("uses-rw",
UseRW(),
fx.Invoke(func(conn *DBConn) {
println("uses-rw:", conn.name)
}),
),
fx.Module("uses-ro",
UseRO(),
fx.Invoke(func(conn *DBConn) {
println("uses-ro:", conn.name)
}),
),
).Run()
}
Running this fails with the error:
% go run .
[Fx] PROVIDE *main.DBConn[name = "rw"] <= main.NewDB()
[Fx] PROVIDE *main.DBConn[name = "ro"] <= main.NewDB()
[Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] DECORATE *main.DBConn <= main.UseRW.func1() from module "uses-rw"
[Fx] DECORATE *main.DBConn <= main.UseRO.func1() from module "uses-ro"
[Fx] INVOKE main.main.func1() from module "uses-rw"
[Fx] ERROR fx.Invoke(main.main.func1()) called from:
main.main
[..]/fx-decorate-bug/main.go:53
runtime.main
[..]/go/1.21.5/libexec/src/runtime/proc.go:267
Failed: missing dependencies for function "main".main.func1
[..]/fx-decorate-bug/main.go:53:
missing type:
- *main.DBConn (did you mean to Provide it?)
[Fx] ERROR Failed to start: missing dependencies for function "main".main.func1
[..]/fx-decorate-bug/main.go:53:
missing type:
- *main.DBConn (did you mean to Provide it?)
exit status 1
Expected behavior
The program should run successfully.
Additional context
To get the desired behavior, a placeholder value is needed in the container.
This works, for example:
func main() {
fx.New(
+ fx.Supply(&DBConn{name: "invalid"}),
fx.Provide(
Library versions:
go.uber.org/fx v1.20.1
go.uber.org/dig v1.17.1
I don't think this is intended behavior, but I could be wrong.
@abhinav this is the intended design for fx.Decorate. There's a test asserting this behavior also: https://github.com/uber-go/fx/blob/master/decorate_test.go#L471
Thinking about it, that probably means we didn't document this behavior properly. Let me fix that.