uber-go/fx

Annotated structs cannot be used to resolve interfaces

thefuga opened this issue · 4 comments

Currently, FX offers some limitations when resolving interfaces to implementations. This brings complications when following the concept of accepting interfaces (and defining them on the client's side) and returning values.
Given two similar interfaces to be bound to the same struct, what I usually do to overcome this is to make a wrapper constructor to resolve structs to the interface. Such as:

package foo

type interface Performer{
    Perform()
}

type Foo{
    performer Performer
}

func NewFoo(p Performer) *Foo {
    return &Foo{performer: p}
}
package bar

type interface Performer{
    Perform()
}

type Bar{
    performer Performer
}

func NewBar(p Performer) *Bar {
    return &Bar{performer: p}
}
package performer

type struct Performer{}

func (Performer) Perform() {}

func NewPerformer() *Performer {
    return &Performer{}
}

And eventually in a base package:

fx.Provide(
    performer.NewPerformer()
    func(p *performer.Performer) foo.Performer { return p }
    func(p *performer.Performer) bar.Performer { return p }
)

By doing this, I can effectively bind several interfaces to the same (or multiple) implementations. It's not that bad but has the drawback of having to redefine these constructors for every interface.
I've been using FX for a while doing this. But lately, I was reworking some of my providers and decided to try fx.In, fx.Out, and fx.Annotaed.
It would be perfect to bind these interfaces based on their names. NestJS does something similar.
I'm not sure if the lack of support for this is a limitation or is by design, especially because of the fx.In docs, it says "Note that both the name AND type of the fields on the parameter struct must match the corresponding result struct.".

Anyway, would you be open to a PR to change this to allow named values to be injected based solely on the name and interface, and not on the actual type?

Hi there! We have a couple unreleased changes in flight that would help here.

In #757, we added a new fx.Anntotate API which first allows a more convenient way of using fx.Annotated:

You will now be able to do,

fx.Annotate(performer.NewPerformer, fx.ParamTags(`name:"foo"`))

That alone doesn't solve your problem.
However, with #795, which is still in flight, you will be able to do:

fx.Annotate(performer.NewPerformer, fx.As(new(foo.Performer)))

Would that be helpful?

We're hoping to land #795 and release these changes this week.

Before writing this issue I've seen #795 (which I really appreciated, even though not directly related to what I was looking for), but it seemed like a solution to provide multiple implementations to the same interface based on the client, while what I was looking for was to provide the same implementation to similar (or equal) interfaces defined in different places. Would this along with #757 allow me to "bypass" the type checking?
Just to be clear, right now what happens when using Annotated along with an input parameter is an error saying that foo.Performer doesn't have the same type as performer.Performer. Well, in fact, they are not the same type, even though performer.Performer implements foo.Performer and bar.Performer. This strict type-checking is what limits the use I was trying to acheive.

@thefuga Yes, as long as performer.Performer is an implementation of foo.Performer and bar.Performer, you should be able to do:

func newPerformer(...) performer.Performer
fx.Provide(
  fx.Annotate(newPerformer, fx.As(foo.Performer))
  fx.Annotate(newPerformer, fx.As(bar.Performer))
)

with #795

Awesome, @sywhang! This will make resolving interfaces perfect. Definitely does what I was needing. Thank you, guys!