Impossible to inject struct that is part of a group by itself
Closed this issue · 2 comments
Describe the bug
When trying to inject a struct that is part of a group (tagged via ResultTags
) singularily, fx
cannot find the requested dependency. Let's say we have two interfaces: GameFetcher
and UserFetcher
, both embedding the Fetcher
interface.
Both implementations are provided as part of the fetchers
group, annotated with both interfaces:
func AsFetcher(f any, fetcherType any) any {
return fx.Annotate(
f,
fx.As(new(Fetcher)),
fx.As(fetcherType),
fx.ResultTags(`group:"fetchers"`),
)
}
...
fx.Provide(
AsFetcher(newGameFetcher, new(GameFetcher)),
AsFetcher(newUserFetcher, new(UserFetcher)),
)
I am not sure whether this is a bug or whether I'm doing things wrong, but I'd appreciate some guidance.
To Reproduce
https://gist.github.com/giovannizotta/d74a5ccfa28208ad2f582590d10b605c
package main
import (
"go.uber.org/fx"
)
type Fetcher interface{}
type GameFetcher interface {
Fetcher
}
type UserFetcher interface {
Fetcher
}
type GameFetcherImpl struct{}
type UserFetcherImpl struct{}
func newUserFetcher() *UserFetcherImpl {
return &UserFetcherImpl{}
}
func newGameFetcher() *GameFetcherImpl {
return &GameFetcherImpl{}
}
type StructUsingAllFetchers struct {
fetchers []Fetcher
}
func NewStructUsingAllFetchers(fetchers []Fetcher) *StructUsingAllFetchers {
return &StructUsingAllFetchers{
fetchers: fetchers,
}
}
func AsFetcher(f any, fetcherType any) any {
return fx.Annotate(
f,
fx.As(new(Fetcher)),
fx.As(fetcherType),
fx.ResultTags(`group:"fetchers"`),
)
}
func main() {
fx.New(
fx.Provide(
AsFetcher(newGameFetcher, new(GameFetcher)),
AsFetcher(newUserFetcher, new(UserFetcher)),
fx.Annotate(
NewStructUsingAllFetchers,
fx.ParamTags(`group:"fetchers"`),
),
),
fx.Invoke(func(s *StructUsingAllFetchers) {}),
fx.Invoke(func(gameFetcher GameFetcher) {}),
).Run()
}
Output:
[Fx] PROVIDE main.Fetcher[group = "fetchers"] <= fx.Annotate(main.newGameFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.GameFetcher]])
[Fx] PROVIDE main.GameFetcher[group = "fetchers"] <= fx.Annotate(main.newGameFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.GameFetcher]])
[Fx] PROVIDE main.Fetcher[group = "fetchers"] <= fx.Annotate(main.newUserFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.UserFetcher]])
[Fx] PROVIDE main.UserFetcher[group = "fetchers"] <= fx.Annotate(main.newUserFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.UserFetcher]])
[Fx] PROVIDE *main.StructUsingAllFetchers <= fx.Annotate(main.NewStructUsingAllFetchers(), fx.ParamTags(["group:\"fetchers\""])
[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] INVOKE main.main.func1()
[Fx] RUN provide: fx.Annotate(main.newGameFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.GameFetcher]])
[Fx] RUN provide: fx.Annotate(main.newUserFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.UserFetcher]])
[Fx] RUN provide: fx.Annotate(main.NewStructUsingAllFetchers(), fx.ParamTags(["group:\"fetchers\""])
[Fx] INVOKE main.main.func2()
[Fx] ERROR fx.Invoke(main.main.func2()) called from:
main.main
main.go:59
runtime.main
/opt/homebrew/Cellar/go/1.21.5/libexec/src/runtime/proc.go:267
Failed: missing dependencies for function "main".main.func2
main.go:59:
missing type:
- main.GameFetcher (did you mean to use one of *main.StructUsingAllFetchers, fx.DotGraph, fx.Lifecycle, or fx.Shutdowner?)
[Fx] ERROR Failed to start: missing dependencies for function "main".main.func2
main.go:59:
missing type:
- main.GameFetcher (did you mean to use one of *main.StructUsingAllFetchers, fx.DotGraph, fx.Lifecycle, or fx.Shutdowner?)
exit status 1
Expected behavior
I would expect the second invoked function to be injected with the GameFetcher
, since fx provided it earlier:
[Fx] PROVIDE main.GameFetcher[group = "fetchers"] <= fx.Annotate(main.newGameFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.GameFetcher]])
Additional context
I found it possible to achieve what I describe in another way, by injecting a slice of the specific elements []GameFetcher
:
package main
import (
"go.uber.org/fx"
)
type Fetcher interface {
Fetch()
}
type GameFetcher interface {
Fetcher
}
type UserFetcher interface {
Fetcher
}
type GameFetcherImpl struct{}
type UserFetcherImpl struct{}
func (g *GameFetcherImpl) Fetch() {}
func (u *UserFetcherImpl) Fetch() {}
func newUserFetcher() *UserFetcherImpl {
return &UserFetcherImpl{}
}
func newGameFetcher() *GameFetcherImpl {
return &GameFetcherImpl{}
}
type StructUsingAllFetchers struct {
fetchers []Fetcher
}
func NewStructUsingAllFetchers(fetchers []Fetcher) *StructUsingAllFetchers {
return &StructUsingAllFetchers{
fetchers: fetchers,
}
}
type StructUsingSpecificFetcher struct {
gameFetcher GameFetcher
}
func NewStructUsingSpecificFetcher(f []GameFetcher) *StructUsingSpecificFetcher {
if len(f) != 1 {
panic("expected 1 fetcher")
}
return &StructUsingSpecificFetcher{
gameFetcher: f[0],
}
}
func AsFetcher(f any, fetcherType any) any {
return fx.Annotate(
f,
fx.As(new(Fetcher)),
fx.As(fetcherType),
fx.ResultTags(`group:"fetchers"`),
)
}
func main() {
fx.New(
fx.Provide(
AsFetcher(newGameFetcher, new(GameFetcher)),
AsFetcher(newUserFetcher, new(UserFetcher)),
fx.Annotate(
NewStructUsingAllFetchers,
fx.ParamTags(`group:"fetchers"`),
),
fx.Annotate(
NewStructUsingSpecificFetcher,
fx.ParamTags(`group:"fetchers"`),
),
),
fx.Invoke(func(s *StructUsingAllFetchers) {}),
fx.Invoke(func(s *StructUsingSpecificFetcher) {}),
).Run()
}
However, I find this undesirable because I know there will always be one element in the slice, even though I am forced to provide a slice of GameFetcher
. It could very well be that I'm doing things wrong and I'm missing something, please let me know if there is a better way to do this!
Hey @giovannizotta, thanks for the issue!
Other maintainers please correct me if I'm wrong but: I think the problem is that from Fx's perspective, types annotated with group or name tags are considered their own distinct types. Looking at the logs from the example you gave,
[Fx] PROVIDE main.Fetcher[group = "fetchers"] <= fx.Annotate(main.newGameFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.GameFetcher]])
[Fx] PROVIDE main.GameFetcher[group = "fetchers"] <= fx.Annotate(main.newGameFetcher(), fx.ResultTags(["group:\"fetchers\""]), fx.As([[main.Fetcher] [main.GameFetcher]])
The current code is providing the result of newGameFetcher
into the group of Fetcher
s named fetchers
(main.Fetcher[group = "fetchers"]
), and another group of GameFetcher
s, also named fetchers
(main.GameFetcher[group = "fetchers"]
). Notably, the result is not being provided as a raw (unannotated) GameFetcher
, which is what is requested by fx.Invoke(func(gameFetcher GameFetcher) {})
. Because of this, Fx fails.
The way to get around this right now is to provide the same instance but both annotated w/ the group tag and unannotated. For example, by returning a result struct from the fetcher constructors like:
type GameFetcherResult struct {
fx.Out
GameFetcher GameFetcher
Fetcher Fetcher `group:"fetchers"`
}
func newGameFetcher() GameFetcherResult {
res := &GameFetcherImpl{}
return GameFetcherResult{
GameFetcher: res,
Fetcher: res,
}
}
(runnable/error-free example: https://go.dev/play/p/2Mh7-kVqP10)
As a side note, this is somewhat related to these issues - #998, #1036 - in that Fx does not support a clean way to both name an instance and place it in a group, and be able to depend elsewhere on either the individual object or the group. There is discussion/WIP to allow annotating a type w/ both a name and a group tag, and be able to retrieve either the entire group, or one specific item (uber-go/dig#381). With this, you could annotate the result of newGameFetcher w/ something like:
fx.Annotate(
newGameFetcher,
fx.As(new(Fetcher)),
fx.ResultTags(`group:"fetchers" name:"gamefetcher"`),
)
and then be able to get both the group and the individual game fetcher.
Thank you @JacobOaks the quick response and for the explanation!
I think the problem is that from Fx's perspective, types annotated with group or name tags are considered their own distinct types
This is indeed what I was missing. The rest all makes sense, thanks for the information, I'll stay on the lookout for a better way to achieve this in the future!