Skip to content

Commit

Permalink
Feature/inject func factory (#6)
Browse files Browse the repository at this point in the history
* Extends the registry and resolver modules to accept registrations for factory methods
* Reorganizes the whole package structure (adds sub-packages, moves integration tests to the internal package)
* Renames test methods
* Fixes typo in error message
  • Loading branch information
matzefriedrich authored Jul 13, 2024
1 parent ea07b7e commit 7474ef2
Show file tree
Hide file tree
Showing 24 changed files with 455 additions and 325 deletions.
40 changes: 24 additions & 16 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,57 @@ 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.4.0 - 2024-07-13

### Added

* Support for factory functions to create instances of services based on input parameters provided at runtime

### Changed

* Reorganizes the whole package structure; adds sub-packages for `registration` and `resolving`. A bunch of types that support the inner functionality of the package have been moved to `internal.`.
* Integration tests are moved to the `internal` package.


## 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
* Service registrations can be bundled in a `ModuleFunc` to register related types as a unit.
* The 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
* 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

### Added

* Resolver can now detect circular dependencies
* Adds helpers to register services with a certain lifetime scope
* The resolver can now detect circular dependencies.
* Adds helpers to register services with a certain lifetime scope.

### Changed

* Registry rejects non-interface types
* The registry rejects non-interface types.

### Fixes

* Fixes error wrapping in custom error types
* Improves error handling for service registry and resolver
* Fixes error wrapping in custom error types.
* Improves error handling for service registry and resolver.


## v0.1.0 - 2024-07-10

### Added

* Adds service registry; the registry can map interfaces to implementation types via constructor functions
* Assign lifetime behaviour to services (singleton, scoped, or transient)
* Adds resolver (container) service
* Adds a service registry; the registry can map interfaces to implementation types via constructor functions.
* Assign lifetime behaviour to services (singleton, scoped, or transient).
* Adds a basic resolver (container) service.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Though dependency injection is less prevalent in Golang (compared to other langu
- ✔️ Bundle type registrations as modules to register them via `RegisterModule` as a unit
- ✔️ Resolve objects on-demand
- ✔️ Allow consumption of `Resolver` in favor of custom factories
- ⏳ Lazy loading objects by injecting dependencies as `Lazy[T]`
- ✔️ Register factory functions to create instances of services based on input parameters provided at runtime
- ⏳ 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
Expand Down
4 changes: 2 additions & 2 deletions pkg/function_info.go → internal/core/function_info.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package pkg
package core

import (
"errors"
Expand All @@ -14,7 +14,7 @@ type functionInfo struct {

var _ types.FunctionInfo = &functionInfo{}

func reflectFunctionInfoFrom(value reflect.Value) (types.FunctionInfo, error) {
func ReflectFunctionInfoFrom(value reflect.Value) (types.FunctionInfo, error) {
funcType := value.Type()
if funcType.Kind() != reflect.Func {
return nil, errors.New("not a function")
Expand Down
2 changes: 1 addition & 1 deletion internal/instances.go → internal/core/instances.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package internal
package core

import (
"context"
Expand Down
2 changes: 1 addition & 1 deletion internal/service_id.go → internal/core/service_id.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package internal
package core

import "sync"

Expand Down
89 changes: 89 additions & 0 deletions internal/tests/registry_register_func_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package tests

import (
"context"
"testing"

"github.com/matzefriedrich/parsley/pkg/registration"
"github.com/matzefriedrich/parsley/pkg/resolving"
"github.com/stretchr/testify/assert"
)

func Test_Registry_RegisterTransient_registers_factory_function_to_resolve_dynamic_dependency_at_runtime(t *testing.T) {

// Arrange
sut := registration.NewServiceRegistry()

_ = registration.RegisterTransient(sut, NewPersonFactory)
_ = registration.RegisterTransient(sut, NewUserController)

r := resolving.NewResolver(sut)

const userId = "123"

// Act
controller, _ := resolving.ResolveRequiredService[UserController](r, context.Background())
model := controller.GetUserInfo(userId)

userServiceFactory, _ := resolving.ResolveRequiredService[func(string) (UserService, error)](r, context.Background())
service, factoryErr := userServiceFactory(userId)

// Assert
assert.NoError(t, factoryErr)
assert.NotNil(t, service)

assert.Equal(t, userId, model.Id)
assert.Equal(t, userId, service.UserId())
}

type userService struct {
id string
}

func (f *userService) UserId() string {
return f.id
}

type userModel struct {
Id string `json:"id"`
Name string `json:"name"`
}

func (f *userService) UserName() string {
return "" // retrieve username from data backend
}

type userController struct {
userServiceFactory func(name string) (UserService, error)
}

func (f *userController) GetUserInfo(userId string) userModel {
userService, _ := f.userServiceFactory(userId)
return userModel{
Id: userId,
Name: userService.UserName(),
}
}

type UserService interface {
UserName() string
UserId() string
}

type UserController interface {
GetUserInfo(userId string) userModel
}

func NewPersonFactory() func(string) (UserService, error) {
return func(userId string) (UserService, error) {
return &userService{
id: userId,
}, nil
}
}

func NewUserController(userServiceFactory func(string) (UserService, error)) UserController {
return &userController{
userServiceFactory: userServiceFactory,
}
}
180 changes: 180 additions & 0 deletions internal/tests/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package tests

import (
"context"
"reflect"
"testing"

"github.com/matzefriedrich/parsley/internal/core"
"github.com/matzefriedrich/parsley/pkg/registration"
"github.com/matzefriedrich/parsley/pkg/resolving"

"github.com/matzefriedrich/parsley/pkg/types"

"github.com/stretchr/testify/assert"
)

func Test_ServiceRegistry_register_types_with_different_lifetime_behavior(t *testing.T) {

// Arrange
sut := registration.NewServiceRegistry()

// Act
_ = registration.RegisterSingleton(sut, NewFoo)
_ = registration.RegisterTransient(sut, NewFooConsumer)

fooRegistered := sut.IsRegistered(registration.ServiceType[Foo]())
fooConsumerRegistered := sut.IsRegistered(registration.ServiceType[FooConsumer]())

// Assert
assert.True(t, fooRegistered)
assert.True(t, fooConsumerRegistered)
}

func Test_Registry_NewResolver_resolve_type_with_dependencies(t *testing.T) {

// Arrange
sut := registration.NewServiceRegistry()

// Act
_ = registration.RegisterTransient(sut, NewFoo)
_ = registration.RegisterTransient(sut, NewFooConsumer)

// Assert
r := resolving.NewResolver(sut)
parsleyContext := core.NewScopedContext(context.Background())
resolved, _ := r.Resolve(parsleyContext, registration.ServiceType[FooConsumer]())
assert.NotNil(t, resolved)

actual, ok := resolved.(FooConsumer)
assert.True(t, ok)
assert.NotNil(t, actual)
}

func Test_Registry_NewResolver_resolve_scoped_from_same_context_must_be_return_same_instance(t *testing.T) {

// Arrange
sut := registration.NewServiceRegistry()

// Act
_ = registration.RegisterSingleton(sut, NewFoo)
_ = registration.RegisterScoped(sut, NewFooConsumer)

// Assert
r := resolving.NewResolver(sut)
parsleyContext := core.NewScopedContext(context.Background())
consumer1, _ := r.Resolve(parsleyContext, registration.ServiceType[FooConsumer]())
assert.NotNil(t, consumer1)

consumer2, _ := r.Resolve(parsleyContext, registration.ServiceType[FooConsumer]())
assert.NotNil(t, consumer2)

actual, ok := consumer1.(FooConsumer)
assert.True(t, ok)
assert.NotNil(t, actual)
assert.Equal(t, reflect.ValueOf(consumer1).Pointer(), reflect.ValueOf(consumer2).Pointer())
}

func Test_Registry_NewResolver_resolve_scoped_from_different_context_must_be_return_different_instance(t *testing.T) {

// Arrange
sut := registration.NewServiceRegistry()

// Act
_ = registration.RegisterSingleton(sut, NewFoo)
_ = registration.RegisterScoped(sut, NewFooConsumer)

// Assert
r := resolving.NewResolver(sut)

parsleyContext1 := core.NewScopedContext(context.Background())
consumer1, _ := r.Resolve(parsleyContext1, registration.ServiceType[FooConsumer]())
assert.NotNil(t, consumer1)

parsleyContext2 := core.NewScopedContext(context.Background())
consumer2, _ := r.Resolve(parsleyContext2, registration.ServiceType[FooConsumer]())
assert.NotNil(t, consumer2)

actual, ok := consumer1.(FooConsumer)
assert.True(t, ok)
assert.NotNil(t, actual)
assert.NotEqual(t, reflect.ValueOf(consumer1).Pointer(), reflect.ValueOf(consumer2).Pointer())
}

func Test_Registry_RegisterModule_registers_collection_of_services(t *testing.T) {

// Arrange
sut := registration.NewServiceRegistry()

// Act
fooModule := func(r types.ServiceRegistry) error {
_ = registration.RegisterSingleton(r, NewFoo)
_ = registration.RegisterScoped(r, NewFooConsumer)
return nil
}

_ = sut.RegisterModule(fooModule)

fooRegistered := sut.IsRegistered(registration.ServiceType[Foo]())
fooConsumerRegistered := sut.IsRegistered(registration.ServiceType[FooConsumer]())

// Assert
assert.True(t, fooRegistered)
assert.True(t, fooConsumerRegistered)
}

func Test_Registry_RegisterInstance_registers_singleton_service_from_object(t *testing.T) {

// Arrange
sut := registration.NewServiceRegistry()

instance := NewFoo()

// Act
_ = registration.RegisterInstance(sut, instance)

fooRegistered := sut.IsRegistered(registration.ServiceType[Foo]())

r := resolving.NewResolver(sut)

// Arrange
assert.True(t, fooRegistered)

resolved, _ := r.Resolve(context.Background(), registration.ServiceType[Foo]())
assert.NotNil(t, resolved)

actual, ok := resolved.(Foo)
assert.True(t, ok)
assert.NotNil(t, actual)
assert.Equal(t, reflect.ValueOf(instance).Pointer(), reflect.ValueOf(actual).Pointer())
}

type Foo interface {
Bar()
}

type foo struct{}

func (f *foo) Bar() {}

func NewFoo() Foo {
return &foo{}
}

type FooConsumer interface {
FooBar()
}

type fooConsumer struct {
foo Foo
}

func (fb *fooConsumer) FooBar() {
fb.foo.Bar()
}

func NewFooConsumer(foo Foo) FooConsumer {
return &fooConsumer{
foo: foo,
}
}
Loading

0 comments on commit 7474ef2

Please sign in to comment.