Skip to content

Commit

Permalink
Feature/named services (#11)
Browse files Browse the repository at this point in the history
* Adds support for named service registrations
* Changes comparison service registrations (Func is always treated as not equal)
* Fixes a bug in the detectCircularDependency helper function, which caused an infinite loop
  • Loading branch information
matzefriedrich authored Jul 26, 2024
1 parent 5899c46 commit 76ea475
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 9 deletions.
16 changes: 11 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions internal/tests/registry_register_named_test.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
58 changes: 58 additions & 0 deletions pkg/features/named_services.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
13 changes: 13 additions & 0 deletions pkg/registration/named_service_registration.go
Original file line number Diff line number Diff line change
@@ -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
}
}
13 changes: 11 additions & 2 deletions pkg/registration/service_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
24 changes: 24 additions & 0 deletions pkg/resolving/named_service_resolver.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
2 changes: 1 addition & 1 deletion pkg/resolving/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
34 changes: 34 additions & 0 deletions pkg/types/parsley_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
}
}
}
5 changes: 5 additions & 0 deletions pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 76ea475

Please sign in to comment.