uber-go / fx

A dependency injection based application framework for Go.
https://uber-go.github.io/fx/
MIT License
5.48k stars 283 forks source link

Ability to use fx.As and fx.ResultTags for the same Provider #1214

Open Dan6erbond opened 1 month ago

Dan6erbond commented 1 month ago

Is your feature request related to a problem? Please describe.

When annotating a provider function with fx.As() and fx.ResultTags() at the same time, Fx fails with an error:

2024-06-12T08:18:56.009Z        ERROR   fxevent/zap.go:59       start failed    {"error": "missing dependencies for function \"gitea.example.com/myproject/http/httpfx\".ConfigureServer (/workspaces/myproject/http/httpfx/module.go:20): missing type: http.Handler", "errorVerbose": "missing dependencies for function \"gitea.example.com/myproject/http/httpfx\".ConfigureServer\n\t/workspaces/carbase-backend/http/httpfx/module.go:20:\nmissing type:\n\t- http.Handler (did you mean to Provide it?)"}

Describe the solution you'd like A clear and concise description of what you want to happen.

It would be great if I could do this:

var Module = fx.Module("chi",
    fx.Provide(
        fx.Annotate(
            NewRouter,
            fx.As(new(http.Handler)),
            fx.As(new(chi.Router)),
            fx.ResultTags(`name:"router"`),
        ),
    ),
)

Allowing parts of my application to expect the router while my core bootstrapping logic isn't coupled to Chi in any way and just accepts a router.

Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered.

At the moment I just ensure only one http.Handler is available in the DI.

Is this a breaking change? We do not accept breaking changes to the existing API. Please consider if your proposed solution is backwards compatible. If not, we can help you make it backwards compatible, but this must be considered when we consider new features.

I don't think so.

Additional context Add any other context or screenshots about the feature request here.

The code above should give a good overview of what I'm trying to achieve, as I want to decouple my HTTP server from whichever router I end up using and just attach the http.Handler to it.

JacobOaks commented 3 weeks ago

Hey @Dan6erbond thanks for the issue.

With the way that ResultTags works, the code you showed is applying the router name to both the http.Handler and the chi.Router. Named types are considered entirely separate from their unnamed counterparts from Fx's perspective. That's why you see Fx complaining about not having an unnamed http.Handler:

missing dependencies <...> missing type: http.Handler.

For now, you'll have to workaround this by either doing two returns and annotating them independently like:

func NewRouter() (chi.Router, http.Handler) {
    /* logic to create the router */
    return router, router
}

fx.Provide(
    fx.Annotate(
        NewRouter,
        fx.ResultTags(`name:"router"`),
    )
)

Or if you can't change NewRouter, you can sort of manually annotate using "constructors" to add the 2nd representation.

fx.Provide(
    NewRouter, // let's say this returns a *routerImpl
    func(r *routerImpl) http.Handler { return r },
    fx.Annotate(
        func(r *routerImpl) chi.Router { return r },
        fx.ResultTags(`name:"router"`),
    ),
)

This does unfortunately expose the underlying type to the container. If you can provide the exact definitions of chiRouter and the return types of NewRouter I can help you with an example to avoid this if that's an isue.

This is admittedly not very ergonomic and is an unfortunate consequence of how annotations work, which is why #899 is on our radar. Nested annotations would help a little bit allowing you to do something like this to keep the handler unnamed but the router named:

fx.Provide(
    fx.Annotate(
        fx.Annotate(
            NewRouter,
            fx.As(new(http.Handler)),
        ),
    fx.As(new(chi.Router)),
    fx.ResultTags(`name:"router"`),
)