google / wire

Compile-time Dependency Injection for Go
Apache License 2.0
13.14k stars 625 forks source link

Support for request scoped DI code generation #389

Closed moredure closed 1 year ago

moredure commented 1 year ago

Hi Wire authors/contributors @zombiezen! How about implement smth like this. Do you have smth like this in roadmap/plans?

package main

import (
    "fmt"
    "net/http"
)

import "github.com/google/wire"

type WebControllerInterface interface {
    Get(w http.ResponseWriter, r *http.Request)
}
func NewDB() *DB
func NewHTTPClient() *HTTPClient
func NewServer(a WebControllerInterface, db *DB) *SomePopularWebserver {
    a = &SomePopularWebserver{}
    a.Middleware(func(f func(w http.ResponseWriter, r *http.Request)) {
        // db create transaction
                 f(w, r.WithContext(context.WithValue(r.Context(), "tx", tx)))
        // will be clear when to stop with 
        // db transaction.Commit/Rollback 
    })
    a.GET("/", a.Get)
    a.POST("/", a.Post) // other methods
    a.PUT("/", a.Put) // other methods
    return
}
// web controller which methods work inside transaction scope
func NewWebController(ModelRepositoryInterface, *Logger, *HTTPClient) *Controller
// repo with request scope transaction
func NewModelRepository(*Transaction) *ModelRepository
// request scope transaction
func NewTransaction(*http.Request) (*Transaction)
// get some headers and create logger probably some parent logger injection will take place as well
func NewLogger(*http.Request) (*Logger)

func InitializeServer() Server {
    var scoped = wire.Scoped(NewTransaction, NewLogger, NewModelRepository)
    wire.Build(
        NewDB,
        NewServer,
                 NewHTTPClient,
        wire.Prototype(NewWebController,
            wire.As(WebControllerInterface), scoped,
            // wire.Prototype(NewWebController2,
            //  wire.As(WebControllerInterface2), scoped),
        ),
        )
    return Server{}
}

// which results after wire CLI into NewServer injected with custom WebControllerInterface implementation constructed with singleton scoped dependencies
// where each method is factory to produce NewWebController and execute matching method 
// like:

// constructed by wire
type DecoratedWebController struct {
    client *HTTPClient
}

func (d *DecoratedWebController) Get(w http.ResponseWriter, r *http.Request) {
    l := NewLogger(r) // in case of NewLogger() (*logger, error) just panic on error?
    t := NewTransaction(r)
    mr := NewModelRepository(t)
    c := NewWebController(mr, l, d.client)
    c.Get(w, r)
       // later execute cleanup functions like in wire
}

// highly likely inject only instances which not provided by Prototype scope constructors results or wrapped by them like dig.Decorate
func NewDecoratedWebController(c *HTTPClient) *DecoratedWebController {
    return &DecoratedWebController{c}
}

// and finally smth like this after wire CLI:

func InitializeServer() Server {
    db := NewDB()
    c := NewHTTPClient()
    dc := NewDecoratedWebController(c)
    return NewServer(dc, db)
}

In fact I though about building some kind of wire preprocessor to construct such NewDecoratedWebController and later regenerate code once more with wire. Is it make sense or do you think such a thing will be hard to customize accross frameworks which have some kind of request/response functionality/hard to check errors on cleanup/ etc ?

zombiezen commented 1 year ago

We intentionally chose to omit scoping to reduce complexity. See this comment for background.

moredure commented 1 year ago

aha, I see, thanks for feedback.