diff --git a/CHANGELOG.md b/CHANGELOG.md index 51845e6..e7dbbef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +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] +## [v0.6.0] - 2024-07-26 ### Added -* Adds the `Activate[T]` method which can resolve an instance from an unregistered activator func -* Allows registration and activation of pointer types +* Adds the `Activate[T]` method which can resolve an instance from an unregistered activator func. +* Allows registration and activation of pointer types (to not enforce usage of interfaces as abstractions). +* Adds the `RegisterNamed[T]` method to register services of the same interface and allow to resolve them by name. ### Changes -* Renames the `ServiceType[T]` method to `MakeServiceType[T]`; adds service type representing the reflected type and typename -* Replaces all usages of `reflect.Type` by `ServiceType` in all Parsley interfaces +* Renames the `ServiceType[T]` method to `MakeServiceType[T]`; a service type represents now the reflected type and its name (which makes debugging and understanding service dependencies much easier). +* Replaces all usages of `reflect.Type` by `ServiceType` in all Parsley interfaces. +* Changes the `IsSame` method of the `ServiceRegistration` type; service registrations of type function are always treated as different service types. + +### Fixes + +* Fixes a bug in the `detectCircularDependency` function which could make the method get stuck in an infinite loop. ## v0.5.0 - 2024-07-16 diff --git a/README.md b/README.md index 510f9e6..bcf11cb 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Though dependency injection is less prevalent in Golang (compared to other langu - ⏳ 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 - - ⏳ Register named services (mutiple services), resolve via `func(key string) any` + - ✔️ Register named services (mutiple services), resolve via `func(key string) T` - ✔️ Resolve services as list (default) - ⏳ Support sub-scopes - ⏳ Automatic clean-up diff --git a/internal/tests/registry_register_named_test.go b/internal/tests/registry_register_named_test.go new file mode 100644 index 0000000..4f5eb04 --- /dev/null +++ b/internal/tests/registry_register_named_test.go @@ -0,0 +1,100 @@ +package tests + +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_Registry_register_named_service_resolve_factory(t *testing.T) { + + // Arrange + registry := registration.NewServiceRegistry() + + // Act + err := features.RegisterNamed[dataService](registry, + registration.NamedServiceRegistration("remote", newRemoteDataService, types.LifetimeSingleton), + registration.NamedServiceRegistration("local", newLocalDataService, types.LifetimeTransient)) + + resolver := resolving.NewResolver(registry) + scopedContext := resolving.NewScopedContext(context.Background()) + + namedServiceFactory, _ := resolving.ResolveRequiredService[func(string) (dataService, error)](resolver, scopedContext) + remote, _ := namedServiceFactory("remote") + local, _ := namedServiceFactory("local") + + // Assert + assert.NoError(t, err) + assert.NotNil(t, namedServiceFactory) + assert.NotNil(t, remote) + assert.NotNil(t, local) +} + +func Test_Registry_register_named_service_consume_factory(t *testing.T) { + + // Arrange + registry := registration.NewServiceRegistry() + _ = registration.RegisterSingleton(registry, newController) + _ = features.RegisterNamed[dataService](registry, + registration.NamedServiceRegistration("remote", newRemoteDataService, types.LifetimeSingleton), + registration.NamedServiceRegistration("local", newLocalDataService, types.LifetimeTransient)) + + resolver := resolving.NewResolver(registry) + scopedContext := resolving.NewScopedContext(context.Background()) + + // Act + actual, err := resolving.ResolveRequiredService[*controller](resolver, scopedContext) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, actual) + assert.NotNil(t, actual.remoteDataService) + assert.NotNil(t, actual.localDataService) +} + +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{} + +type controller struct { + remoteDataService dataService + localDataService dataService +} + +func newController(dataServiceFactory func(string) (dataService, error)) *controller { + remote, _ := dataServiceFactory("remote") + local, _ := dataServiceFactory("local") + return &controller{ + remoteDataService: remote, + localDataService: local, + } +} diff --git a/pkg/features/named_services.go b/pkg/features/named_services.go new file mode 100644 index 0000000..fec28d1 --- /dev/null +++ b/pkg/features/named_services.go @@ -0,0 +1,58 @@ +package features + +import ( + "github.com/matzefriedrich/parsley/pkg/registration" + "github.com/matzefriedrich/parsley/pkg/resolving" + "github.com/matzefriedrich/parsley/pkg/types" +) + +type namedService struct { + name string + activatorFunc any +} + +func (n namedService) ActivatorFunc() any { + return n.activatorFunc +} + +func (n namedService) Name() string { + return n.name +} + +func RegisterNamed[T any](registry types.ServiceRegistry, services ...registration.NamedServiceRegistrationFunc) error { + + registrationErrors := make([]error, 0) + + for _, service := range services { + name, serviceActivatorFunc, _ := service() + if len(name) == 0 || serviceActivatorFunc == nil { + return types.NewRegistryError("invalid named service registration") + } + namedActivator := newNamedServiceFactory[T](name, serviceActivatorFunc) + err := registration.RegisterInstance(registry, namedActivator) + if err != nil { + registrationErrors = append(registrationErrors, err) + } + } + + nameServiceResolver := resolving.CreateNamedServiceResolverActivatorFunc[T]() + err := registration.RegisterTransient(registry, nameServiceResolver) + if err != nil { + registrationErrors = append(registrationErrors, err) + } + + if len(registrationErrors) > 0 { + return types.NewRegistryError("failed to register named services", types.WithAggregatedCause(registrationErrors...)) + } + + return nil +} + +func newNamedServiceFactory[T any](name string, activatorFunc any) func() types.NamedService[T] { + return func() types.NamedService[T] { + return &namedService{ + name: name, + activatorFunc: activatorFunc, + } + } +} diff --git a/pkg/registration/named_service_registration.go b/pkg/registration/named_service_registration.go new file mode 100644 index 0000000..844f26e --- /dev/null +++ b/pkg/registration/named_service_registration.go @@ -0,0 +1,13 @@ +package registration + +import ( + "github.com/matzefriedrich/parsley/pkg/types" +) + +type NamedServiceRegistrationFunc func() (name string, activatorFunc any, scope types.LifetimeScope) + +func NamedServiceRegistration(name string, activatorFunc any, scope types.LifetimeScope) NamedServiceRegistrationFunc { + return func() (string, any, types.LifetimeScope) { + return name, activatorFunc, scope + } +} diff --git a/pkg/registration/service_registration.go b/pkg/registration/service_registration.go index 4c48bc9..82585aa 100644 --- a/pkg/registration/service_registration.go +++ b/pkg/registration/service_registration.go @@ -59,7 +59,16 @@ func (s *serviceRegistration) SetId(id uint64) error { func (s *serviceRegistration) IsSame(other types.ServiceRegistration) bool { sr, ok := other.(*serviceRegistration) - return ok && s.activatorFunc.Pointer() == sr.activatorFunc.Pointer() + if ok { + serviceType := sr.serviceType.t + reflectedType := serviceType.ReflectedType() + switch reflectedType.Kind() { + case reflect.Func: + return false + } + return s.activatorFunc.Pointer() == sr.activatorFunc.Pointer() + } + return false } func (s *serviceRegistration) LifetimeScope() types.LifetimeScope { @@ -101,7 +110,7 @@ func CreateServiceRegistration(activatorFunc any, lifetimeScope types.LifetimeSc serviceType := info.ReturnType() switch serviceType.ReflectedType().Kind() { case reflect.Func: - return newServiceRegistration(serviceType, lifetimeScope, value), nil + fallthrough case reflect.Pointer: fallthrough case reflect.Interface: diff --git a/pkg/resolving/named_service_resolver.go b/pkg/resolving/named_service_resolver.go new file mode 100644 index 0000000..58313bb --- /dev/null +++ b/pkg/resolving/named_service_resolver.go @@ -0,0 +1,24 @@ +package resolving + +import ( + "context" + "github.com/matzefriedrich/parsley/pkg/types" +) + +type NamedServiceResolverActivatorFunc[T any] func(types.Resolver) func(string) (T, error) + +func CreateNamedServiceResolverActivatorFunc[T any]() NamedServiceResolverActivatorFunc[T] { + return func(resolver types.Resolver) func(string) (T, error) { + var nilInstance T + requiredServices, _ := ResolveRequiredServices[func() types.NamedService[T]](resolver, context.Background()) + return func(name string) (T, error) { + for _, service := range requiredServices { + s := service() + if s.Name() == name { + return Activate[T](resolver, context.Background(), s.ActivatorFunc()) + } + } + return nilInstance, types.NewResolverError("failed to resolve named service") + } + } +} diff --git a/pkg/resolving/resolver.go b/pkg/resolving/resolver.go index 12d6471..7e15ccb 100644 --- a/pkg/resolving/resolver.go +++ b/pkg/resolving/resolver.go @@ -67,7 +67,7 @@ func detectCircularDependency(sr types.ServiceRegistration, consumer types.Depen if next.Registration().Id() == sr.Id() { return types.NewResolverError(types.ErrorCircularDependencyDetected, types.ForServiceType(next.ServiceTypeName())) } - parent := consumer.Consumer() + parent := next.Consumer() if parent != nil { stack.Push(parent) } diff --git a/pkg/types/parsley_error.go b/pkg/types/parsley_error.go index d1e3356..9205af5 100644 --- a/pkg/types/parsley_error.go +++ b/pkg/types/parsley_error.go @@ -43,3 +43,37 @@ func ForServiceType(serviceType string) ParsleyErrorFunc { } } } + +type ParsleyAggregateError struct { + errors []error + msg string +} + +func (f ParsleyAggregateError) Error() string { + return f.msg +} + +func (f ParsleyAggregateError) Is(err error) bool { + if f.Error() == err.Error() { + return true + } + for _, err := range f.errors { + if errors.Is(err, err) { + return true + } + } + return false +} + +func WithAggregatedCause(errs ...error) ParsleyErrorFunc { + return func(e error) { + var funqErr *ParsleyError + ok := errors.As(e, &funqErr) + if ok { + funqErr.cause = &ParsleyAggregateError{ + errors: errs, + msg: "one or more errors occurred", + } + } + } +} diff --git a/pkg/types/types.go b/pkg/types/types.go index e19950b..67e21fc 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -55,6 +55,11 @@ type ServiceRegistrationSetup interface { SetId(id uint64) error } +type NamedService[T any] interface { + Name() string + ActivatorFunc() any +} + type RegistrationConfigurationFunc func(r ServiceRegistration) type ResolverOptionsFunc func(registry ServiceRegistry) error