Skip to content

Commit

Permalink
Feature/inject non registered types (#5)
Browse files Browse the repository at this point in the history
* Extends the resolver to support injection of objects of unregistered types
  • Loading branch information
matzefriedrich authored Jul 12, 2024
1 parent b780037 commit 037cf96
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 28 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@ 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] - 2024-07-11
## 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
* `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
* 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.
*

## v0.2.0 - 2024-07-11

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ This dependency injection package may become your favorite ingredient for your G
- ✔️ Resolve objects on-demand
- ✔️ Allow consumption of `Resolver` in favor of custom factories
- ⏳ Validate registered services; fail early during application startup if missing registrations are encountered
- Provide parameters for non-registered types and data
- ✔️ Provide instances for non-registered types, use `ResolveWithOptions[T]` insted of `Resolve[T]`
- ⏳ Support multiple service registrations for the same interface
- ⏳ Register named services (mutiple services), resolve via `func(key string) any`
- ⏳ Resolve list of service
Expand Down
21 changes: 21 additions & 0 deletions pkg/activator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package pkg

import (
"github.com/matzefriedrich/parsley/internal"
"github.com/matzefriedrich/parsley/pkg/types"
"reflect"
)

func CreateServiceActivatorFrom[T any](instance T) (func() T, error) {
if internal.IsNil(instance) {
return nil, types.NewRegistryError(types.ErrorInstanceCannotBeNil)
}
t := reflect.TypeOf((*T)(nil)).Elem()
if t.Kind() != reflect.Interface {
return nil, types.NewRegistryError(types.ErrorActivatorFunctionsMustReturnAnInterface)
}
instanceFunc := func() T {
return instance
}
return instanceFunc, nil
}
50 changes: 29 additions & 21 deletions pkg/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,38 +24,27 @@ func RegisterSingleton(registry types.ServiceRegistry, activatorFunc any) error
}

func RegisterInstance[T any](registry types.ServiceRegistry, instance T) error {
if internal.IsNil(instance) {
return types.NewRegistryError(types.ErrorInstanceCannotBeNil)
}
t := reflect.TypeOf((*T)(nil)).Elem()
if t.Kind() != reflect.Interface {
return types.NewRegistryError(types.ErrorActivatorFunctionsMustReturnAnInterface)
}
instanceFunc := func() T {
return instance
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 {

value := reflect.ValueOf(activatorFunc)

info, err := reflectFunctionInfoFrom(value)
registration, err := CreateServiceRegistration(activatorFunc, lifetimeScope)
if err != nil {
return types.NewRegistryError(types.ErrorRequiresFunctionValue, types.WithCause(err))
return err
}

serviceType := info.ReturnType()
if serviceType.Kind() != reflect.Interface {
return types.NewRegistryError(types.ErrorActivatorFunctionsMustReturnAnInterface)
id := s.identifierSource.Next()
setupErr := registration.SetId(id)
if setupErr != nil {
return types.NewRegistryError("failed to set up type registration", types.WithCause(setupErr))
}

requiredTypes := info.ParameterTypes()

registration := newServiceRegistration(serviceType, lifetimeScope, value, requiredTypes...)

registration.id = s.identifierSource.Next()
serviceType := registration.ServiceType()
s.registrations[serviceType] = registration

return nil
Expand Down Expand Up @@ -95,6 +84,25 @@ func NewServiceRegistry() types.ServiceRegistry {
}
}

func (s *serviceRegistry) CreateLinkedRegistry() types.ServiceRegistry {
registrations := make(map[reflect.Type]types.ServiceRegistration)
return &serviceRegistry{
identifierSource: s.identifierSource,
registrations: registrations,
}
}

func (s *serviceRegistry) CreateScope() types.ServiceRegistry {
registrations := make(map[reflect.Type]types.ServiceRegistration)
for serviceType, registration := range s.registrations {
registrations[serviceType] = registration
}
return &serviceRegistry{
identifierSource: s.identifierSource,
registrations: registrations,
}
}

func (s *serviceRegistry) BuildResolver() types.Resolver {
r := NewResolver(s)
_ = RegisterInstance(s, r)
Expand Down
30 changes: 30 additions & 0 deletions pkg/registry_accessor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package pkg

import (
"github.com/matzefriedrich/parsley/pkg/types"
"reflect"
)

type multiRegistryAccessor struct {
registries []types.ServiceRegistryAccessor
}

func (m *multiRegistryAccessor) TryGetServiceRegistration(serviceType reflect.Type) (types.ServiceRegistration, bool) {
for _, registry := range m.registries {
registration, ok := registry.TryGetServiceRegistration(serviceType)
if ok {
return registration, ok
}
}
return nil, false
}

var _ types.ServiceRegistryAccessor = &multiRegistryAccessor{}

func NewMultiRegistryAccessor(registries ...types.ServiceRegistryAccessor) types.ServiceRegistryAccessor {
serviceRegistries := make([]types.ServiceRegistryAccessor, 0)
serviceRegistries = append(serviceRegistries, registries...)
return &multiRegistryAccessor{
registries: serviceRegistries,
}
}
29 changes: 25 additions & 4 deletions pkg/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

type resolver struct {
registry types.ServiceRegistryAccessor
registry types.ServiceRegistry
globalInstances *internal.InstanceBag
}

Expand All @@ -25,7 +25,7 @@ func ResolveRequiredService[T any](resolver types.Resolver, ctx context.Context)
return resolve.(T), err
}

func NewResolver(registry types.ServiceRegistryAccessor) types.Resolver {
func NewResolver(registry types.ServiceRegistry) types.Resolver {
return &resolver{
registry: registry,
globalInstances: internal.NewGlobalInstanceBag(),
Expand All @@ -50,9 +50,30 @@ func detectCircularDependency(sr types.ServiceRegistration, consumer types.Depen
return nil
}

func (r *resolver) createResolverRegistryAccessor(resolverOptions ...types.ResolverOptionsFunc) (types.ServiceRegistryAccessor, error) {
if len(resolverOptions) > 0 {
transientRegistry := r.registry.CreateLinkedRegistry()
err := applyResolverOptions(transientRegistry, resolverOptions...)
if err != nil {
return nil, err
}
return NewMultiRegistryAccessor(r.registry, transientRegistry), nil
}
return r.registry, nil
}

func (r *resolver) Resolve(ctx context.Context, serviceType reflect.Type) (interface{}, error) {
return r.ResolveWithOptions(ctx, serviceType)
}

func (r *resolver) ResolveWithOptions(ctx context.Context, serviceType reflect.Type, resolverOptions ...types.ResolverOptionsFunc) (interface{}, error) {

registry, registryErr := r.createResolverRegistryAccessor(resolverOptions...)
if registryErr != nil {
return nil, types.NewResolverError("failed to create resolver service registry", types.WithCause(registryErr))
}

registration, found := r.registry.TryGetServiceRegistration(serviceType)
registration, found := registry.TryGetServiceRegistration(serviceType)
if !found {
return nil, types.NewResolverError(types.ErrorServiceTypeNotRegistered, types.ForServiceType(serviceType.Name()))
}
Expand All @@ -75,7 +96,7 @@ func (r *resolver) Resolve(ctx context.Context, serviceType reflect.Type) (inter
resolverStack.Push(next)
requiredServices := next.RequiredServiceTypes()
for _, requiredService := range requiredServices {
requiredServiceRegistration, isRegistered := r.registry.TryGetServiceRegistration(requiredService)
requiredServiceRegistration, isRegistered := registry.TryGetServiceRegistration(requiredService)
if isRegistered == false {
return nil, types.NewResolverError(types.ErrorServiceTypeNotRegistered, types.ForServiceType(requiredService.Name()))
}
Expand Down
25 changes: 25 additions & 0 deletions pkg/resolver_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package pkg

import (
"github.com/matzefriedrich/parsley/pkg/types"
)

func applyResolverOptions(registry types.ServiceRegistry, options ...types.ResolverOptionsFunc) error {
for _, option := range options {
err := option(registry)
if err != nil {
return err
}
}
return nil
}

func WithInstance[T any](instance T) types.ResolverOptionsFunc {
return func(registry types.ServiceRegistry) error {
err := RegisterInstance[T](registry, instance)
if err != nil {
return types.NewRegistryError(types.ErrorCannotRegisterTypeWithResolverOptions, types.WithCause(err))
}
return nil
}
}
28 changes: 28 additions & 0 deletions pkg/resolver_options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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)
}
28 changes: 28 additions & 0 deletions pkg/service_registration.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package pkg

import (
"errors"
"fmt"
"github.com/matzefriedrich/parsley/pkg/types"
"reflect"
Expand Down Expand Up @@ -47,6 +48,14 @@ func (s *serviceRegistration) Id() uint64 {
return s.id
}

func (s *serviceRegistration) SetId(id uint64) error {
if s.id != 0 {
return errors.New("the id cannot be changed once set")
}
s.id = id
return nil
}

func (s *serviceRegistration) LifetimeScope() types.LifetimeScope {
return s.lifetimeScope
}
Expand Down Expand Up @@ -75,6 +84,24 @@ func (s *serviceRegistration) String() string {
return buffer.String()
}

func CreateServiceRegistration(activatorFunc any, lifetimeScope types.LifetimeScope) (types.ServiceRegistrationSetup, error) {
value := reflect.ValueOf(activatorFunc)

info, err := 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)
}

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 {
parameterTypeInfos := make([]typeInfo, len(parameters))
for i, p := range parameters {
Expand All @@ -89,3 +116,4 @@ func newServiceRegistration(serviceType reflect.Type, scope types.LifetimeScope,
}

var _ types.ServiceRegistration = &serviceRegistration{}
var _ types.ServiceRegistrationSetup = &serviceRegistration{}
2 changes: 2 additions & 0 deletions pkg/types/resolver_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
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 (
Expand All @@ -20,6 +21,7 @@ var (
ErrCircularDependencyDetected = errors.New(ErrorCircularDependencyDetected)
ErrInstanceCannotBeNil = errors.New(ErrorInstanceCannotBeNil)
ErrServiceTypeMustBeInterface = errors.New(ErrorServiceTypeMustBeInterface)
ErrCannotRegisterTypeWithResolverOptions = errors.New(ErrorCannotRegisterTypeWithResolverOptions)
)

type ResolverError struct {
Expand Down
12 changes: 11 additions & 1 deletion pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ type FunctionInfo interface {
}

type ServiceRegistry interface {
ServiceRegistryAccessor
BuildResolver() Resolver
CreateLinkedRegistry() ServiceRegistry
CreateScope() ServiceRegistry
IsRegistered(serviceType reflect.Type) bool
Register(activatorFunc any, scope LifetimeScope) error
RegisterModule(modules ...ModuleFunc) error
Expand All @@ -21,7 +24,6 @@ type ServiceRegistry interface {
type ModuleFunc func(registry ServiceRegistry) error

type ServiceRegistryAccessor interface {
ServiceRegistry
TryGetServiceRegistration(serviceType reflect.Type) (ServiceRegistration, bool)
}

Expand All @@ -33,10 +35,18 @@ type ServiceRegistration interface {
LifetimeScope() LifetimeScope
}

type ServiceRegistrationSetup interface {
ServiceRegistration
SetId(id uint64) error
}

type RegistrationConfigurationFunc func(r ServiceRegistration)

type ResolverOptionsFunc func(registry ServiceRegistry) error

type Resolver interface {
Resolve(ctx context.Context, serviceType reflect.Type) (interface{}, error)
ResolveWithOptions(ctx context.Context, serviceType reflect.Type, options ...ResolverOptionsFunc) (interface{}, error)
}

type DependencyInfo interface {
Expand Down

0 comments on commit 037cf96

Please sign in to comment.