Skip to content

Commit

Permalink
Fix/resolver inject list (#15)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
matzefriedrich authored Aug 9, 2024
1 parent 91ab986 commit 9e92fb6
Show file tree
Hide file tree
Showing 14 changed files with 238 additions and 68 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions internal/tests/core/function_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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,
}

Expand All @@ -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,
}

Expand Down
30 changes: 30 additions & 0 deletions internal/tests/features/data_services.go
Original file line number Diff line number Diff line change
@@ -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{}
61 changes: 61 additions & 0 deletions internal/tests/features/registry_register_list_test.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
49 changes: 21 additions & 28 deletions internal/tests/features/registry_register_named_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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,
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/features/lazy_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions pkg/features/list_services.go
Original file line number Diff line number Diff line change
@@ -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)
}
19 changes: 9 additions & 10 deletions pkg/registration/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -61,15 +60,15 @@ 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
}

func (s *serviceRegistry) TryGetServiceRegistrations(serviceType types.ServiceType) (types.ServiceRegistrationList, bool) {
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
}
Expand All @@ -89,23 +88,23 @@ 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,
}
}

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,
}
}

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
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/resolving/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions pkg/resolving/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/types/registry_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 9e92fb6

Please sign in to comment.