diff --git a/docs/docs/container/options.md b/docs/docs/container/options.md index fcbdc6d..55c3eff 100644 --- a/docs/docs/container/options.md +++ b/docs/docs/container/options.md @@ -23,13 +23,39 @@ For a quick start, you may use the default global container. This is highly disc ```go import "github.com/samber/do/v2" -Provide(nil, ...) -Invoke(nil, ...) +do.Provide(nil, ...) +do.Invoke(nil, ...) // equal to: -Provide(do.DefaultRootScope, ...) -Invoke(do.DefaultRootScope, ...) +do.Provide(do.DefaultRootScope, ...) +do.Invoke(do.DefaultRootScope, ...) +``` + +## Register services on container initialization + +The services can be assembled into a package, and then, imported all at once into a new container. + +```go +// pkg/stores/package.go + +var Package = do.Package( + do.Lazy(NewPostgresqlConnectionService), + do.Lazy(NewUserRepository), + do.Lazy(NewArticleRepository), + do.EagerNamed("repository.logger", slog.New(slog.NewTextHandler(os.Stdout, nil))), +) +``` + +```go +// cmd/main.go + +import ( + "example/pkg/stores" + "example/pkg/handlers" +) + +injector := do.New(stores.Package) ``` ## Custom options diff --git a/docs/docs/service-registration/package-loading.md b/docs/docs/service-registration/package-loading.md new file mode 100644 index 0000000..9ef87eb --- /dev/null +++ b/docs/docs/service-registration/package-loading.md @@ -0,0 +1,54 @@ +--- +title: Package loading +description: Package loading groups multiple service registrations. +sidebar_position: 4 +--- + +# Package loading + +Package loading groups multiple service registrations. + +## Registration + +The services can be assembled into a package, and then, exported all at once. + +```go +// pkg/stores/package.go + +var Package = do.Package( + do.Lazy(NewPostgresqlConnectionService), + do.Lazy(NewUserRepository), + do.Lazy(NewArticleRepository), + do.EagerNamed("repository.logger", slog.New(slog.NewTextHandler(os.Stdout, nil))), +) +``` + +```go +// cmd/main.go + +import ( + "example/pkg/stores" + "example/pkg/handlers" +) + +func main() { + injector := do.New(stores.Package) + // ... + + // optional scope: + scope := injector.Scope("handlers", handlers.Package) + + // ... +} +``` + +The traditional vocab can be translated for service registration: + +- `Provide[T](Injector)` -> `Lazy(T)` +- `ProvideNamed[T](Injector, string)` -> `LazyNamed(string, T)` +- `ProvideValue(Injector, T)` -> `Eager(T)` +- `ProvideNamedValue[T](Injector, string, T)` -> `EagerNamed(string, T)` +- `ProvideTransient[T](Injector)` -> `Transient(T)` +- `ProvideNamedTransient[T](Injector, string)` -> `TransientNamed(string, T)` +- `As[Initial, Alias](Injector)` -> `Bind[Initial, Alias]()` +- `AsNamed[Initial, Alias](Injector, string, string)` -> `BindNamed[Initial, Alias](string, string)` diff --git a/injector.go b/injector.go index 871c6cb..e431810 100644 --- a/injector.go +++ b/injector.go @@ -7,7 +7,7 @@ type Injector interface { // api ID() string Name() string - Scope(string) *Scope + Scope(string, ...func(Injector)) *Scope RootScope() *RootScope Ancestors() []*Scope Children() []*Scope diff --git a/package.go b/package.go new file mode 100644 index 0000000..effeea8 --- /dev/null +++ b/package.go @@ -0,0 +1,57 @@ +package do + +func Package(services ...func(i Injector)) func(Injector) { + return func(injector Injector) { + for i := range services { + services[i](injector) + } + } +} + +func Lazy[T any](p Provider[T]) func(Injector) { + return func(injector Injector) { + Provide(injector, p) + } +} + +func LazyNamed[T any](serviceName string, p Provider[T]) func(Injector) { + return func(injector Injector) { + ProvideNamed(injector, serviceName, p) + } +} + +func Eager[T any](value T) func(Injector) { + return func(injector Injector) { + ProvideValue(injector, value) + } +} + +func EagerNamed[T any](serviceName string, value T) func(Injector) { + return func(injector Injector) { + ProvideNamedValue(injector, serviceName, value) + } +} + +func Transient[T any](p Provider[T]) func(Injector) { + return func(injector Injector) { + ProvideTransient(injector, p) + } +} + +func TransientNamed[T any](serviceName string, p Provider[T]) func(Injector) { + return func(injector Injector) { + ProvideNamedTransient(injector, serviceName, p) + } +} + +func Bind[Initial any, Alias any]() func(Injector) { + return func(injector Injector) { + MustAs[Initial, Alias](injector) + } +} + +func BindNamed[Initial any, Alias any](initial string, alias string) func(Injector) { + return func(injector Injector) { + MustAsNamed[Initial, Alias](injector, initial, alias) + } +} diff --git a/package_test.go b/package_test.go new file mode 100644 index 0000000..8390c73 --- /dev/null +++ b/package_test.go @@ -0,0 +1,329 @@ +package do + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPackage(t *testing.T) { + is := assert.New(t) + + type test struct{} + type iTest interface{} + + provider1 := func(i Injector) (*test, error) { + return &test{}, nil + } + + pkg := Package( + Lazy(provider1), + Eager(test{}), + Bind[*test, iTest](), + ) + + root := New() + pkg(root) + + svc1 := newEdgeService(root.ID(), root.Name(), NameOf[*test]()) + svc2 := newEdgeService(root.ID(), root.Name(), NameOf[test]()) + svc3 := newEdgeService(root.ID(), root.Name(), NameOf[iTest]()) + + is.ElementsMatch([]EdgeService{svc1, svc2, svc3}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{}, root.ListInvokedServices()) + + is.NotPanics(func() { + _ = MustInvoke[*test](root) + _ = MustInvoke[test](root) + _ = MustInvoke[iTest](root) + }) + + is.ElementsMatch([]EdgeService{svc1, svc2, svc3}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{svc1, svc2, svc3}, root.ListInvokedServices()) +} + +func TestNewWithPackage(t *testing.T) { + is := assert.New(t) + + type test struct{} + type iTest interface{} + + provider1 := func(i Injector) (*test, error) { + return &test{}, nil + } + + pkg := Package( + Lazy(provider1), + Eager(test{}), + ) + + root := New( + pkg, + Bind[*test, iTest](), + ) + + svc1 := newEdgeService(root.ID(), root.Name(), NameOf[*test]()) + svc2 := newEdgeService(root.ID(), root.Name(), NameOf[test]()) + svc3 := newEdgeService(root.ID(), root.Name(), NameOf[iTest]()) + + is.ElementsMatch([]EdgeService{svc1, svc2, svc3}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{}, root.ListInvokedServices()) + + is.NotPanics(func() { + _ = MustInvoke[*test](root) + _ = MustInvoke[test](root) + _ = MustInvoke[iTest](root) + }) + + is.ElementsMatch([]EdgeService{svc1, svc2, svc3}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{svc1, svc2, svc3}, root.ListInvokedServices()) +} + +func TestLazy(t *testing.T) { + is := assert.New(t) + + type test struct{} + + provider1 := func(i Injector) (*test, error) { + return &test{}, nil + } + + provider2 := func(i Injector) (test, error) { + return test{}, fmt.Errorf("error") + } + + root := New() + Lazy(provider1)(root) + Lazy(provider2)(root) + + svc1 := newEdgeService(root.ID(), root.Name(), NameOf[*test]()) + svc2 := newEdgeService(root.ID(), root.Name(), NameOf[test]()) + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{}, root.ListInvokedServices()) + + is.NotPanics(func() { + _ = MustInvoke[*test](root) + _, _ = Invoke[test](root) + }) + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{svc1}, root.ListInvokedServices()) +} + +func TestLazyNamed(t *testing.T) { + is := assert.New(t) + + type test struct{} + + provider1 := func(i Injector) (*test, error) { + return &test{}, nil + } + + provider2 := func(i Injector) (test, error) { + return test{}, fmt.Errorf("error") + } + + root := New() + LazyNamed("p1", provider1)(root) + LazyNamed("p2", provider2)(root) + + svc1 := newEdgeService(root.ID(), root.Name(), "p1") + svc2 := newEdgeService(root.ID(), root.Name(), "p2") + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{}, root.ListInvokedServices()) + + is.NotPanics(func() { + _ = MustInvokeNamed[*test](root, "p1") + _, _ = InvokeNamed[test](root, "p2") + }) + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{svc1}, root.ListInvokedServices()) +} + +func TestEager(t *testing.T) { + is := assert.New(t) + + type test struct{} + + root := New() + Eager(&test{})(root) + Eager(test{})(root) + + svc1 := newEdgeService(root.ID(), root.Name(), NameOf[*test]()) + svc2 := newEdgeService(root.ID(), root.Name(), NameOf[test]()) + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{}, root.ListInvokedServices()) + + is.NotPanics(func() { + _ = MustInvoke[*test](root) + _ = MustInvoke[test](root) + }) + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListInvokedServices()) +} + +func TestEagerNamed(t *testing.T) { + is := assert.New(t) + + type test struct{} + + root := New() + EagerNamed("p1", &test{})(root) + EagerNamed("p2", test{})(root) + + svc1 := newEdgeService(root.ID(), root.Name(), "p1") + svc2 := newEdgeService(root.ID(), root.Name(), "p2") + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{}, root.ListInvokedServices()) + + is.NotPanics(func() { + _ = MustInvokeNamed[*test](root, "p1") + _ = MustInvokeNamed[test](root, "p2") + }) + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListInvokedServices()) +} + +func TestTransient(t *testing.T) { + is := assert.New(t) + + type test struct{} + + provider1 := func(i Injector) (*test, error) { + return &test{}, nil + } + + provider2 := func(i Injector) (test, error) { + return test{}, fmt.Errorf("error") + } + + root := New() + Transient(provider1)(root) + Transient(provider2)(root) + + svc1 := newEdgeService(root.ID(), root.Name(), NameOf[*test]()) + svc2 := newEdgeService(root.ID(), root.Name(), NameOf[test]()) + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{}, root.ListInvokedServices()) + + is.NotPanics(func() { + _ = MustInvoke[*test](root) + _, _ = Invoke[test](root) + }) + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{svc1}, root.ListInvokedServices()) +} + +func TestTransientNamed(t *testing.T) { + is := assert.New(t) + + type test struct{} + + provider1 := func(i Injector) (*test, error) { + return &test{}, nil + } + + provider2 := func(i Injector) (test, error) { + return test{}, fmt.Errorf("error") + } + + root := New() + TransientNamed("p1", provider1)(root) + TransientNamed("p2", provider2)(root) + + svc1 := newEdgeService(root.ID(), root.Name(), "p1") + svc2 := newEdgeService(root.ID(), root.Name(), "p2") + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{}, root.ListInvokedServices()) + + is.NotPanics(func() { + _ = MustInvokeNamed[*test](root, "p1") + _, _ = InvokeNamed[test](root, "p2") + }) + + is.ElementsMatch([]EdgeService{svc1, svc2}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{svc1}, root.ListInvokedServices()) +} + +func TestBind(t *testing.T) { + is := assert.New(t) + + type test struct{} + type iTest interface{} + + provider1 := func(i Injector) (*test, error) { + return &test{}, nil + } + + provider2 := func(i Injector) (test, error) { + return test{}, fmt.Errorf("error") + } + + root := New() + Lazy(provider1)(root) + Lazy(provider2)(root) + Bind[*test, iTest]()(root) + + svc1 := newEdgeService(root.ID(), root.Name(), NameOf[*test]()) + svc2 := newEdgeService(root.ID(), root.Name(), NameOf[test]()) + svc3 := newEdgeService(root.ID(), root.Name(), NameOf[iTest]()) + + is.ElementsMatch([]EdgeService{svc1, svc2, svc3}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{}, root.ListInvokedServices()) + + is.NotPanics(func() { + _ = MustInvoke[*test](root) + _, _ = Invoke[test](root) + _ = MustInvoke[iTest](root) + }) + + is.ElementsMatch([]EdgeService{svc1, svc2, svc3}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{svc1, svc3}, root.ListInvokedServices()) +} + +func TestBindNamed(t *testing.T) { + is := assert.New(t) + + type test struct{} + type iTest interface{} + + provider1 := func(i Injector) (*test, error) { + return &test{}, nil + } + + provider2 := func(i Injector) (test, error) { + return test{}, fmt.Errorf("error") + } + + root := New() + Lazy(provider1)(root) + Lazy(provider2)(root) + BindNamed[*test, iTest](NameOf[*test](), NameOf[iTest]())(root) + + svc1 := newEdgeService(root.ID(), root.Name(), NameOf[*test]()) + svc2 := newEdgeService(root.ID(), root.Name(), NameOf[test]()) + svc3 := newEdgeService(root.ID(), root.Name(), NameOf[iTest]()) + + is.ElementsMatch([]EdgeService{svc1, svc2, svc3}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{}, root.ListInvokedServices()) + + is.NotPanics(func() { + _ = MustInvoke[*test](root) + _, _ = Invoke[test](root) + _ = MustInvoke[iTest](root) + }) + + is.ElementsMatch([]EdgeService{svc1, svc2, svc3}, root.ListProvidedServices()) + is.ElementsMatch([]EdgeService{svc1, svc3}, root.ListInvokedServices()) +} diff --git a/root_scope.go b/root_scope.go index 3f4f55e..9068a8e 100644 --- a/root_scope.go +++ b/root_scope.go @@ -13,12 +13,12 @@ var DefaultRootScope *RootScope = New() var noOpLogf = func(format string, args ...any) {} // New creates a new injector. -func New() *RootScope { - return NewWithOpts(&InjectorOpts{}) +func New(packages ...func(Injector)) *RootScope { + return NewWithOpts(&InjectorOpts{}, packages...) } // NewWithOpts creates a new injector with options. -func NewWithOpts(opts *InjectorOpts) *RootScope { +func NewWithOpts(opts *InjectorOpts, packages ...func(Injector)) *RootScope { if opts.Logf == nil { opts.Logf = noOpLogf } @@ -38,6 +38,10 @@ func NewWithOpts(opts *InjectorOpts) *RootScope { root.opts.Logf("DI: injector created") + for _, pkg := range packages { + pkg(root) + } + return root } @@ -52,17 +56,17 @@ type RootScope struct { } // pass through -func (s *RootScope) ID() string { return s.self.ID() } -func (s *RootScope) Name() string { return s.self.Name() } -func (s *RootScope) Scope(name string) *Scope { return s.self.Scope(name) } -func (s *RootScope) RootScope() *RootScope { return s.self.RootScope() } -func (s *RootScope) Ancestors() []*Scope { return []*Scope{} } -func (s *RootScope) Children() []*Scope { return s.self.Children() } -func (s *RootScope) ChildByID(id string) (*Scope, bool) { return s.self.ChildByID(id) } -func (s *RootScope) ChildByName(name string) (*Scope, bool) { return s.self.ChildByName(name) } -func (s *RootScope) ListProvidedServices() []EdgeService { return s.self.ListProvidedServices() } -func (s *RootScope) ListInvokedServices() []EdgeService { return s.self.ListInvokedServices() } -func (s *RootScope) HealthCheck() map[string]error { return s.self.HealthCheck() } +func (s *RootScope) ID() string { return s.self.ID() } +func (s *RootScope) Name() string { return s.self.Name() } +func (s *RootScope) Scope(name string, p ...func(Injector)) *Scope { return s.self.Scope(name, p...) } +func (s *RootScope) RootScope() *RootScope { return s.self.RootScope() } +func (s *RootScope) Ancestors() []*Scope { return []*Scope{} } +func (s *RootScope) Children() []*Scope { return s.self.Children() } +func (s *RootScope) ChildByID(id string) (*Scope, bool) { return s.self.ChildByID(id) } +func (s *RootScope) ChildByName(name string) (*Scope, bool) { return s.self.ChildByName(name) } +func (s *RootScope) ListProvidedServices() []EdgeService { return s.self.ListProvidedServices() } +func (s *RootScope) ListInvokedServices() []EdgeService { return s.self.ListInvokedServices() } +func (s *RootScope) HealthCheck() map[string]error { return s.self.HealthCheck() } func (s *RootScope) HealthCheckWithContext(ctx context.Context) map[string]error { return s.self.HealthCheckWithContext(ctx) } diff --git a/scope.go b/scope.go index 916f62e..8cf3ed9 100644 --- a/scope.go +++ b/scope.go @@ -51,7 +51,7 @@ func (s *Scope) Name() string { } // Scope creates a new child scope. -func (s *Scope) Scope(name string) *Scope { +func (s *Scope) Scope(name string, packages ...func(Injector)) *Scope { s.mu.Lock() defer s.mu.Unlock() @@ -62,6 +62,10 @@ func (s *Scope) Scope(name string) *Scope { child := newScope(name, s.rootScope, s) s.childScopes[name] = child + for _, pkg := range packages { + pkg(child) + } + return child } diff --git a/virtual_scope.go b/virtual_scope.go index 6e41f6e..c076e68 100644 --- a/virtual_scope.go +++ b/virtual_scope.go @@ -16,17 +16,17 @@ type virtualScope struct { } // pass through -func (s *virtualScope) ID() string { return s.self.ID() } -func (s *virtualScope) Name() string { return s.self.Name() } -func (s *virtualScope) Scope(name string) *Scope { return s.self.Scope(name) } -func (s *virtualScope) RootScope() *RootScope { return s.self.RootScope() } -func (s *virtualScope) Ancestors() []*Scope { return s.self.Ancestors() } -func (s *virtualScope) Children() []*Scope { return s.self.Children() } -func (s *virtualScope) ChildByID(id string) (*Scope, bool) { return s.self.ChildByID(id) } -func (s *virtualScope) ChildByName(name string) (*Scope, bool) { return s.self.ChildByName(name) } -func (s *virtualScope) ListProvidedServices() []EdgeService { return s.self.ListProvidedServices() } -func (s *virtualScope) ListInvokedServices() []EdgeService { return s.self.ListInvokedServices() } -func (s *virtualScope) HealthCheck() map[string]error { return s.self.HealthCheck() } +func (s *virtualScope) ID() string { return s.self.ID() } +func (s *virtualScope) Name() string { return s.self.Name() } +func (s *virtualScope) Scope(n string, p ...func(Injector)) *Scope { return s.self.Scope(n, p...) } +func (s *virtualScope) RootScope() *RootScope { return s.self.RootScope() } +func (s *virtualScope) Ancestors() []*Scope { return s.self.Ancestors() } +func (s *virtualScope) Children() []*Scope { return s.self.Children() } +func (s *virtualScope) ChildByID(id string) (*Scope, bool) { return s.self.ChildByID(id) } +func (s *virtualScope) ChildByName(name string) (*Scope, bool) { return s.self.ChildByName(name) } +func (s *virtualScope) ListProvidedServices() []EdgeService { return s.self.ListProvidedServices() } +func (s *virtualScope) ListInvokedServices() []EdgeService { return s.self.ListInvokedServices() } +func (s *virtualScope) HealthCheck() map[string]error { return s.self.HealthCheck() } func (s *virtualScope) HealthCheckWithContext(ctx context.Context) map[string]error { return s.self.HealthCheckWithContext(ctx) }