uber-go/fx

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 Fetchers named fetchers (main.Fetcher[group = "fetchers"]), and another group of GameFetchers, 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!