From 9e92fb6d3f61cd6136ba7a1b1e351899884577de Mon Sep 17 00:00:00 2001 From: Matthias Friedrich <1573457+matzefriedrich@users.noreply.github.com> Date: Sat, 10 Aug 2024 01:01:55 +0200 Subject: [PATCH] Fix/resolver inject list (#15) * Extracts error message and object for failed service registrations * Extends the service_type.go module (changes registration lookup key) * Fixes the handling of types reflected from anonymous functions * Ignores the scope parameter in RegisterLazy (not supported yet) * Extracts error message and object from the activator.go module * Adds the RegisterList method to enable the resolver to inject lists of a specific dependency * Adds another test to verify that named services can be resolved as a list * Removes the scope parameter from the RegisterList method (lists must always be transient objects) --- CHANGELOG.md | 19 ++++++ internal/tests/core/function_info_test.go | 6 +- internal/tests/features/data_services.go | 30 +++++++++ .../features/registry_register_list_test.go | 61 +++++++++++++++++++ .../features/registry_register_named_test.go | 49 +++++++-------- pkg/features/lazy_services.go | 2 +- pkg/features/list_services.go | 14 +++++ pkg/registration/registry.go | 19 +++--- pkg/resolving/activate.go | 2 +- pkg/resolving/resolver.go | 1 + pkg/types/registry_error.go | 2 + pkg/types/resolver_error.go | 36 +++++------ pkg/types/service_type.go | 55 ++++++++++++++--- pkg/types/types.go | 10 +++ 14 files changed, 238 insertions(+), 68 deletions(-) create mode 100644 internal/tests/features/data_services.go create mode 100644 internal/tests/features/registry_register_list_test.go create mode 100644 pkg/features/list_services.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c1061bb..4017760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,27 @@ 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). +## [v0.7.1] - 2024-08-10 + +This version addresses issues with resolving and injecting services as lists. + +### Added + +* Adds the `RegisterList[T]` method to enable the resolver to inject lists of services. While resolving lists of a specific service type was already possible by the `ResolveRequiredServices[T]` method, the consumption of arrays in constructor functions requires an explicit registration. The list registration can be mixed with named service registrations. + +### Changes + +* Changes the key-type used to register and lookup service registrations (uses `ServiceKey` instead of `reflect.Type`). + +* Adds `fmt.Stringer` implementations to registration types to improve the debugging experience. It also fixes the handling of types reflected from anonymous functions. + +* Extracts some registry and resolver errors. + + ## [v0.7.0] - 2024-08-05 +### Added + * Adds the `RegisterLazy[T]` method to register lazy service factories. Use the type `Lazy[T]` to consume a lazy service dependency and call the `Value() T` method on the lazy factory to request the actual service instance. The factory will create the service instance upon the first request, cache it, and return it for subsequent calls using the `Value` method. ## [v0.6.1] - 2024-07-30 diff --git a/internal/tests/core/function_info_test.go b/internal/tests/core/function_info_test.go index 09c58a0..af05cc2 100644 --- a/internal/tests/core/function_info_test.go +++ b/internal/tests/core/function_info_test.go @@ -57,7 +57,7 @@ func Test_FunctionInfo_ReflectFunctionInfoFrom_local_function_returning_an_inter expected := expectedFunctionInfo{ functionName: expectedAnonymousFunctionName, formattedSignatureString: fmt.Sprintf("%s() core.some", expectedAnonymousFunctionName), - returnTypeName: "core.some", + returnTypeName: "some", numParameters: 0, } @@ -73,7 +73,7 @@ func Test_FunctionInfo_ReflectFunctionInfoFrom_named_function_returning_an_inter expected := expectedFunctionInfo{ functionName: expectedFunctionName, formattedSignatureString: fmt.Sprintf("%s() core.some", expectedFunctionName), - returnTypeName: "core.some", + returnTypeName: "some", numParameters: 0, } @@ -89,7 +89,7 @@ func Test_FunctionInfo_ReflectFunctionInfoFrom_named_function_with_parameters_re expected := expectedFunctionInfo{ functionName: expectedFunctionName, formattedSignatureString: fmt.Sprintf("%s(interface {}) core.some", expectedFunctionName), - returnTypeName: "core.some", + returnTypeName: "some", numParameters: 1, } diff --git a/internal/tests/features/data_services.go b/internal/tests/features/data_services.go new file mode 100644 index 0000000..376f1e7 --- /dev/null +++ b/internal/tests/features/data_services.go @@ -0,0 +1,30 @@ +package features + +type dataService interface { + FetchData() string +} + +type remoteDataService struct { +} + +func newRemoteDataService() dataService { + return &remoteDataService{} +} + +func (r *remoteDataService) FetchData() string { + return "data from remote service" +} + +var _ dataService = &remoteDataService{} + +type localDataService struct{} + +func newLocalDataService() dataService { + return &localDataService{} +} + +func (l *localDataService) FetchData() string { + return "data from local service" +} + +var _ dataService = &localDataService{} diff --git a/internal/tests/features/registry_register_list_test.go b/internal/tests/features/registry_register_list_test.go new file mode 100644 index 0000000..9449940 --- /dev/null +++ b/internal/tests/features/registry_register_list_test.go @@ -0,0 +1,61 @@ +package features + +import ( + "context" + "github.com/matzefriedrich/parsley/pkg/features" + "github.com/matzefriedrich/parsley/pkg/registration" + "github.com/matzefriedrich/parsley/pkg/resolving" + "github.com/matzefriedrich/parsley/pkg/types" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_Resolver_register_list_resolver(t *testing.T) { + + // Arrange + registry := registration.NewServiceRegistry() + registry.Register(newLocalDataService, types.LifetimeTransient) + registry.Register(newRemoteDataService, types.LifetimeTransient) + features.RegisterList[dataService](registry) + + resolver := resolving.NewResolver(registry) + ctx := resolving.NewScopedContext(context.Background()) + + // Act + actual, err := resolving.ResolveRequiredService[[]dataService](resolver, ctx) + + // Assert + assert.NoError(t, err) + assert.Len(t, actual, 2) +} + +func Test_Resolver_resolve_multiple_instances_of_type(t *testing.T) { + + // Arrange + registry := registration.NewServiceRegistry() + registry.Register(newLocalDataService, types.LifetimeTransient) + registry.Register(newRemoteDataService, types.LifetimeTransient) + features.RegisterList[dataService](registry) + registry.Register(newControllerWithServiceList, types.LifetimeTransient) + + resolver := resolving.NewResolver(registry) + ctx := resolving.NewScopedContext(context.Background()) + + // Act + actual, err := resolving.ResolveRequiredService[*controllerWithServiceList](resolver, ctx) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, actual) + +} + +type controllerWithServiceList struct { + dataServices []dataService +} + +func newControllerWithServiceList(dataServices []dataService) *controllerWithServiceList { + return &controllerWithServiceList{ + dataServices: dataServices, + } +} diff --git a/internal/tests/features/registry_register_named_test.go b/internal/tests/features/registry_register_named_test.go index 84e9101..d7dd4c7 100644 --- a/internal/tests/features/registry_register_named_test.go +++ b/internal/tests/features/registry_register_named_test.go @@ -38,7 +38,7 @@ func Test_Registry_register_named_service_consume_factory(t *testing.T) { // Arrange registry := registration.NewServiceRegistry() - _ = registration.RegisterSingleton(registry, newController) + _ = registration.RegisterSingleton(registry, newControllerWithNamedServiceFactory) _ = features.RegisterNamed[dataService](registry, registration.NamedServiceRegistration("remote", newRemoteDataService, types.LifetimeSingleton), registration.NamedServiceRegistration("local", newLocalDataService, types.LifetimeTransient)) @@ -47,7 +47,7 @@ func Test_Registry_register_named_service_consume_factory(t *testing.T) { scopedContext := resolving.NewScopedContext(context.Background()) // Act - actual, err := resolving.ResolveRequiredService[*controller](resolver, scopedContext) + actual, err := resolving.ResolveRequiredService[*controllerWithNamedServices](resolver, scopedContext) // Assert assert.NoError(t, err) @@ -56,7 +56,7 @@ func Test_Registry_register_named_service_consume_factory(t *testing.T) { assert.NotNil(t, actual.localDataService) } -func Test_Registry_register_named_service_resolve_as_list(t *testing.T) { +func Test_Registry_register_named_service_resolve_all_named_services(t *testing.T) { // Arrange registry := registration.NewServiceRegistry() @@ -83,44 +83,37 @@ func Test_Registry_register_named_service_resolve_as_list(t *testing.T) { assert.Equal(t, "data from local service", local.FetchData()) } -type dataService interface { - FetchData() string -} - -type remoteDataService struct { -} - -func newRemoteDataService() dataService { - return &remoteDataService{} -} +func Test_Registry_register_named_service_resolve_all_named_services_as_list(t *testing.T) { -func (r *remoteDataService) FetchData() string { - return "data from remote service" -} + // Arrange + registry := registration.NewServiceRegistry() + _ = features.RegisterNamed[dataService](registry, + registration.NamedServiceRegistration("remote", newRemoteDataService, types.LifetimeSingleton), + registration.NamedServiceRegistration("local", newLocalDataService, types.LifetimeTransient)) -var _ dataService = &remoteDataService{} + features.RegisterList[dataService](registry) -type localDataService struct{} + resolver := resolving.NewResolver(registry) + scopedContext := resolving.NewScopedContext(context.Background()) -func newLocalDataService() dataService { - return &localDataService{} -} + // Act + actual, err := resolving.ResolveRequiredService[[]dataService](resolver, scopedContext) -func (l *localDataService) FetchData() string { - return "data from local service" + // Assert + assert.NoError(t, err) + assert.NotNil(t, actual) + assert.Equal(t, 2, len(actual)) } -var _ dataService = &localDataService{} - -type controller struct { +type controllerWithNamedServices struct { remoteDataService dataService localDataService dataService } -func newController(dataServiceFactory func(string) (dataService, error)) *controller { +func newControllerWithNamedServiceFactory(dataServiceFactory func(string) (dataService, error)) *controllerWithNamedServices { remote, _ := dataServiceFactory("remote") local, _ := dataServiceFactory("local") - return &controller{ + return &controllerWithNamedServices{ remoteDataService: remote, localDataService: local, } diff --git a/pkg/features/lazy_services.go b/pkg/features/lazy_services.go index 70990f4..359b478 100644 --- a/pkg/features/lazy_services.go +++ b/pkg/features/lazy_services.go @@ -35,7 +35,7 @@ type Lazy[T any] interface { var _ Lazy[any] = &lazy[any]{} -func RegisterLazy[T any](registry types.ServiceRegistry, activatorFunc func() T, scope types.LifetimeScope) error { +func RegisterLazy[T any](registry types.ServiceRegistry, activatorFunc func() T, _ types.LifetimeScope) error { lazyActivator := newLazyServiceFactory[T](activatorFunc) err := registration.RegisterInstance(registry, lazyActivator) diff --git a/pkg/features/list_services.go b/pkg/features/list_services.go new file mode 100644 index 0000000..957d871 --- /dev/null +++ b/pkg/features/list_services.go @@ -0,0 +1,14 @@ +package features + +import ( + "context" + "github.com/matzefriedrich/parsley/pkg/resolving" + "github.com/matzefriedrich/parsley/pkg/types" +) + +func RegisterList[T any](registry types.ServiceRegistry) error { + return registry.Register(func(resolver types.Resolver) []T { + services, _ := resolving.ResolveRequiredServices[T](resolver, context.Background()) + return services + }, types.LifetimeTransient) +} diff --git a/pkg/registration/registry.go b/pkg/registration/registry.go index e5a7fcb..658f199 100644 --- a/pkg/registration/registry.go +++ b/pkg/registration/registry.go @@ -3,12 +3,11 @@ package registration import ( "github.com/matzefriedrich/parsley/internal/core" "github.com/matzefriedrich/parsley/pkg/types" - "reflect" ) type serviceRegistry struct { identifierSource core.ServiceIdSequence - registrations map[reflect.Type]types.ServiceRegistrationList + registrations map[types.ServiceKey]types.ServiceRegistrationList } func RegisterTransient(registry types.ServiceRegistry, activatorFunc any) error { @@ -24,12 +23,12 @@ func RegisterSingleton(registry types.ServiceRegistry, activatorFunc any) error } func (s *serviceRegistry) addOrUpdateServiceRegistrationListFor(serviceType types.ServiceType) types.ServiceRegistrationList { - list, exists := s.registrations[serviceType.ReflectedType()] + list, exists := s.registrations[serviceType.LookupKey()] if exists { return list } list = NewServiceRegistrationList(s.identifierSource) - s.registrations[serviceType.ReflectedType()] = list + s.registrations[serviceType.LookupKey()] = list return list } @@ -44,7 +43,7 @@ func (s *serviceRegistry) Register(activatorFunc any, lifetimeScope types.Lifeti list := s.addOrUpdateServiceRegistrationListFor(serviceType) addRegistrationErr := list.AddRegistration(registration) if addRegistrationErr != nil { - return types.NewRegistryError("failed to register type", types.WithCause(addRegistrationErr)) + return types.NewRegistryError(types.ErrorFailedToRegisterType, types.WithCause(addRegistrationErr)) } return nil @@ -61,7 +60,7 @@ func (s *serviceRegistry) RegisterModule(modules ...types.ModuleFunc) error { } func (s *serviceRegistry) IsRegistered(serviceType types.ServiceType) bool { - _, found := s.registrations[serviceType.ReflectedType()] + _, found := s.registrations[serviceType.LookupKey()] return found } @@ -69,7 +68,7 @@ func (s *serviceRegistry) TryGetServiceRegistrations(serviceType types.ServiceTy if s.IsRegistered(serviceType) == false { return nil, false } - list, found := s.registrations[serviceType.ReflectedType()] + list, found := s.registrations[serviceType.LookupKey()] if found && list.IsEmpty() == false { return list, true } @@ -89,7 +88,7 @@ func (s *serviceRegistry) TryGetSingleServiceRegistration(serviceType types.Serv } func NewServiceRegistry() types.ServiceRegistry { - registrations := make(map[reflect.Type]types.ServiceRegistrationList) + registrations := make(map[types.ServiceKey]types.ServiceRegistrationList) return &serviceRegistry{ identifierSource: core.NewServiceId(0), registrations: registrations, @@ -97,7 +96,7 @@ func NewServiceRegistry() types.ServiceRegistry { } func (s *serviceRegistry) CreateLinkedRegistry() types.ServiceRegistry { - registrations := make(map[reflect.Type]types.ServiceRegistrationList) + registrations := make(map[types.ServiceKey]types.ServiceRegistrationList) return &serviceRegistry{ identifierSource: s.identifierSource, registrations: registrations, @@ -105,7 +104,7 @@ func (s *serviceRegistry) CreateLinkedRegistry() types.ServiceRegistry { } func (s *serviceRegistry) CreateScope() types.ServiceRegistry { - registrations := make(map[reflect.Type]types.ServiceRegistrationList) + registrations := make(map[types.ServiceKey]types.ServiceRegistrationList) for serviceType, registration := range s.registrations { registrations[serviceType] = registration } diff --git a/pkg/resolving/activate.go b/pkg/resolving/activate.go index c69e3de..8dd0f0c 100644 --- a/pkg/resolving/activate.go +++ b/pkg/resolving/activate.go @@ -13,7 +13,7 @@ func Activate[T any](resolver types.Resolver, ctx context.Context, activatorFunc lifetimeScope := types.LifetimeTransient registration, registrationErr := registration.CreateServiceRegistration(activatorFunc, lifetimeScope) if registrationErr != nil { - return nilInstance, types.NewResolverError("failed to create instance of unregistered type", types.WithCause(registrationErr)) + return nilInstance, types.NewResolverError(types.ErrorCannotCreateInstanceOfUnregisteredType, types.WithCause(registrationErr)) } serviceType := registration.ServiceType() diff --git a/pkg/resolving/resolver.go b/pkg/resolving/resolver.go index 6015f4b..c4111bd 100644 --- a/pkg/resolving/resolver.go +++ b/pkg/resolving/resolver.go @@ -20,6 +20,7 @@ func ResolveRequiredServices[T any](resolver types.Resolver, ctx context.Context case reflect.Func: case reflect.Interface: case reflect.Pointer: + case reflect.Slice: default: return []T{}, types.NewResolverError(types.ErrorActivatorFunctionInvalidReturnType) } diff --git a/pkg/types/registry_error.go b/pkg/types/registry_error.go index c781f5d..077cd87 100644 --- a/pkg/types/registry_error.go +++ b/pkg/types/registry_error.go @@ -7,12 +7,14 @@ const ( ErrorCannotRegisterModule = "failed to register module" ErrorTypeAlreadyRegistered = "type already registered" ErrorServiceAlreadyLinkedWithAnotherList = "service already linked with another list" + ErrorFailedToRegisterType = "failed to register type" ) var ( ErrRequiresFunctionValue = errors.New(ErrorRequiresFunctionValue) ErrCannotRegisterModule = errors.New(ErrorCannotRegisterModule) ErrTypeAlreadyRegistered = errors.New(ErrorTypeAlreadyRegistered) + ErrFailedToRegisterType = errors.New(ErrorFailedToRegisterType) ) type registryError struct { diff --git a/pkg/types/resolver_error.go b/pkg/types/resolver_error.go index f91b8b0..8d0d155 100644 --- a/pkg/types/resolver_error.go +++ b/pkg/types/resolver_error.go @@ -3,26 +3,28 @@ package types import "errors" const ( - ErrorServiceTypeNotRegistered = "service type is not registered" - ErrorRequiredServiceNotRegistered = "required service type is not registered" - ErrorCannotResolveService = "cannot resolve service" - ErrorAmbiguousServiceInstancesResolved = "the resolve operation resulted in multiple service instances" - 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" + ErrorServiceTypeNotRegistered = "service type is not registered" + ErrorRequiredServiceNotRegistered = "required service type is not registered" + ErrorCannotResolveService = "cannot resolve service" + ErrorAmbiguousServiceInstancesResolved = "the resolve operation resulted in multiple service instances" + 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" + ErrorCannotCreateInstanceOfUnregisteredType = "failed to create instance of unregistered type" ) var ( - 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) + 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) + ErrCannotCreateInstanceOfUnregisteredType = errors.New(ErrorCannotCreateInstanceOfUnregisteredType) ) type ResolverError struct { diff --git a/pkg/types/service_type.go b/pkg/types/service_type.go index 0b7db03..64b5a5e 100644 --- a/pkg/types/service_type.go +++ b/pkg/types/service_type.go @@ -1,12 +1,26 @@ package types import ( + "fmt" "reflect" ) type serviceType struct { reflectedType reflect.Type name string + packagePath string + list bool + lookupKey ServiceKey +} + +func (s serviceType) String() string { + return fmt.Sprintf("Name: \"%s\", Package: \"%s\", List: %t)", s.name, s.packagePath, s.list) +} + +var _ ServiceType = &serviceType{} + +func (s serviceType) LookupKey() ServiceKey { + return s.lookupKey } func (s serviceType) ReflectedType() reflect.Type { @@ -17,28 +31,53 @@ func (s serviceType) Name() string { return s.name } +func (s serviceType) PackagePath() string { + return s.packagePath +} + func MakeServiceType[T any]() ServiceType { elem := reflect.TypeOf(new(T)).Elem() - return &serviceType{ - reflectedType: elem, - name: elem.String(), - } + return ServiceTypeFrom(elem) } func ServiceTypeFrom(t reflect.Type) ServiceType { - name := "" + isList := false + elemType := t switch t.Kind() { case reflect.Ptr: - name = t.Elem().String() + elemType = t.Elem() case reflect.Interface: - name = t.String() + break case reflect.Func: - name = t.String() + break + case reflect.Slice: + t = t.Elem() + isList = true default: panic("unsupported type: " + t.String()) } + return newServiceType(t, elemType, isList) +} + +func newServiceType(t reflect.Type, elemType reflect.Type, isList bool) ServiceType { + packagePath := t.PkgPath() + if len(packagePath) == 0 { + packagePath = "anonymous" + } + name := elemType.Name() + if len(name) == 0 { + name = t.String() + } + key := fmt.Sprintf("%s.%s", packagePath, name) + if isList { + key = fmt.Sprintf("%s.%s[]", packagePath, name) + } + serviceKey := ServiceKey{value: key} return &serviceType{ reflectedType: t, name: name, + packagePath: packagePath, + list: isList, + lookupKey: serviceKey, } } diff --git a/pkg/types/types.go b/pkg/types/types.go index 6b660d8..bbf0dee 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -19,9 +19,19 @@ type FunctionParameterInfo interface { Type() ServiceType } +type ServiceKey struct { + value string +} + +func (s ServiceKey) String() string { + return s.value +} + type ServiceType interface { Name() string + PackagePath() string ReflectedType() reflect.Type + LookupKey() ServiceKey } type ServiceRegistry interface {