uber-go/fx

Application fails to start with fx.Annotate + fx.As + fx.In + fx.Out

noam-ma-ma opened this issue · 2 comments

Describe the bug
Application fails to start when using fx.In + fx.Out parameters with fx.Annotate and fx.As with error of fx.In structs cannot be annotated.

I've tried to use fx.Annotate(ProvideConcreteType, fx.As(new(interface)), but when using fx.In and fx.Out the error occurs.

To overcome this - I've used fx.ParamTags, but from what I read from the documentation - it should be working both ways.

fx.Annotate(
		CreateGreaterWorks,
		fx.ParamTags(`name:"helloTo"`),
		fx.As(new(Greater))),

To Reproduce
This code snippet shows 2 working applications and a failed one (the current bug) -

  1. fx.In + fx.ParamTags and interface (works)
  2. fx.In + fx.Out without interface (works)
  3. fx.In + fx.Out with interface (doesn't work)
package main

import (
	"context"
	"fmt"
	"go.uber.org/fx"
	"time"
)

type Greater interface {
	SayHello()
}

type ConcreteGreater struct {
	helloTo string
}

func (c ConcreteGreater) SayHello() {
	fmt.Printf("Hello %s", c.helloTo)
}

type Params struct {
	fx.In
	HelloTo string `name:"helloTo"`
}

func CreateGreater(params Params) ConcreteGreater {
	return ConcreteGreater{helloTo: params.HelloTo}
}

func CreateGreaterWorks(helloTo string) ConcreteGreater {
	return ConcreteGreater{helloTo: helloTo}
}

type OutParams struct {
	fx.Out
	HelloTo string `name:"helloTo"`
}

func NewParams() OutParams {
	return OutParams{HelloTo: "world"}
}

func main() {
	appWorks := fx.New(
		fx.Provide(
			NewParams,
			fx.Annotate(
				CreateGreaterWorks,
				fx.ParamTags(`name:"helloTo"`),
				fx.As(new(Greater))),
		),
		fx.Invoke(func(greater Greater) { greater.SayHello() }),
	)

	if err := appWorks.Start(context.Background()); err != nil {
		panic("failed to start application")
	}
	defer appWorks.Stop(context.Background())
	fmt.Printf("Started application with ParamTags")
	time.Sleep(time.Second * 3)

	appWorksWithoutInterface := fx.New(
		fx.Provide(
			NewParams,
			CreateGreater,
		),
		fx.Invoke(func(greater ConcreteGreater) { greater.SayHello() }),
	)
	if err := appWorksWithoutInterface.Start(context.Background()); err != nil {
		panic("failed to application with fx.In params")
	}
	defer appWorksWithoutInterface.Stop(context.Background())
	fmt.Printf("Started application with fx.In and fx.Out without interface")
	time.Sleep(time.Second * 3)

	appFail := fx.New(
		fx.Provide(
			NewParams,
			fx.Annotate(CreateGreater, fx.As(new(Greater))),
		),
		fx.Invoke(func(greater Greater) { greater.SayHello() }),
	)
	if err := appFail.Start(context.Background()); err != nil {
		panic("failed to application with fx.In + fx.Out params with interface")
	}
	defer appFail.Stop(context.Background())
}

Expected behavior
The application with fx.In + fx.Out and fx.As works the same as fx.ParamTags, as describe in the documentation of fx.Annotate:

image

Additional context
Thanks!

Hi @noam-ma-ma, thanks for reach out! It seems like the error you're running into is the actual expected behavior of fx. fx.Annotate is meant to be an alternative way of declaring values with annotations without having to wrap them up in a separate structure.

I too encountered this issue where fx.Annotate() cannot be used on a constructor which takes a fx.In params struct, and I would argue that this is unexpected behavior from a developer's perspective.

For example, consider the case where I have a constructor that returns a concrete type instead of an interface (as is the best practice in golang), and I want to use fx.As() to provide the interface. This call to fx.Annotate() fails if the constructor happens to take a params struct that embeds fx.In:

fx.Provide(fx.Annotate(constructorThatTakesFxInParams, fx.As(new(Interface)))),

I can understand why this is failing, due to the way fx.Annotate() is implemented, but the result is that legitimate looking usages of the API fail unexpectedly.