diff --git a/README.md b/README.md index af5879c..ca10ed9 100644 --- a/README.md +++ b/README.md @@ -8,23 +8,23 @@ ## Description -Package izidic defines a tiny dependency injection container for Go projects. +Package [izidic](https://github.com/fgm/izidic) defines a tiny dependency injection container for Go projects. That container can hold two different kinds of data: - parameters, which are mutable data without any dependency; -- services, which are functions providing a typed object providing a feature, +- services, which are functions returning a typed object providing a feature, and may depend on other services and parameters. The basic feature is that storing service definitions does not create instances, -allowing users to store definitions of services requiring other services +allowing users to store definitions of services requiring other services, before those are actually defined. Notice that parameters do not need to be primitive types. For instance, most applications are likely to store a `stdout` object with value `os.Stdout`. Unlike heavyweights like google/wire or uber/zap, it works as a single step, -explicit, process, without reflection or code generation, to keep everything in sight. +explicit process, without reflection or code generation, to keep everything in sight. ## Usage @@ -54,7 +54,7 @@ Parameters can be any value type. They can be stored in the container in any ord Services like `loggerService` in the previous example are instances ot the `Service` type, which is defined as: -`type Service func(*Container) (any, error)` +`type Service func(Container) (any, error)` - Services can use any other service and parameters to return the instance they build. The only restriction is that cycles are not supported. @@ -94,54 +94,56 @@ as in this example: package di import ( - "io" - "log" + "io" + "log" - "github.com/fgm/izidic" + "github.com/fgm/izidic" ) -type container struct { - *izidic.Container +type Container struct { + izidic.Container } // Logger is a typed service accessor. -func (c *container) Logger() *log.Logger { - return c.MustService("logger").(*log.Logger) +func (c *Container) Logger() *log.Logger { + return c.MustService("logger").(*log.Logger) } // Name is a types parameter accessor. -func (c *container) Name() string { - return c.MustParam("name").(string) +func (c *Container) Name() string { + return c.MustParam("name").(string) } // loggerService is an izidic.Service also containing a one-time initialization action. -func loggerService(dic *izidic.Container) (any, error) { - w := dic.MustParam("writer").(io.Writer) - log.SetOutput(w) // Support dependency code not taking an injected logger. - logger := log.New(w, "", log.LstdFlags) - return logger, nil +func loggerService(dic izidic.Container) (any, error) { + w := dic.MustParam("writer").(io.Writer) + log.SetOutput(w) // Support dependency code not taking an injected logger. + logger := log.New(w, "", log.LstdFlags) + return logger, nil } -func appService(dic *izidic.Container) (any, error) { - wdic := container{dic} // wrapped container with typed accessors - logger := dic.Logger() // typed service instance - name := dic.Name() // typed parameter value - appFeature := makeAppFeature(name, logger) - return appFeature, nil +func appService(dic izidic.Container) (any, error) { + wdic := Container{dic} // wrapped Container with typed accessors + logger := wdic.Logger() // typed service instance + name := wdic.Name() // typed parameter value + appFeature := makeAppFeature(name, logger) + return appFeature, nil } -func resolve(w io.Writer, name string, args []string) izidic.Container { - dic := izidic.New() - dic.Store("writer", w) - dic.Register("logger", loggerService) - // others... - dic.Freeze() - return dic +func Resolve(w io.Writer, name string, args []string) izidic.Container { + dic := izidic.New() + dic.Store("name", name) + dic.Store("writer", w) + dic.Register("logger", loggerService) + dic.Register("app", appService) + // others... + dic.Freeze() + return dic } ``` These accessors will be useful when defining services, as in `appService` above, -or in the boot sequence, which typically neeeds at least a `logger` and one or +or in the boot sequence, which typically needs at least a `logger` and one or more application-domain service instances. @@ -160,3 +162,5 @@ Instead, in the service providing a given feature, use something like `appServic In most cases, the value obtained thus will be a `struct` or a `func`, ready to be used without further data from the container. + +See a complete demo in [examples/demo.go](examples/demo.go). diff --git a/examples/demo.go b/examples/demo.go new file mode 100644 index 0000000..e0678d4 --- /dev/null +++ b/examples/demo.go @@ -0,0 +1,17 @@ +package main + +import ( + "log" + "os" + + "github.com/fgm/izidic/examples/di" +) + +func main() { + dic := di.Resolve(os.Stdout, os.Args[0], os.Args[1:]) + app := dic.MustService("app").(di.App) + log.Printf("app: %#v\n", app) + if err := app(); err != nil { + os.Exit(1) + } +} diff --git a/examples/di/app.go b/examples/di/app.go new file mode 100644 index 0000000..887700a --- /dev/null +++ b/examples/di/app.go @@ -0,0 +1,12 @@ +package di + +import "log" + +type App func() error + +func makeAppFeature(name string, logger *log.Logger) App { + return func() error { + logger.Println(name) + return nil + } +} diff --git a/examples/di/di.go b/examples/di/di.go new file mode 100644 index 0000000..398fb33 --- /dev/null +++ b/examples/di/di.go @@ -0,0 +1,49 @@ +package di + +import ( + "io" + "log" + + "github.com/fgm/izidic" +) + +type Container struct { + izidic.Container +} + +// Logger is a typed service accessor. +func (c *Container) Logger() *log.Logger { + return c.MustService("logger").(*log.Logger) +} + +// Name is a types parameter accessor. +func (c *Container) Name() string { + return c.MustParam("name").(string) +} + +// loggerService is an izidic.Service also containing a one-time initialization action. +func loggerService(dic izidic.Container) (any, error) { + w := dic.MustParam("writer").(io.Writer) + log.SetOutput(w) // Support dependency code not taking an injected logger. + logger := log.New(w, "", log.LstdFlags) + return logger, nil +} + +func appService(dic izidic.Container) (any, error) { + wdic := Container{dic} // wrapped Container with typed accessors + logger := wdic.Logger() // typed service instance + name := wdic.Name() // typed parameter value + appFeature := makeAppFeature(name, logger) + return appFeature, nil +} + +func Resolve(w io.Writer, name string, args []string) izidic.Container { + dic := izidic.New() + dic.Store("name", name) + dic.Store("writer", w) + dic.Register("logger", loggerService) + dic.Register("app", appService) + // others... + dic.Freeze() + return dic +} diff --git a/izidic.go b/izidic.go index ebbb12d..4ead9fa 100644 --- a/izidic.go +++ b/izidic.go @@ -4,7 +4,7 @@ // allowing users to store definitions of services requiring other services // before those are actually defined. // -// Container writes are not concurrency-safe, so they are locked with Container.Freeze() +// container writes are not concurrency-safe, so they are locked with container.Freeze() // after the initial setup, which is assumed to be non-concurrent package izidic @@ -22,10 +22,21 @@ import ( // which should then be type-asserted before use. // // Any access to a service from the container returns the same instance. -type Service func(dic *Container) (any, error) +type Service func(dic Container) (any, error) + +type Container interface { + Freeze() + MustParam(name string) any + MustService(name string) any + Names() map[string][]string + Param(name string) (any, error) + Register(name string, fn Service) + Store(name string, param any) + Service(name string) (any, error) +} -// Container is the container, holding both parameters and services -type Container struct { +// container is the container, holding both parameters and services +type container struct { sync.RWMutex // Lock for service instances frozen bool parameters map[string]any @@ -35,12 +46,28 @@ type Container struct { // Freeze converts the container from build mode, which does not support // concurrency, to run mode, which does. -func (dic *Container) Freeze() { +func (dic *container) Freeze() { dic.frozen = true } +func (dic *container) MustParam(name string) any { + p, err := dic.Param(name) + if err != nil { + panic(err) + } + return p +} + +func (dic *container) MustService(name string) any { + instance, err := dic.Service(name) + if err != nil { + panic(err) + } + return instance +} + // Names returns the names of all the parameters and instances defined on the container. -func (dic *Container) Names() map[string][]string { +func (dic *container) Names() map[string][]string { dump := map[string][]string{ "params": make([]string, 0, len(dic.parameters)), "services": make([]string, 0, len(dic.serviceDefs)), @@ -58,24 +85,27 @@ func (dic *Container) Names() map[string][]string { return dump } -// Register registers a service with the container. -func (dic *Container) Register(name string, fn Service) { - if dic.frozen { - panic("Cannot register services on frozen container") +func (dic *container) Param(name string) (any, error) { + dic.RLock() + defer dic.RUnlock() + + p, found := dic.parameters[name] + if !found { + return nil, fmt.Errorf("parameter not found: %q", name) } - dic.serviceDefs[name] = fn + return p, nil } -// Store stores a parameter in the container. -func (dic *Container) Store(name string, param any) { +// Register registers a service with the container. +func (dic *container) Register(name string, fn Service) { if dic.frozen { - panic("Cannot store parameters on frozen container") + panic("Cannot register services on frozen container") } - dic.parameters[name] = param + dic.serviceDefs[name] = fn } // Service returns the single instance of the requested service on success. -func (dic *Container) Service(name string) (any, error) { +func (dic *container) Service(name string) (any, error) { // Reuse existing instance if any. dic.RLock() instance, found := dic.services[name] @@ -86,7 +116,7 @@ func (dic *Container) Service(name string) (any, error) { // Otherwise instantiate. No lock because no concurrent writes can happen: // - during build, recursive calls may happen, but not concurrently - // - after freeze, no new services may be created: see Container.Register + // - after freeze, no new services may be created: see container.Register service, found := dic.serviceDefs[name] if !found { return nil, fmt.Errorf("service not found: %q", name) @@ -96,7 +126,7 @@ func (dic *Container) Service(name string) (any, error) { // this step than there are services defined in the container, then resolution // for at least one service was attempted more than once, which implies a // dependency cycle. - const funcName = "github.com/fgm/izidic.(*Container).Service" + const funcName = "github.com/fgm/izidic.(*container).Service" // We need a vastly oversized value to cover the case of deeply nested dic.Service() calls. pcs := make([]uintptr, 1e6) n := runtime.Callers(1, pcs) @@ -128,36 +158,17 @@ func (dic *Container) Service(name string) (any, error) { return instance, nil } -func (dic *Container) MustService(name string) any { - instance, err := dic.Service(name) - if err != nil { - panic(err) - } - return instance -} - -func (dic *Container) Param(name string) (any, error) { - dic.RLock() - defer dic.RUnlock() - - p, found := dic.parameters[name] - if !found { - return nil, fmt.Errorf("parameter not found: %q", name) - } - return p, nil -} - -func (dic *Container) MustParam(name string) any { - p, err := dic.Param(name) - if err != nil { - panic(err) +// Store stores a parameter in the container. +func (dic *container) Store(name string, param any) { + if dic.frozen { + panic("Cannot store parameters on frozen container") } - return p + dic.parameters[name] = param } // New creates a container ready for use. -func New() *Container { - return &Container{ +func New() Container { + return &container{ RWMutex: sync.RWMutex{}, parameters: make(map[string]any), serviceDefs: make(map[string]Service), diff --git a/izidic_test.go b/izidic_test.go index 70afc1b..e173a43 100644 --- a/izidic_test.go +++ b/izidic_test.go @@ -1,4 +1,4 @@ -package izidic +package izidic_test import ( "errors" @@ -6,14 +6,15 @@ import ( "strings" "testing" + "github.com/fgm/izidic" "github.com/google/go-cmp/cmp" ) var ( - s1 = func(c *Container) (any, error) { + s1 = func(c izidic.Container) (any, error) { return "s1", nil } - s2 = func(c *Container) (any, error) { + s2 = func(c izidic.Container) (any, error) { s1, err := c.Service("s1") if err != nil { return nil, fmt.Errorf("could not get service s1: %w", err) @@ -39,7 +40,7 @@ func TestContainer_Param(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - dic := New() + dic := izidic.New() for _, kv := range test.stored { dic.Store(kv.k, kv.v) } @@ -67,7 +68,7 @@ func TestContainer_MustParam(t *testing.T) { t.Fatalf("got %q, but expected %q", actual.Error(), expected) } }() - dic := New() + dic := izidic.New() // Happy path dic.Store("k", "v") actual := dic.MustParam("k").(string) @@ -81,7 +82,7 @@ func TestContainer_MustParam(t *testing.T) { func TestContainer_Service(t *testing.T) { const expected = "s1s2" - dic := New() + dic := izidic.New() dic.Register("s1", s1) dic.Register("s2", s2) s, err := dic.Service("s2") @@ -110,9 +111,9 @@ func TestContainer_MustService_Missing(t *testing.T) { t.Fatalf("got %q, but expected %q", actual.Error(), expected) } }() - dic := New() + dic := izidic.New() // Happy path - s := func(*Container) (any, error) { return 42, nil } + s := func(izidic.Container) (any, error) { return 42, nil } dic.Register("s", s) actual := dic.MustService("s").(int) expected, _ := s(dic) @@ -126,10 +127,10 @@ func TestContainer_MustService_Missing(t *testing.T) { func TestContainer_Service_Failing(t *testing.T) { instErr := errors.New("failed") - s := func(dic *Container) (any, error) { + s := func(dic izidic.Container) (any, error) { return nil, instErr } - dic := New() + dic := izidic.New() dic.Register("s", s) actualService, err := dic.Service("s") if actualService != nil { @@ -146,12 +147,12 @@ func TestContainer_Service_Failing(t *testing.T) { func TestContainer_Service_Reuse(t *testing.T) { const name = "s" counter := 0 - service := func(dic *Container) (any, error) { + service := func(dic izidic.Container) (any, error) { counter++ return counter, nil } - dic := New() + dic := izidic.New() dic.Register(name, service) actual := dic.MustService(name).(int) if actual != 1 { @@ -168,7 +169,7 @@ func TestContainer_Names(t *testing.T) { vpt *string vt string ) - dic := New() + dic := izidic.New() dic.Store("p1", vt) dic.Store("p2", vpt) dic.Register("s1", s1) @@ -187,11 +188,11 @@ func TestContainer_Names(t *testing.T) { func TestContainer_Freeze(t *testing.T) { tests := [...]struct { name string - attempt func(*Container) + attempt func(container izidic.Container) expected string }{ - {"register", func(dic *Container) { dic.Register("p", nil) }, "Cannot register services on frozen container"}, - {"store", func(dic *Container) { dic.Store("p", "v") }, "Cannot store parameters on frozen container"}, + {"register", func(dic izidic.Container) { dic.Register("p", nil) }, "Cannot register services on frozen container"}, + {"store", func(dic izidic.Container) { dic.Store("p", "v") }, "Cannot store parameters on frozen container"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -205,7 +206,7 @@ func TestContainer_Freeze(t *testing.T) { t.Fatalf("Got %s but expected %s", msg, test.expected) } }() - dic := New() + dic := izidic.New() dic.Freeze() test.attempt(dic) }) @@ -214,21 +215,21 @@ func TestContainer_Freeze(t *testing.T) { func TestContainer_Service_CircularDeps(t *testing.T) { // We build a 3-level dependency because some simpler strategies to address 2-level (mutual) dependencies do not catch more complex ones, - sA := func(c *Container) (any, error) { + sA := func(c izidic.Container) (any, error) { sC, err := c.Service("sC") if err != nil { return nil, fmt.Errorf("could not get service sC: %w", err) } return sC.(string) + "sA", nil } - sB := func(c *Container) (any, error) { + sB := func(c izidic.Container) (any, error) { sA, err := c.Service("sA") if err != nil { return nil, fmt.Errorf("could not get service sA: %w", err) } return sA.(string) + "sB", nil } - sC := func(c *Container) (any, error) { + sC := func(c izidic.Container) (any, error) { sB, err := c.Service("sB") if err != nil { return nil, fmt.Errorf("could not get service sB: %w", err) @@ -236,7 +237,7 @@ func TestContainer_Service_CircularDeps(t *testing.T) { return sB.(string) + "sC", nil } - dic := New() + dic := izidic.New() dic.Register("sA", sA) dic.Register("sB", sB) dic.Register("sC", sC)