Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Impossible to inject struct that is part of a group by itself #1181

Closed
giovannizotta opened this issue Mar 21, 2024 · 2 comments
Closed

Impossible to inject struct that is part of a group by itself #1181

giovannizotta opened this issue Mar 21, 2024 · 2 comments

Comments

@giovannizotta
Copy link

giovannizotta commented Mar 21, 2024

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!

@JacobOaks
Copy link
Contributor

JacobOaks commented Mar 22, 2024

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.

@giovannizotta
Copy link
Author

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!

@uber-go uber-go locked and limited conversation to collaborators Apr 2, 2024
@JacobOaks JacobOaks converted this issue into discussion #1184 Apr 2, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Development

No branches or pull requests

2 participants