From 7474ef2b00ce1707f228938b9123f4b99963ba5a Mon Sep 17 00:00:00 2001 From: Matthias Friedrich <1573457+matzefriedrich@users.noreply.github.com> Date: Sat, 13 Jul 2024 22:47:06 +0200 Subject: [PATCH] Feature/inject func factory (#6) * Extends the registry and resolver modules to accept registrations for factory methods * Reorganizes the whole package structure (adds sub-packages, moves integration tests to the internal package) * Renames test methods * Fixes typo in error message --- CHANGELOG.md | 40 ++-- README.md | 2 + {pkg => internal/core}/function_info.go | 4 +- internal/{ => core}/instances.go | 2 +- internal/{ => core}/service_id.go | 2 +- internal/tests/registry_register_func_test.go | 89 +++++++++ internal/tests/registry_test.go | 180 ++++++++++++++++++ .../tests}/resolver_factory_test.go | 17 +- internal/tests/resolver_options_test.go | 30 +++ {pkg => internal/tests}/resolver_test.go | 19 +- internal/{ => tests}/service_id_test.go | 5 +- pkg/{ => registration}/activator.go | 17 +- pkg/{types => registration}/dependency.go | 32 ++-- pkg/{ => registration}/registry.go | 22 +-- pkg/{ => registration}/registry_accessor.go | 2 +- .../service_registration.go | 19 +- pkg/{types => registration}/service_type.go | 2 +- pkg/registry_test.go | 177 ----------------- pkg/resolver_options_test.go | 28 --- pkg/{ => resolving}/resolver.go | 33 ++-- pkg/{ => resolving}/resolver_options.go | 5 +- pkg/types/dependency_error.go | 20 ++ pkg/types/resolver_error.go | 32 ++-- pkg/types/types.go | 1 - 24 files changed, 455 insertions(+), 325 deletions(-) rename {pkg => internal/core}/function_info.go (94%) rename internal/{ => core}/instances.go (99%) rename internal/{ => core}/service_id.go (95%) create mode 100644 internal/tests/registry_register_func_test.go create mode 100644 internal/tests/registry_test.go rename {pkg => internal/tests}/resolver_factory_test.go (60%) create mode 100644 internal/tests/resolver_options_test.go rename {pkg => internal/tests}/resolver_test.go (55%) rename internal/{ => tests}/service_id_test.go (82%) rename pkg/{ => registration}/activator.go (56%) rename pkg/{types => registration}/dependency.go (68%) rename pkg/{ => registration}/registry.go (83%) rename pkg/{ => registration}/registry_accessor.go (97%) rename pkg/{ => registration}/service_registration.go (86%) rename pkg/{types => registration}/service_type.go (82%) delete mode 100644 pkg/registry_test.go delete mode 100644 pkg/resolver_options_test.go rename pkg/{ => resolving}/resolver.go (80%) rename pkg/{ => resolving}/resolver_options.go (80%) create mode 100644 pkg/types/dependency_error.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b8694e9..c799fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,25 +5,33 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - +## v0.4.0 - 2024-07-13 +### Added + +* Support for factory functions to create instances of services based on input parameters provided at runtime + +### Changed + +* Reorganizes the whole package structure; adds sub-packages for `registration` and `resolving`. A bunch of types that support the inner functionality of the package have been moved to `internal.`. +* Integration tests are moved to the `internal` package. ## v0.3.0 - 2024-07-12 ### Added -* Service registrations can be bundled in a `ModuleFunc` to register related types as a unit -* Service registry accepts object instances as singleton service registrations -* Adds the `ResolveRequiredService[T]` convenience function that resolves and safe-casts objects -* Registers resolver instance with the registry so that the `Resolver` object can be injected into factory and constructor methods -* The resolver can now accept instances of non-registered types via the `ResolveWithOptions[T]` method +* Service registrations can be bundled in a `ModuleFunc` to register related types as a unit. +* The service registry accepts object instances as singleton service registrations. +* Adds the `ResolveRequiredService[T]` convenience function that resolves and safe-casts objects. +* Registers resolver instance with the registry so that the `Resolver` object can be injected into factory and constructor methods. +* The resolver can now accept instances of non-registered types via the `ResolveWithOptions[T]` method. * `ServiceRegistry` has new methods for creating linked and scoped registry objects (which share the same `ServiceIdSequence`). Scoped registries inherit all parent service registrations, while linked registries are empty. See `CreateLinkedRegistry` and `CreateScope` methods. ### Changed -* A `ServiceRegistryAccessor` is no longer a `ServiceRegisty`, it is the other way around -* The creation of service registrations and type activators has been refactored; see `activator.go` and `service_registration.go` modules +* A `ServiceRegistryAccessor` is no longer a `ServiceRegisty`, it is the other way around. +* The creation of service registrations and type activators has been refactored; see `activator.go` and `service_registration.go` modules. * Multiple registries can be grouped with `NewMultiRegistryAccessor` to simplify the lookup of service registrations from linked registries. The resolver uses this accessor type to merge registered service types with object instances for unregistered types. @@ -31,23 +39,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Resolver can now detect circular dependencies -* Adds helpers to register services with a certain lifetime scope +* The resolver can now detect circular dependencies. +* Adds helpers to register services with a certain lifetime scope. ### Changed -* Registry rejects non-interface types +* The registry rejects non-interface types. ### Fixes -* Fixes error wrapping in custom error types -* Improves error handling for service registry and resolver +* Fixes error wrapping in custom error types. +* Improves error handling for service registry and resolver. ## v0.1.0 - 2024-07-10 ### Added -* Adds service registry; the registry can map interfaces to implementation types via constructor functions -* Assign lifetime behaviour to services (singleton, scoped, or transient) -* Adds resolver (container) service +* Adds a service registry; the registry can map interfaces to implementation types via constructor functions. +* Assign lifetime behaviour to services (singleton, scoped, or transient). +* Adds a basic resolver (container) service. diff --git a/README.md b/README.md index 0577893..5db0a33 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Though dependency injection is less prevalent in Golang (compared to other langu - ✔️ Bundle type registrations as modules to register them via `RegisterModule` as a unit - ✔️ Resolve objects on-demand - ✔️ Allow consumption of `Resolver` in favor of custom factories + - ⏳ Lazy loading objects by injecting dependencies as `Lazy[T]` + - ✔️ Register factory functions to create instances of services based on input parameters provided at runtime - ⏳ Validate registered services; fail early during application startup if missing registrations are encountered - ✔️ Provide instances for non-registered types, use `ResolveWithOptions[T]` insted of `Resolve[T]` - ⏳ Support multiple service registrations for the same interface diff --git a/pkg/function_info.go b/internal/core/function_info.go similarity index 94% rename from pkg/function_info.go rename to internal/core/function_info.go index 8784dde..3422d74 100644 --- a/pkg/function_info.go +++ b/internal/core/function_info.go @@ -1,4 +1,4 @@ -package pkg +package core import ( "errors" @@ -14,7 +14,7 @@ type functionInfo struct { var _ types.FunctionInfo = &functionInfo{} -func reflectFunctionInfoFrom(value reflect.Value) (types.FunctionInfo, error) { +func ReflectFunctionInfoFrom(value reflect.Value) (types.FunctionInfo, error) { funcType := value.Type() if funcType.Kind() != reflect.Func { return nil, errors.New("not a function") diff --git a/internal/instances.go b/internal/core/instances.go similarity index 99% rename from internal/instances.go rename to internal/core/instances.go index f37f0e9..1095a43 100644 --- a/internal/instances.go +++ b/internal/core/instances.go @@ -1,4 +1,4 @@ -package internal +package core import ( "context" diff --git a/internal/service_id.go b/internal/core/service_id.go similarity index 95% rename from internal/service_id.go rename to internal/core/service_id.go index 868c930..8a7bdb1 100644 --- a/internal/service_id.go +++ b/internal/core/service_id.go @@ -1,4 +1,4 @@ -package internal +package core import "sync" diff --git a/internal/tests/registry_register_func_test.go b/internal/tests/registry_register_func_test.go new file mode 100644 index 0000000..a9f3b70 --- /dev/null +++ b/internal/tests/registry_register_func_test.go @@ -0,0 +1,89 @@ +package tests + +import ( + "context" + "testing" + + "github.com/matzefriedrich/parsley/pkg/registration" + "github.com/matzefriedrich/parsley/pkg/resolving" + "github.com/stretchr/testify/assert" +) + +func Test_Registry_RegisterTransient_registers_factory_function_to_resolve_dynamic_dependency_at_runtime(t *testing.T) { + + // Arrange + sut := registration.NewServiceRegistry() + + _ = registration.RegisterTransient(sut, NewPersonFactory) + _ = registration.RegisterTransient(sut, NewUserController) + + r := resolving.NewResolver(sut) + + const userId = "123" + + // Act + controller, _ := resolving.ResolveRequiredService[UserController](r, context.Background()) + model := controller.GetUserInfo(userId) + + userServiceFactory, _ := resolving.ResolveRequiredService[func(string) (UserService, error)](r, context.Background()) + service, factoryErr := userServiceFactory(userId) + + // Assert + assert.NoError(t, factoryErr) + assert.NotNil(t, service) + + assert.Equal(t, userId, model.Id) + assert.Equal(t, userId, service.UserId()) +} + +type userService struct { + id string +} + +func (f *userService) UserId() string { + return f.id +} + +type userModel struct { + Id string `json:"id"` + Name string `json:"name"` +} + +func (f *userService) UserName() string { + return "" // retrieve username from data backend +} + +type userController struct { + userServiceFactory func(name string) (UserService, error) +} + +func (f *userController) GetUserInfo(userId string) userModel { + userService, _ := f.userServiceFactory(userId) + return userModel{ + Id: userId, + Name: userService.UserName(), + } +} + +type UserService interface { + UserName() string + UserId() string +} + +type UserController interface { + GetUserInfo(userId string) userModel +} + +func NewPersonFactory() func(string) (UserService, error) { + return func(userId string) (UserService, error) { + return &userService{ + id: userId, + }, nil + } +} + +func NewUserController(userServiceFactory func(string) (UserService, error)) UserController { + return &userController{ + userServiceFactory: userServiceFactory, + } +} diff --git a/internal/tests/registry_test.go b/internal/tests/registry_test.go new file mode 100644 index 0000000..0867d06 --- /dev/null +++ b/internal/tests/registry_test.go @@ -0,0 +1,180 @@ +package tests + +import ( + "context" + "reflect" + "testing" + + "github.com/matzefriedrich/parsley/internal/core" + "github.com/matzefriedrich/parsley/pkg/registration" + "github.com/matzefriedrich/parsley/pkg/resolving" + + "github.com/matzefriedrich/parsley/pkg/types" + + "github.com/stretchr/testify/assert" +) + +func Test_ServiceRegistry_register_types_with_different_lifetime_behavior(t *testing.T) { + + // Arrange + sut := registration.NewServiceRegistry() + + // Act + _ = registration.RegisterSingleton(sut, NewFoo) + _ = registration.RegisterTransient(sut, NewFooConsumer) + + fooRegistered := sut.IsRegistered(registration.ServiceType[Foo]()) + fooConsumerRegistered := sut.IsRegistered(registration.ServiceType[FooConsumer]()) + + // Assert + assert.True(t, fooRegistered) + assert.True(t, fooConsumerRegistered) +} + +func Test_Registry_NewResolver_resolve_type_with_dependencies(t *testing.T) { + + // Arrange + sut := registration.NewServiceRegistry() + + // Act + _ = registration.RegisterTransient(sut, NewFoo) + _ = registration.RegisterTransient(sut, NewFooConsumer) + + // Assert + r := resolving.NewResolver(sut) + parsleyContext := core.NewScopedContext(context.Background()) + resolved, _ := r.Resolve(parsleyContext, registration.ServiceType[FooConsumer]()) + assert.NotNil(t, resolved) + + actual, ok := resolved.(FooConsumer) + assert.True(t, ok) + assert.NotNil(t, actual) +} + +func Test_Registry_NewResolver_resolve_scoped_from_same_context_must_be_return_same_instance(t *testing.T) { + + // Arrange + sut := registration.NewServiceRegistry() + + // Act + _ = registration.RegisterSingleton(sut, NewFoo) + _ = registration.RegisterScoped(sut, NewFooConsumer) + + // Assert + r := resolving.NewResolver(sut) + parsleyContext := core.NewScopedContext(context.Background()) + consumer1, _ := r.Resolve(parsleyContext, registration.ServiceType[FooConsumer]()) + assert.NotNil(t, consumer1) + + consumer2, _ := r.Resolve(parsleyContext, registration.ServiceType[FooConsumer]()) + assert.NotNil(t, consumer2) + + actual, ok := consumer1.(FooConsumer) + assert.True(t, ok) + assert.NotNil(t, actual) + assert.Equal(t, reflect.ValueOf(consumer1).Pointer(), reflect.ValueOf(consumer2).Pointer()) +} + +func Test_Registry_NewResolver_resolve_scoped_from_different_context_must_be_return_different_instance(t *testing.T) { + + // Arrange + sut := registration.NewServiceRegistry() + + // Act + _ = registration.RegisterSingleton(sut, NewFoo) + _ = registration.RegisterScoped(sut, NewFooConsumer) + + // Assert + r := resolving.NewResolver(sut) + + parsleyContext1 := core.NewScopedContext(context.Background()) + consumer1, _ := r.Resolve(parsleyContext1, registration.ServiceType[FooConsumer]()) + assert.NotNil(t, consumer1) + + parsleyContext2 := core.NewScopedContext(context.Background()) + consumer2, _ := r.Resolve(parsleyContext2, registration.ServiceType[FooConsumer]()) + assert.NotNil(t, consumer2) + + actual, ok := consumer1.(FooConsumer) + assert.True(t, ok) + assert.NotNil(t, actual) + assert.NotEqual(t, reflect.ValueOf(consumer1).Pointer(), reflect.ValueOf(consumer2).Pointer()) +} + +func Test_Registry_RegisterModule_registers_collection_of_services(t *testing.T) { + + // Arrange + sut := registration.NewServiceRegistry() + + // Act + fooModule := func(r types.ServiceRegistry) error { + _ = registration.RegisterSingleton(r, NewFoo) + _ = registration.RegisterScoped(r, NewFooConsumer) + return nil + } + + _ = sut.RegisterModule(fooModule) + + fooRegistered := sut.IsRegistered(registration.ServiceType[Foo]()) + fooConsumerRegistered := sut.IsRegistered(registration.ServiceType[FooConsumer]()) + + // Assert + assert.True(t, fooRegistered) + assert.True(t, fooConsumerRegistered) +} + +func Test_Registry_RegisterInstance_registers_singleton_service_from_object(t *testing.T) { + + // Arrange + sut := registration.NewServiceRegistry() + + instance := NewFoo() + + // Act + _ = registration.RegisterInstance(sut, instance) + + fooRegistered := sut.IsRegistered(registration.ServiceType[Foo]()) + + r := resolving.NewResolver(sut) + + // Arrange + assert.True(t, fooRegistered) + + resolved, _ := r.Resolve(context.Background(), registration.ServiceType[Foo]()) + assert.NotNil(t, resolved) + + actual, ok := resolved.(Foo) + assert.True(t, ok) + assert.NotNil(t, actual) + assert.Equal(t, reflect.ValueOf(instance).Pointer(), reflect.ValueOf(actual).Pointer()) +} + +type Foo interface { + Bar() +} + +type foo struct{} + +func (f *foo) Bar() {} + +func NewFoo() Foo { + return &foo{} +} + +type FooConsumer interface { + FooBar() +} + +type fooConsumer struct { + foo Foo +} + +func (fb *fooConsumer) FooBar() { + fb.foo.Bar() +} + +func NewFooConsumer(foo Foo) FooConsumer { + return &fooConsumer{ + foo: foo, + } +} diff --git a/pkg/resolver_factory_test.go b/internal/tests/resolver_factory_test.go similarity index 60% rename from pkg/resolver_factory_test.go rename to internal/tests/resolver_factory_test.go index 1291a6e..a585820 100644 --- a/pkg/resolver_factory_test.go +++ b/internal/tests/resolver_factory_test.go @@ -1,8 +1,10 @@ -package pkg +package tests import ( "context" - "github.com/matzefriedrich/parsley/internal" + "github.com/matzefriedrich/parsley/internal/core" + "github.com/matzefriedrich/parsley/pkg/registration" + "github.com/matzefriedrich/parsley/pkg/resolving" "github.com/matzefriedrich/parsley/pkg/types" "github.com/stretchr/testify/assert" "reflect" @@ -12,14 +14,15 @@ import ( func Test_Resolver_ResolveRequiredService_factory_function_receives_current_resolver(t *testing.T) { // Arrange - sut := NewServiceRegistry() - _ = RegisterSingleton(sut, NewFactory) + sut := registration.NewServiceRegistry() + _ = registration.RegisterSingleton(sut, NewFactory) - r := sut.BuildResolver() - ctx := internal.NewScopedContext(context.Background()) + r := resolving.NewResolver(sut) + + ctx := core.NewScopedContext(context.Background()) // Act - serviceFactory, _ := ResolveRequiredService[FactoryService](r, ctx) + serviceFactory, _ := resolving.ResolveRequiredService[FactoryService](r, ctx) f := serviceFactory.(*factory) actual := f.resolver diff --git a/internal/tests/resolver_options_test.go b/internal/tests/resolver_options_test.go new file mode 100644 index 0000000..8b8032f --- /dev/null +++ b/internal/tests/resolver_options_test.go @@ -0,0 +1,30 @@ +package tests + +import ( + "context" + "github.com/matzefriedrich/parsley/internal/core" + "github.com/matzefriedrich/parsley/pkg/registration" + "github.com/matzefriedrich/parsley/pkg/resolving" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_Resolver_ResolveWithOptions_inject_unregistered_service_instance(t *testing.T) { + + // Arrange + sut := registration.NewServiceRegistry() + + // Act + _ = registration.RegisterScoped(sut, NewFooConsumer) + + // Assert + r := resolving.NewResolver(sut) + + parsleyContext := core.NewScopedContext(context.Background()) + consumer1, _ := r.ResolveWithOptions(parsleyContext, registration.ServiceType[FooConsumer](), resolving.WithInstance[Foo](NewFoo())) + assert.NotNil(t, consumer1) + + actual, ok := consumer1.(FooConsumer) + assert.True(t, ok) + assert.NotNil(t, actual) +} diff --git a/pkg/resolver_test.go b/internal/tests/resolver_test.go similarity index 55% rename from pkg/resolver_test.go rename to internal/tests/resolver_test.go index 43cf962..97dd1a0 100644 --- a/pkg/resolver_test.go +++ b/internal/tests/resolver_test.go @@ -1,8 +1,10 @@ -package pkg +package tests import ( "context" - "github.com/matzefriedrich/parsley/internal" + "github.com/matzefriedrich/parsley/internal/core" + "github.com/matzefriedrich/parsley/pkg/registration" + "github.com/matzefriedrich/parsley/pkg/resolving" "github.com/matzefriedrich/parsley/pkg/types" "github.com/stretchr/testify/assert" "testing" @@ -11,15 +13,16 @@ import ( func Test_Resolver_Resolve_returns_err_if_circular_dependency_detected(t *testing.T) { // Arrange - registry := NewServiceRegistry() - _ = RegisterTransient(registry, newFoo) - _ = RegisterTransient(registry, newBar) + registry := registration.NewServiceRegistry() + _ = registration.RegisterTransient(registry, newFoo) + _ = registration.RegisterTransient(registry, newBar) - r := registry.BuildResolver() - scope := internal.NewScopedContext(context.Background()) + r := resolving.NewResolver(registry) + + scope := core.NewScopedContext(context.Background()) // Act - _, err := r.Resolve(scope, types.ServiceType[Foo0]()) + _, err := r.Resolve(scope, registration.ServiceType[Foo0]()) // Assert assert.ErrorIs(t, err, types.ErrCircularDependencyDetected) diff --git a/internal/service_id_test.go b/internal/tests/service_id_test.go similarity index 82% rename from internal/service_id_test.go rename to internal/tests/service_id_test.go index 6dcb498..26af7f4 100644 --- a/internal/service_id_test.go +++ b/internal/tests/service_id_test.go @@ -1,6 +1,7 @@ -package internal +package tests import ( + "github.com/matzefriedrich/parsley/internal/core" "github.com/stretchr/testify/assert" "testing" ) @@ -8,7 +9,7 @@ import ( func Test_ServiceIdSequence_Next_produces_incr_values(t *testing.T) { // Arrange - sut := NewServiceId(0) + sut := core.NewServiceId(0) collectedIdentifiers := make([]uint64, 0) // Act diff --git a/pkg/activator.go b/pkg/registration/activator.go similarity index 56% rename from pkg/activator.go rename to pkg/registration/activator.go index 723f2dc..7da8628 100644 --- a/pkg/activator.go +++ b/pkg/registration/activator.go @@ -1,4 +1,4 @@ -package pkg +package registration import ( "github.com/matzefriedrich/parsley/internal" @@ -11,11 +11,22 @@ func CreateServiceActivatorFrom[T any](instance T) (func() T, error) { return nil, types.NewRegistryError(types.ErrorInstanceCannotBeNil) } t := reflect.TypeOf((*T)(nil)).Elem() - if t.Kind() != reflect.Interface { - return nil, types.NewRegistryError(types.ErrorActivatorFunctionsMustReturnAnInterface) + switch t.Kind() { + case reflect.Func: + case reflect.Interface: + default: + return nil, types.NewRegistryError(types.ErrorActivatorFunctionInvalidReturnType) } instanceFunc := func() T { return instance } return instanceFunc, nil } + +func RegisterInstance[T any](registry types.ServiceRegistry, instance T) error { + instanceFunc, err := CreateServiceActivatorFrom[T](instance) + if err != nil { + return err + } + return registry.Register(instanceFunc, types.LifetimeSingleton) +} diff --git a/pkg/types/dependency.go b/pkg/registration/dependency.go similarity index 68% rename from pkg/types/dependency.go rename to pkg/registration/dependency.go index b320b70..1554cb9 100644 --- a/pkg/types/dependency.go +++ b/pkg/registration/dependency.go @@ -1,48 +1,40 @@ -package types +package registration import ( - "errors" + "github.com/matzefriedrich/parsley/pkg/types" "reflect" "sync" ) type dependencyInfo struct { - registration ServiceRegistration + registration types.ServiceRegistration instance interface{} - children []DependencyInfo - consumer DependencyInfo + children []types.DependencyInfo + consumer types.DependencyInfo m *sync.RWMutex } -var _ DependencyInfo = &dependencyInfo{} +var _ types.DependencyInfo = &dependencyInfo{} -const ( - ErrorInstanceAlreadySet = "instance already set" -) - -var ( - ErrInstanceAlreadySet = errors.New(ErrorInstanceAlreadySet) -) - -func NewDependencyInfo(registration ServiceRegistration, instance interface{}, consumer DependencyInfo) DependencyInfo { +func NewDependencyInfo(registration types.ServiceRegistration, instance interface{}, consumer types.DependencyInfo) types.DependencyInfo { return &dependencyInfo{ registration: registration, instance: instance, - children: make([]DependencyInfo, 0), + children: make([]types.DependencyInfo, 0), consumer: consumer, m: &sync.RWMutex{}, } } -func (d *dependencyInfo) Consumer() DependencyInfo { +func (d *dependencyInfo) Consumer() types.DependencyInfo { return d.consumer } -func (d *dependencyInfo) Registration() ServiceRegistration { +func (d *dependencyInfo) Registration() types.ServiceRegistration { return d.registration } -func (d *dependencyInfo) AddRequiredServiceInfo(child DependencyInfo) { +func (d *dependencyInfo) AddRequiredServiceInfo(child types.DependencyInfo) { d.m.Lock() defer d.m.Unlock() d.children = append(d.children, child) @@ -79,7 +71,7 @@ func (d *dependencyInfo) SetInstance(instance interface{}) error { d.m.Lock() defer d.m.Unlock() if d.instance != nil { - return errors.New(ErrorInstanceAlreadySet) + return types.NewDependencyError(types.ErrorInstanceAlreadySet) } d.instance = instance return nil diff --git a/pkg/registry.go b/pkg/registration/registry.go similarity index 83% rename from pkg/registry.go rename to pkg/registration/registry.go index fd43c37..0729d1a 100644 --- a/pkg/registry.go +++ b/pkg/registration/registry.go @@ -1,13 +1,13 @@ -package pkg +package registration import ( - "github.com/matzefriedrich/parsley/internal" + "github.com/matzefriedrich/parsley/internal/core" "github.com/matzefriedrich/parsley/pkg/types" "reflect" ) type serviceRegistry struct { - identifierSource internal.ServiceIdSequence + identifierSource core.ServiceIdSequence registrations map[reflect.Type]types.ServiceRegistration } @@ -23,14 +23,6 @@ func RegisterSingleton(registry types.ServiceRegistry, activatorFunc any) error return registry.Register(activatorFunc, types.LifetimeSingleton) } -func RegisterInstance[T any](registry types.ServiceRegistry, instance T) error { - instanceFunc, err := CreateServiceActivatorFrom[T](instance) - if err != nil { - return err - } - return registry.Register(instanceFunc, types.LifetimeSingleton) -} - func (s *serviceRegistry) Register(activatorFunc any, lifetimeScope types.LifetimeScope) error { registration, err := CreateServiceRegistration(activatorFunc, lifetimeScope) @@ -79,7 +71,7 @@ func (s *serviceRegistry) TryGetServiceRegistration(serviceType reflect.Type) (t func NewServiceRegistry() types.ServiceRegistry { registrations := make(map[reflect.Type]types.ServiceRegistration) return &serviceRegistry{ - identifierSource: internal.NewServiceId(0), + identifierSource: core.NewServiceId(0), registrations: registrations, } } @@ -103,11 +95,5 @@ func (s *serviceRegistry) CreateScope() types.ServiceRegistry { } } -func (s *serviceRegistry) BuildResolver() types.Resolver { - r := NewResolver(s) - _ = RegisterInstance(s, r) - return r -} - var _ types.ServiceRegistry = &serviceRegistry{} var _ types.ServiceRegistryAccessor = &serviceRegistry{} diff --git a/pkg/registry_accessor.go b/pkg/registration/registry_accessor.go similarity index 97% rename from pkg/registry_accessor.go rename to pkg/registration/registry_accessor.go index 2b343cf..805bb67 100644 --- a/pkg/registry_accessor.go +++ b/pkg/registration/registry_accessor.go @@ -1,4 +1,4 @@ -package pkg +package registration import ( "github.com/matzefriedrich/parsley/pkg/types" diff --git a/pkg/service_registration.go b/pkg/registration/service_registration.go similarity index 86% rename from pkg/service_registration.go rename to pkg/registration/service_registration.go index b9b90ec..b941c47 100644 --- a/pkg/service_registration.go +++ b/pkg/registration/service_registration.go @@ -1,8 +1,9 @@ -package pkg +package registration import ( "errors" "fmt" + "github.com/matzefriedrich/parsley/internal/core" "github.com/matzefriedrich/parsley/pkg/types" "reflect" "strings" @@ -87,19 +88,21 @@ func (s *serviceRegistration) String() string { func CreateServiceRegistration(activatorFunc any, lifetimeScope types.LifetimeScope) (types.ServiceRegistrationSetup, error) { value := reflect.ValueOf(activatorFunc) - info, err := reflectFunctionInfoFrom(value) + info, err := core.ReflectFunctionInfoFrom(value) if err != nil { return nil, types.NewRegistryError(types.ErrorRequiresFunctionValue, types.WithCause(err)) } serviceType := info.ReturnType() - if serviceType.Kind() != reflect.Interface { - return nil, types.NewRegistryError(types.ErrorActivatorFunctionsMustReturnAnInterface) + switch serviceType.Kind() { + case reflect.Func: + return newServiceRegistration(serviceType, lifetimeScope, value), nil + case reflect.Interface: + requiredTypes := info.ParameterTypes() + return newServiceRegistration(serviceType, lifetimeScope, value, requiredTypes...), nil + default: + return nil, types.NewRegistryError(types.ErrorActivatorFunctionInvalidReturnType) } - - requiredTypes := info.ParameterTypes() - - return newServiceRegistration(serviceType, lifetimeScope, value, requiredTypes...), nil } func newServiceRegistration(serviceType reflect.Type, scope types.LifetimeScope, activatorFunc reflect.Value, parameters ...reflect.Type) *serviceRegistration { diff --git a/pkg/types/service_type.go b/pkg/registration/service_type.go similarity index 82% rename from pkg/types/service_type.go rename to pkg/registration/service_type.go index 6da3f02..da1e629 100644 --- a/pkg/types/service_type.go +++ b/pkg/registration/service_type.go @@ -1,4 +1,4 @@ -package types +package registration import "reflect" diff --git a/pkg/registry_test.go b/pkg/registry_test.go deleted file mode 100644 index c787c58..0000000 --- a/pkg/registry_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package pkg - -import ( - "context" - "reflect" - "testing" - - "github.com/matzefriedrich/parsley/internal" - - "github.com/matzefriedrich/parsley/pkg/types" - - "github.com/stretchr/testify/assert" -) - -func Test_ServiceRegistry_register_types(t *testing.T) { - - // Arrange - sut := NewServiceRegistry() - - // Act - _ = RegisterSingleton(sut, NewFoo) - _ = RegisterTransient(sut, NewFooConsumer) - - fooRegistered := sut.IsRegistered(types.ServiceType[Foo]()) - fooConsumerRegistered := sut.IsRegistered(types.ServiceType[FooConsumer]()) - - // Assert - assert.True(t, fooRegistered) - assert.True(t, fooConsumerRegistered) -} - -func Test_Registry_BuildResolver_resolve_type_with_dependencies(t *testing.T) { - - // Arrange - sut := NewServiceRegistry() - - // Act - _ = RegisterTransient(sut, NewFoo) - _ = RegisterTransient(sut, NewFooConsumer) - - // Assert - r := sut.BuildResolver() - parsleyContext := internal.NewScopedContext(context.Background()) - resolved, _ := r.Resolve(parsleyContext, types.ServiceType[FooConsumer]()) - assert.NotNil(t, resolved) - - actual, ok := resolved.(FooConsumer) - assert.True(t, ok) - assert.NotNil(t, actual) -} - -func Test_Registry_BuildResolver_resolve_scoped_from_same_context_must_be_return_same_instance(t *testing.T) { - - // Arrange - sut := NewServiceRegistry() - - // Act - _ = RegisterSingleton(sut, NewFoo) - _ = RegisterScoped(sut, NewFooConsumer) - - // Assert - r := sut.BuildResolver() - parsleyContext := internal.NewScopedContext(context.Background()) - consumer1, _ := r.Resolve(parsleyContext, types.ServiceType[FooConsumer]()) - assert.NotNil(t, consumer1) - - consumer2, _ := r.Resolve(parsleyContext, types.ServiceType[FooConsumer]()) - assert.NotNil(t, consumer2) - - actual, ok := consumer1.(FooConsumer) - assert.True(t, ok) - assert.NotNil(t, actual) - assert.Equal(t, reflect.ValueOf(consumer1).Pointer(), reflect.ValueOf(consumer2).Pointer()) -} - -func Test_Registry_BuildResolver_resolve_scoped_from_different_context_must_be_return_different_instance(t *testing.T) { - - // Arrange - sut := NewServiceRegistry() - - // Act - _ = RegisterSingleton(sut, NewFoo) - _ = RegisterScoped(sut, NewFooConsumer) - - // Assert - r := sut.BuildResolver() - parsleyContext1 := internal.NewScopedContext(context.Background()) - consumer1, _ := r.Resolve(parsleyContext1, types.ServiceType[FooConsumer]()) - assert.NotNil(t, consumer1) - - parsleyContext2 := internal.NewScopedContext(context.Background()) - consumer2, _ := r.Resolve(parsleyContext2, types.ServiceType[FooConsumer]()) - assert.NotNil(t, consumer2) - - actual, ok := consumer1.(FooConsumer) - assert.True(t, ok) - assert.NotNil(t, actual) - assert.NotEqual(t, reflect.ValueOf(consumer1).Pointer(), reflect.ValueOf(consumer2).Pointer()) -} - -func Test_Registry_RegisterModule_registers_collection_of_services(t *testing.T) { - - // Arrange - sut := NewServiceRegistry() - - // Act - fooModule := func(r types.ServiceRegistry) error { - _ = RegisterSingleton(r, NewFoo) - _ = RegisterScoped(r, NewFooConsumer) - return nil - } - - _ = sut.RegisterModule(fooModule) - - fooRegistered := sut.IsRegistered(types.ServiceType[Foo]()) - fooConsumerRegistered := sut.IsRegistered(types.ServiceType[FooConsumer]()) - - // Assert - assert.True(t, fooRegistered) - assert.True(t, fooConsumerRegistered) -} - -func Test_Registry_RegisterInstance_registers_object(t *testing.T) { - - // Arrange - sut := NewServiceRegistry() - - instance := NewFoo() - - // Act - _ = RegisterInstance(sut, instance) - - fooRegistered := sut.IsRegistered(types.ServiceType[Foo]()) - - r := sut.BuildResolver() - - // Arrange - assert.True(t, fooRegistered) - - resolved, _ := r.Resolve(context.Background(), types.ServiceType[Foo]()) - assert.NotNil(t, resolved) - - actual, ok := resolved.(Foo) - assert.True(t, ok) - assert.NotNil(t, actual) - assert.Equal(t, reflect.ValueOf(instance).Pointer(), reflect.ValueOf(actual).Pointer()) -} - -type Foo interface { - Bar() -} - -type foo struct{} - -func (f *foo) Bar() {} - -func NewFoo() Foo { - return &foo{} -} - -type FooConsumer interface { - FooBar() -} - -type fooConsumer struct { - foo Foo -} - -func (fb *fooConsumer) FooBar() { - fb.foo.Bar() -} - -func NewFooConsumer(foo Foo) FooConsumer { - return &fooConsumer{ - foo: foo, - } -} diff --git a/pkg/resolver_options_test.go b/pkg/resolver_options_test.go deleted file mode 100644 index 195d7ac..0000000 --- a/pkg/resolver_options_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package pkg - -import ( - "context" - "github.com/matzefriedrich/parsley/internal" - "github.com/matzefriedrich/parsley/pkg/types" - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_Resolver_ResolveWithOptions_inject_unregistered_service_instance(t *testing.T) { - - // Arrange - sut := NewServiceRegistry() - - // Act - _ = RegisterScoped(sut, NewFooConsumer) - - // Assert - r := sut.BuildResolver() - parsleyContext := internal.NewScopedContext(context.Background()) - consumer1, _ := r.ResolveWithOptions(parsleyContext, types.ServiceType[FooConsumer](), WithInstance[Foo](NewFoo())) - assert.NotNil(t, consumer1) - - actual, ok := consumer1.(FooConsumer) - assert.True(t, ok) - assert.NotNil(t, actual) -} diff --git a/pkg/resolver.go b/pkg/resolving/resolver.go similarity index 80% rename from pkg/resolver.go rename to pkg/resolving/resolver.go index e692011..f44e846 100644 --- a/pkg/resolver.go +++ b/pkg/resolving/resolver.go @@ -1,24 +1,29 @@ -package pkg +package resolving import ( "context" "github.com/matzefriedrich/parsley/internal" + "github.com/matzefriedrich/parsley/internal/core" + "github.com/matzefriedrich/parsley/pkg/registration" "github.com/matzefriedrich/parsley/pkg/types" "reflect" ) type resolver struct { registry types.ServiceRegistry - globalInstances *internal.InstanceBag + globalInstances *core.InstanceBag } func ResolveRequiredService[T any](resolver types.Resolver, ctx context.Context) (T, error) { var nilInstance T t := reflect.TypeOf((*T)(nil)).Elem() - if t.Kind() != reflect.Interface { - return nilInstance, types.NewResolverError(types.ErrorActivatorFunctionsMustReturnAnInterface) + switch t.Kind() { + case reflect.Func: + case reflect.Interface: + default: + return nilInstance, types.NewResolverError(types.ErrorActivatorFunctionInvalidReturnType) } - resolve, err := resolver.Resolve(ctx, types.ServiceType[T]()) + resolve, err := resolver.Resolve(ctx, registration.ServiceType[T]()) if err != nil { return nilInstance, err } @@ -26,10 +31,12 @@ func ResolveRequiredService[T any](resolver types.Resolver, ctx context.Context) } func NewResolver(registry types.ServiceRegistry) types.Resolver { - return &resolver{ + r := &resolver{ registry: registry, - globalInstances: internal.NewGlobalInstanceBag(), + globalInstances: core.NewGlobalInstanceBag(), } + _ = registration.RegisterInstance[types.Resolver](registry, r) + return r } func detectCircularDependency(sr types.ServiceRegistration, consumer types.DependencyInfo) error { @@ -57,7 +64,7 @@ func (r *resolver) createResolverRegistryAccessor(resolverOptions ...types.Resol if err != nil { return nil, err } - return NewMultiRegistryAccessor(r.registry, transientRegistry), nil + return registration.NewMultiRegistryAccessor(r.registry, transientRegistry), nil } return r.registry, nil } @@ -73,23 +80,23 @@ func (r *resolver) ResolveWithOptions(ctx context.Context, serviceType reflect.T return nil, types.NewResolverError("failed to create resolver service registry", types.WithCause(registryErr)) } - registration, found := registry.TryGetServiceRegistration(serviceType) + serviceRegistration, found := registry.TryGetServiceRegistration(serviceType) if !found { return nil, types.NewResolverError(types.ErrorServiceTypeNotRegistered, types.ForServiceType(serviceType.Name())) } makeDependencyInfo := func(sr types.ServiceRegistration, consumer types.DependencyInfo) (types.DependencyInfo, error) { - instance, _ := r.globalInstances.TryResolveInstance(ctx, registration) + instance, _ := r.globalInstances.TryResolveInstance(ctx, serviceRegistration) err := detectCircularDependency(sr, consumer) if err != nil { return nil, err } - return types.NewDependencyInfo(sr, instance, consumer), nil + return registration.NewDependencyInfo(sr, instance, consumer), nil } resolverStack := internal.MakeStack[types.DependencyInfo]() - root, _ := makeDependencyInfo(registration, nil) + root, _ := makeDependencyInfo(serviceRegistration, nil) stack := internal.MakeStack[types.DependencyInfo](root) for stack.Any() { next := stack.Pop() @@ -112,7 +119,7 @@ func (r *resolver) ResolveWithOptions(ctx context.Context, serviceType reflect.T } } - instances := internal.NewInstancesBag(r.globalInstances, types.LifetimeTransient) + instances := core.NewInstancesBag(r.globalInstances, types.LifetimeTransient) for resolverStack.Any() { diff --git a/pkg/resolver_options.go b/pkg/resolving/resolver_options.go similarity index 80% rename from pkg/resolver_options.go rename to pkg/resolving/resolver_options.go index 534bff4..f5b3459 100644 --- a/pkg/resolver_options.go +++ b/pkg/resolving/resolver_options.go @@ -1,6 +1,7 @@ -package pkg +package resolving import ( + "github.com/matzefriedrich/parsley/pkg/registration" "github.com/matzefriedrich/parsley/pkg/types" ) @@ -16,7 +17,7 @@ func applyResolverOptions(registry types.ServiceRegistry, options ...types.Resol func WithInstance[T any](instance T) types.ResolverOptionsFunc { return func(registry types.ServiceRegistry) error { - err := RegisterInstance[T](registry, instance) + err := registration.RegisterInstance[T](registry, instance) if err != nil { return types.NewRegistryError(types.ErrorCannotRegisterTypeWithResolverOptions, types.WithCause(err)) } diff --git a/pkg/types/dependency_error.go b/pkg/types/dependency_error.go new file mode 100644 index 0000000..8a3afcc --- /dev/null +++ b/pkg/types/dependency_error.go @@ -0,0 +1,20 @@ +package types + +import "errors" + +const ( + ErrorInstanceAlreadySet = "instance already set" +) + +var ( + ErrInstanceAlreadySet = errors.New(ErrorInstanceAlreadySet) +) + +type DependencyError struct { + ParsleyError +} + +func NewDependencyError(msg string) error { + err := DependencyError{ParsleyError{msg: msg}} + return err +} diff --git a/pkg/types/resolver_error.go b/pkg/types/resolver_error.go index bbb556d..52e287c 100644 --- a/pkg/types/resolver_error.go +++ b/pkg/types/resolver_error.go @@ -3,25 +3,25 @@ package types import "errors" const ( - ErrorServiceTypeNotRegistered = "service type is not registered" - ErrorRequiredServiceNotRegistered = "required service type is not registered" - ErrorCannotResolveService = "cannot resolve service" - ErrorActivatorFunctionsMustReturnAnInterface = "activator functions must return an interfaces" - ErrorCircularDependencyDetected = "circular dependency detected" - ErrorCannotBuildDependencyGraph = "failed to build dependency graph" - ErrorInstanceCannotBeNil = "instance cannot be nil" - ErrorServiceTypeMustBeInterface = "service type must be an interface" - ErrorCannotRegisterTypeWithResolverOptions = "cannot register type with resolver options" + ErrorServiceTypeNotRegistered = "service type is not registered" + ErrorRequiredServiceNotRegistered = "required service type is not registered" + ErrorCannotResolveService = "cannot resolve service" + ErrorActivatorFunctionInvalidReturnType = "activator function has an invalid return type" + ErrorCircularDependencyDetected = "circular dependency detected" + ErrorCannotBuildDependencyGraph = "failed to build dependency graph" + ErrorInstanceCannotBeNil = "instance cannot be nil" + ErrorServiceTypeMustBeInterface = "service type must be an interface" + ErrorCannotRegisterTypeWithResolverOptions = "cannot register type with resolver options" ) var ( - ErrServiceTypeNotRegistered = errors.New(ErrorServiceTypeNotRegistered) - ErrActivatorFunctionsMustReturnAnInterface = errors.New(ErrorCannotResolveService) - ErrCannotBuildDependencyGraph = errors.New(ErrorCannotBuildDependencyGraph) - ErrCircularDependencyDetected = errors.New(ErrorCircularDependencyDetected) - ErrInstanceCannotBeNil = errors.New(ErrorInstanceCannotBeNil) - ErrServiceTypeMustBeInterface = errors.New(ErrorServiceTypeMustBeInterface) - ErrCannotRegisterTypeWithResolverOptions = errors.New(ErrorCannotRegisterTypeWithResolverOptions) + ErrServiceTypeNotRegistered = errors.New(ErrorServiceTypeNotRegistered) + ErrActivatorFunctionInvalidReturnType = errors.New(ErrorCannotResolveService) + ErrCannotBuildDependencyGraph = errors.New(ErrorCannotBuildDependencyGraph) + ErrCircularDependencyDetected = errors.New(ErrorCircularDependencyDetected) + ErrInstanceCannotBeNil = errors.New(ErrorInstanceCannotBeNil) + ErrServiceTypeMustBeInterface = errors.New(ErrorServiceTypeMustBeInterface) + ErrCannotRegisterTypeWithResolverOptions = errors.New(ErrorCannotRegisterTypeWithResolverOptions) ) type ResolverError struct { diff --git a/pkg/types/types.go b/pkg/types/types.go index e21e04f..342393c 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -13,7 +13,6 @@ type FunctionInfo interface { type ServiceRegistry interface { ServiceRegistryAccessor - BuildResolver() Resolver CreateLinkedRegistry() ServiceRegistry CreateScope() ServiceRegistry IsRegistered(serviceType reflect.Type) bool