diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e8078..c3c33ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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 @@ -13,6 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * 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 diff --git a/README.md b/README.md index f83a7ef..dafa326 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/pkg/activator.go b/pkg/activator.go new file mode 100644 index 0000000..723f2dc --- /dev/null +++ b/pkg/activator.go @@ -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 +} diff --git a/pkg/registry.go b/pkg/registry.go index 62ac8d1..fd43c37 100644 --- a/pkg/registry.go +++ b/pkg/registry.go @@ -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 @@ -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) diff --git a/pkg/registry_accessor.go b/pkg/registry_accessor.go new file mode 100644 index 0000000..2b343cf --- /dev/null +++ b/pkg/registry_accessor.go @@ -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, + } +} diff --git a/pkg/resolver.go b/pkg/resolver.go index 2cfb3ce..e692011 100644 --- a/pkg/resolver.go +++ b/pkg/resolver.go @@ -8,7 +8,7 @@ import ( ) type resolver struct { - registry types.ServiceRegistryAccessor + registry types.ServiceRegistry globalInstances *internal.InstanceBag } @@ -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(), @@ -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())) } @@ -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())) } diff --git a/pkg/resolver_options.go b/pkg/resolver_options.go new file mode 100644 index 0000000..534bff4 --- /dev/null +++ b/pkg/resolver_options.go @@ -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 + } +} diff --git a/pkg/resolver_options_test.go b/pkg/resolver_options_test.go new file mode 100644 index 0000000..195d7ac --- /dev/null +++ b/pkg/resolver_options_test.go @@ -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) +} diff --git a/pkg/service_registration.go b/pkg/service_registration.go index 8567e4d..b9b90ec 100644 --- a/pkg/service_registration.go +++ b/pkg/service_registration.go @@ -1,6 +1,7 @@ package pkg import ( + "errors" "fmt" "github.com/matzefriedrich/parsley/pkg/types" "reflect" @@ -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 } @@ -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 { @@ -89,3 +116,4 @@ func newServiceRegistration(serviceType reflect.Type, scope types.LifetimeScope, } var _ types.ServiceRegistration = &serviceRegistration{} +var _ types.ServiceRegistrationSetup = &serviceRegistration{} diff --git a/pkg/types/resolver_error.go b/pkg/types/resolver_error.go index 4d0407a..bbb556d 100644 --- a/pkg/types/resolver_error.go +++ b/pkg/types/resolver_error.go @@ -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 ( @@ -20,6 +21,7 @@ var ( 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 687361d..e21e04f 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -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 @@ -21,7 +24,6 @@ type ServiceRegistry interface { type ModuleFunc func(registry ServiceRegistry) error type ServiceRegistryAccessor interface { - ServiceRegistry TryGetServiceRegistration(serviceType reflect.Type) (ServiceRegistration, bool) } @@ -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 {