Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal of Multiple[T] type to get multiple implementers #47

Merged
merged 6 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 65 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Dependency injection service container for Golang projects.

// SayHello outputs a friendly greeting.
func (s *MyService) SayHello(name string) {
log.Println("Hello,", name)
log.Println("Hello,", name)
}
```
2. Define a service factory.
Expand Down Expand Up @@ -98,9 +98,6 @@ func MyServiceFactory( /* service dependencies */) *MyService {
// MyServiceFactory depends on two services.
func MyServiceFactory(svc1 MyService1, svc2 MyService2) MyService {...}

// MyServiceFactory optionally depends on the service.
func MyServiceFactory(optSvc1 gontainer.Optional[MyService1]) {...}

// MyServiceFactory provides two services.
func MyServiceFactory() (MyService1, MyService2) {...}

Expand All @@ -126,6 +123,43 @@ There are several predefined by container service types that may be used as a de
1. The `gontainer.Resolver` service provides a service to resolve dependencies dynamically.
1. The `gontainer.Invoker` service provides a service to invoke functions dynamically.

In addition, there are several generic types allowing to declare dependencies on a type.

#### Optional Dependency Declaration

The `gontainer.Optional[T]` type allows to depend on a type that may or may not be present.
For example, when developing a factory that uses a telemetry service, this type can be used if the service
is registered in the container. If the telemetry service is not registered, this is not considered an error,
and telemetry initialization can be skipped in the factory.

```go
// MyServiceFactory optionally depends on the service.
func MyServiceFactory(optService1 gontainer.Optional[MyService1]) {
// Get will not produce any error if the MyService1 is not registered
// in the container: it will return zero value for the service type.
service := optSvc1.Get()
}
```

#### Multiple Dependencies Declaration

The `gontainer.Multiple[T]` type allows retrieval of all services that match the type `T`. This feature is
intended to be used when providing concrete service types from multiple factories (e.g., struct pointers like
`*passwordauth.Provider`, `*tokenauth.Provider`) and depending on them as services `Multiple[IProvider]`.
In this case, the length of the `services` slice could be in the range `[0, N]`.

If a concrete non-interface type is specified in `T`, then the length of the slice could only be `[0, N]`
because the container restricts the registration of the same non-interface type more than once.

```go
// MyServiceFactory depends on the all implementing interface types.
func MyServiceFactory(servicesSlice gontainer.Multiple[MyInterface]) {
for _, service := range servicesSlice {

}
}
```

### Services

A service is a functional component of the application, created and managed by a Service Factory.
Expand Down Expand Up @@ -162,13 +196,13 @@ The function serves two primary roles:
```go
// MyServiceFactory is an example of a service function usage.
func MyServiceFactory(ctx context.Context) func () error {
return func () error {
// Await its order in container close.
<-ctx.Done()
return func () error {
// Await its order in container close.
<-ctx.Done()

// Return nil from the `service.Close()`.
return nil
}
// Return nil from the `service.Close()`.
return nil
}
}
```

Expand Down Expand Up @@ -221,34 +255,34 @@ Every handler function could return an `error` which will be joined and returned
```go
// Container defines service container interface.
type Container interface {
// Start initializes every service in the container.
Start() error
// Start initializes every service in the container.
Start() error

// Close closes service container with all services.
// Blocks invocation until the container is closed.
Close() error
// Close closes service container with all services.
// Blocks invocation until the container is closed.
Close() error

// Done is closing after closing of all services.
Done() <-chan struct{}
// Done is closing after closing of all services.
Done() <-chan struct{}

// Factories returns all defined factories.
Factories() []*Factory
// Factories returns all defined factories.
Factories() []*Factory

// Services returns all spawned services.
Services() []any
// Services returns all spawned services.
Services() []any

// Events returns events broker instance.
Events() Events
// Events returns events broker instance.
Events() Events

// Resolver returns service resolver instance.
// If container is not started, only requested services
// will be spawned on `resolver.Resolve(...)` call.
Resolver() Resolver
// Resolver returns service resolver instance.
// If container is not started, only requested services
// will be spawned on `resolver.Resolve(...)` call.
Resolver() Resolver

// Invoker returns function invoker instance.
// If container is not started, only requested services
// will be spawned to invoke the func.
Invoker() Invoker
// Invoker returns function invoker instance.
// If container is not started, only requested services
// will be spawned to invoke the func.
Invoker() Invoker
}
```

Expand Down
10 changes: 0 additions & 10 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,6 @@ type Container interface {
Invoker() Invoker
}

// Optional defines optional service dependency.
type Optional[T any] struct {
value T
}

// Get returns optional service instance.
func (o Optional[T]) Get() T {
return o.value
}

// container implements service container.
type container struct {
ctx context.Context
Expand Down
37 changes: 33 additions & 4 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,30 @@ func TestContainerLifecycle(t *testing.T) {
serviceStarted := atomic.Bool{}
serviceClosed := atomic.Bool{}

svc1 := &testService1{}
svc2 := &testService2{}
svc3 := &testService3{}
svc4 := &testService4{}

container, err := New(
NewService(float64(100500)),
NewFactory(func() string { return "string" }),
NewFactory(func() (int, int64) { return 123, 456 }),
NewFactory(func() *testService1 { return svc1 }),
NewFactory(func() *testService2 { return svc2 }),
NewFactory(func() (*testService3, *testService4) { return svc3, svc4 }),
NewFactory(func(
ctx context.Context,
dep1 float64, dep2 string,
dep3 Optional[int],
dep4 Optional[bool],
dep5 Multiple[interface{ Do2() }],
) any {
equal(t, dep1, float64(100500))
equal(t, dep2, "string")
equal(t, dep3.Get(), 123)
equal(t, dep4.Get(), false)
equal(t, dep5, Multiple[interface{ Do2() }]{svc1, svc2})
factoryStarted.Store(true)
return func() error {
serviceStarted.Store(true)
Expand All @@ -40,7 +50,7 @@ func TestContainerLifecycle(t *testing.T) {
equal(t, container == nil, false)

// Assert factories and services.
equal(t, len(container.Factories()), 7)
equal(t, len(container.Factories()), 10)
equal(t, len(container.Services()), 0)

// Start all factories in the container.
Expand All @@ -49,8 +59,8 @@ func TestContainerLifecycle(t *testing.T) {
equal(t, serviceClosed.Load(), false)

// Assert factories and services.
equal(t, len(container.Factories()), 7)
equal(t, len(container.Services()), 8)
equal(t, len(container.Factories()), 10)
equal(t, len(container.Services()), 12)

// Let factory function start executing in the background.
time.Sleep(time.Millisecond)
Expand All @@ -66,6 +76,25 @@ func TestContainerLifecycle(t *testing.T) {
<-container.Done()

// Assert factories and services.
equal(t, len(container.Factories()), 7)
equal(t, len(container.Factories()), 10)
equal(t, len(container.Services()), 0)
}

type testService1 struct{}

func (t *testService1) Do1() {}
func (t *testService1) Do2() {}
func (t *testService1) Do3() {}

type testService2 struct{}

func (t *testService2) Do1() {}
func (t *testService2) Do2() {}

type testService3 struct{}

func (t *testService3) Do1() {}

type testService4 struct{}

func (t *testService4) Do1() {}
27 changes: 27 additions & 0 deletions multiple.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package gontainer

import (
"reflect"
)

// Multiple defines multiple service dependencies.
type Multiple[T any] []T

// Multiple marks this type as multiple.
func (m Multiple[T]) Multiple() {}

// isMultipleType checks and returns optional box type.
func isMultipleType(typ reflect.Type) (reflect.Type, bool) {
if typ.Kind() == reflect.Slice {
if _, ok := typ.MethodByName("Multiple"); ok {
return typ.Elem(), true
}
}
return nil, false
}

// newOptionalValue boxes an optional factory input to structs.
func newMultipleValue(typ reflect.Type, values []reflect.Value) reflect.Value {
box := reflect.New(typ).Elem()
return reflect.Append(box, values...)
}
42 changes: 42 additions & 0 deletions multiple_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package gontainer

import (
"reflect"
"testing"
)

// TestIsMultipleType tests checking of argument to be multiple.
func TestIsMultipleType(t *testing.T) {
var t1 any
var t2 string
var t3 Multiple[int]

typ := reflect.TypeOf(&t1).Elem()
rtyp, ok := isMultipleType(typ)
equal(t, rtyp, nil)
equal(t, ok, false)

typ = reflect.TypeOf(&t2).Elem()
rtyp, ok = isMultipleType(typ)
equal(t, rtyp, nil)
equal(t, ok, false)

typ = reflect.TypeOf(&t3).Elem()
rtyp, ok = isMultipleType(typ)
equal(t, rtyp, reflect.TypeOf((*int)(nil)).Elem())
equal(t, ok, true)
}

// TestNewMultipleValue tests creation of multiple value.
func TestNewMultipleValue(t *testing.T) {
// When multiple not found.
box := Multiple[string]{}
value := newMultipleValue(reflect.TypeOf(box), nil)
equal(t, value.Interface().(Multiple[string]), Multiple[string](nil))

// When multiple found.
box = Multiple[string]{}
data := []reflect.Value{reflect.ValueOf("result1"), reflect.ValueOf("result2")}
value = newMultipleValue(reflect.TypeOf(box), data)
equal(t, value.Interface().(Multiple[string]), Multiple[string]{"result1", "result2"})
}
48 changes: 48 additions & 0 deletions optional.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package gontainer

import (
"reflect"
"unsafe"
)

// Optional defines optional service dependency.
type Optional[T any] struct {
value T
}

// Get returns optional service instance.
func (o Optional[T]) Get() T {
return o.value
}

// Optional marks this type as optional.
func (o Optional[T]) Optional() {}

// isOptionalType checks and returns optional box type.
func isOptionalType(typ reflect.Type) (reflect.Type, bool) {
if typ.Kind() == reflect.Struct {
if _, ok := typ.MethodByName("Optional"); ok {
if methodValue, ok := typ.MethodByName("Get"); ok {
if methodValue.Type.NumOut() == 1 {
methodType := methodValue.Type.Out(0)
return methodType, true
}
}
}
}
return nil, false
}

// newOptionalValue creates new optional type with a value.
func newOptionalValue(typ reflect.Type, value reflect.Value) reflect.Value {
// Prepare boxing struct for value.
box := reflect.New(typ).Elem()

// Inject factory output value to the boxing struct.
field := box.FieldByName("value")
pointer := unsafe.Pointer(field.UnsafeAddr())
public := reflect.NewAt(field.Type(), pointer)
public.Elem().Set(value)

return box
}
43 changes: 43 additions & 0 deletions optional_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package gontainer

import (
"reflect"
"testing"
)

// TestIsOptionalType tests checking of argument to be optional.
func TestIsOptionalType(t *testing.T) {
var t1 any
var t2 string
var t3 Optional[int]

typ := reflect.TypeOf(&t1).Elem()
rtyp, ok := isOptionalType(typ)
equal(t, rtyp, nil)
equal(t, ok, false)

typ = reflect.TypeOf(&t2).Elem()
rtyp, ok = isOptionalType(typ)
equal(t, rtyp, nil)
equal(t, ok, false)

typ = reflect.TypeOf(&t3).Elem()
rtyp, ok = isOptionalType(typ)
equal(t, rtyp, reflect.TypeOf((*int)(nil)).Elem())
equal(t, ok, true)
}

// TestNewOptionalValue tests creation of optional value.
func TestNewOptionalValue(t *testing.T) {
// When optional not found.
box := Optional[string]{}
data := reflect.New(reflect.TypeOf((*string)(nil)).Elem()).Elem()
value := newOptionalValue(reflect.TypeOf(box), data)
equal(t, value.Interface().(Optional[string]).Get(), "")

// When optional found.
box = Optional[string]{}
data = reflect.ValueOf("result")
value = newOptionalValue(reflect.TypeOf(box), data)
equal(t, value.Interface().(Optional[string]).Get(), "result")
}
Loading
Loading