Skip to content

Commit

Permalink
Implement invoker service (#31)
Browse files Browse the repository at this point in the history
Implement invoker service with invocation result access APIs.

Signed-off-by: Pavel Patrin <[email protected]>
  • Loading branch information
pavelpatrin authored Jun 17, 2024
1 parent aab11ca commit 4b045bc
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 34 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ Dependency injection service container for Golang projects.
log.Fatalf("Failed to start service container: %s", err)
}
```

6. Alternatively to eager start with a `Start()` call it is possible to use `Resolver` or `Invoker` service. It will spawn only explicitly requested services.
```go
var MyService myService
if err := container.Resolver().Resolve(&MyService); err != nil {
log.Fatalf("Failed to resolve MyService dependency: %s", err)
}
myServise.DoSomething()
```
or
```go
if err := container.Invoker().Invoke(func(myService &MyService) {
myServise.DoSomething()
}); err != nil {
log.Fatalf("Failed to invoke a function: %s", err)
}
```

## Key Concepts

Expand Down Expand Up @@ -107,6 +124,7 @@ There are several predefined by container service types that may be used as a de
1. The `gontainer.Events` service provides the events broker. It can be used to send and receive events
inside service container between services or outside from the client code.
1. The `gontainer.Resolver` service provides a service to resolve dependencies dynamically.
1. The `gontainer.Invoker` service provides a service to invoke functions dynamically.

### Container Interface

Expand Down Expand Up @@ -137,10 +155,10 @@ type Container interface {
// will be spawned on `resolver.Resolve(...)` call.
Resolver() Resolver

// Invoke invokes specified function.
// Invoker returns function invoker instance.
// If container is not started, only requested services
// will be spawned to invoke the func.
Invoke(fn any) ([]any, error)
Invoker() Invoker
}
```

Expand Down
49 changes: 21 additions & 28 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package gontainer
import (
"context"
"fmt"
"reflect"
"runtime/debug"
"sync"
)
Expand Down Expand Up @@ -52,12 +51,16 @@ func New(factories ...*Factory) (result Container, err error) {
// Prepare service resolver instance.
resolver := &resolver{ctx: ctx, registry: registry}

// Prepare function invoker instance.
invoker := &invoker{resolver: resolver}

// Prepare service container instance.
container := &container{
ctx: ctx,
cancel: cancel,
events: events,
resolver: resolver,
invoker: invoker,
registry: registry,
}

Expand All @@ -80,6 +83,11 @@ func New(factories ...*Factory) (result Container, err error) {
return nil, fmt.Errorf("failed to register service resolver: %w", err)
}

// Register function invoker instance in the registry.
if err := container.registry.registerFactory(ctx, NewService[Invoker](invoker)); err != nil {
return nil, fmt.Errorf("failed to register function invoker: %w", err)
}

// Register provided factories in the registry.
for _, factory := range factories {
if err := container.registry.registerFactory(ctx, factory); err != nil {
Expand Down Expand Up @@ -115,8 +123,8 @@ type Container interface {
// Resolver returns service resolver instance.
Resolver() Resolver

// Invoke invokes specified function.
Invoke(fn any) ([]any, error)
// Invoker returns function invoker instance.
Invoker() Invoker
}

// Optional defines optional service dependency.
Expand All @@ -142,6 +150,9 @@ type container struct {
// Service resolver.
resolver Resolver

// Function invoker.
invoker Invoker

// Services registry.
registry *registry
}
Expand Down Expand Up @@ -300,34 +311,16 @@ func (c *container) Resolver() Resolver {
}
}

// Invoke invokes specified function.
func (c *container) Invoke(fn any) ([]any, error) {
// Invoker returns function invoker instance.
func (c *container) Invoker() Invoker {
// Acquire read lock.
c.mutex.RLock()
defer c.mutex.RUnlock()

// Get reflection of the fn.
fnValue := reflect.ValueOf(fn)
if fnValue.Kind() != reflect.Func {
return nil, fmt.Errorf("fn must be a function")
}

// Resolve function arguments.
fnInArgs := make([]reflect.Value, 0, fnValue.Type().NumIn())
for i := 0; i < fnValue.Type().NumIn(); i++ {
fnArgValue := reflect.New(fnValue.Type().In(i))
if err := c.resolver.Resolve(fnArgValue.Interface()); err != nil {
return nil, fmt.Errorf("failed to resolve dependency: %w", err)
}
fnInArgs = append(fnInArgs, fnArgValue.Elem())
}

// Convert function results.
fnOutArgs := fnValue.Call(fnInArgs)
results := make([]any, 0, len(fnOutArgs))
for _, fnOut := range fnOutArgs {
results = append(results, fnOut.Interface())
select {
case <-c.ctx.Done():
return nil
default:
return c.invoker
}

return results, nil
}
8 changes: 4 additions & 4 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestContainerLifecycle(t *testing.T) {
equal(t, container == nil, false)

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

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

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

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

// Assert factories and services.
equal(t, len(container.Factories()), 6)
equal(t, len(container.Factories()), 7)
equal(t, len(container.Services()), 0)
}
83 changes: 83 additions & 0 deletions invoker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package gontainer

import (
"fmt"
"reflect"
)

// Invoker defines invoker interface.
type Invoker interface {
// Invoke invokes specified function.
Invoke(fn any) (InvokeResult, error)
}

// invoker implements invoker interface.
type invoker struct {
resolver Resolver
}

// Invoke invokes specified function.
func (i *invoker) Invoke(fn any) (InvokeResult, error) {
// Get reflection of the fn.
fnValue := reflect.ValueOf(fn)
if fnValue.Kind() != reflect.Func {
return nil, fmt.Errorf("fn must be a function")
}

// Resolve function arguments.
fnInArgs := make([]reflect.Value, 0, fnValue.Type().NumIn())
for index := 0; index < fnValue.Type().NumIn(); index++ {
fnArgPtrValue := reflect.New(fnValue.Type().In(index))
if err := i.resolver.Resolve(fnArgPtrValue.Interface()); err != nil {
return nil, fmt.Errorf("failed to resolve dependency: %w", err)
}
fnInArgs = append(fnInArgs, fnArgPtrValue.Elem())
}

// Convert function results.
fnOutArgs := fnValue.Call(fnInArgs)
result := &invokeResult{
values: make([]any, 0, len(fnOutArgs)),
err: nil,
}
for index, fnOut := range fnOutArgs {
// If it is the last return value.
if index == len(fnOutArgs)-1 {
// And type of the value is the error.
if fnOut.Type().Implements(errorType) {
// Use the value as an error.
result.err = fnOut.Interface().(error)
}
}

// Add value to the results slice.
result.values = append(result.values, fnOut.Interface())
}

return result, nil
}

// InvokeResult provides access to the invocation result.
type InvokeResult interface {
// Values returns a slice of function result values.
Values() []any

// Error returns function result error, if any.
Error() error
}

// invokeResult implements corresponding interface.
type invokeResult struct {
values []any
err error
}

// Values implements corresponding interface method.
func (r *invokeResult) Values() []any {
return r.values
}

// Error implements corresponding interface method.
func (r *invokeResult) Error() error {
return r.err
}
92 changes: 92 additions & 0 deletions invoker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package gontainer

import (
"errors"
"testing"
)

// TestInvokerService tests invoker service.
func TestInvokerService(t *testing.T) {
tests := []struct {
name string
haveFn any
wantFn func(t *testing.T, value InvokeResult)
wantErr bool
}{
{
name: "ReturnNothing",
haveFn: func(var1 string, var2 int) {},
wantFn: func(t *testing.T, value InvokeResult) {
equal(t, len(value.Values()), 0)
equal(t, value.Error(), nil)
},
wantErr: false,
},
{
name: "ReturnValuesNoError",
haveFn: func(var1 string, var2 int) (string, int, bool) {
return var1 + "-X", var2 + 100, true
},
wantFn: func(t *testing.T, value InvokeResult) {
equal(t, len(value.Values()), 3)
equal(t, value.Values()[0], "string-X")
equal(t, value.Values()[1], 223)
equal(t, value.Values()[2], true)
equal(t, value.Error(), nil)
},
wantErr: false,
},
{
name: "ReturnNoValuesWithError",
haveFn: func(var1 string, var2 int) (string, int, error) {
return var1 + "-X", var2 + 100, errors.New("failed")
},
wantFn: func(t *testing.T, value InvokeResult) {
equal(t, len(value.Values()), 3)
equal(t, value.Values()[0], "string-X")
equal(t, value.Values()[1], 223)
equal(t, value.Values()[2].(error).Error(), "failed")
equal(t, value.Error().Error(), "failed")
equal(t, value.Error(), value.Values()[2])
},
wantErr: false,
},
{
name: "ReturnMultipleError",
haveFn: func(var1 string, var2 int) (error, error, error) {
return nil, errors.New("error-1"), errors.New("error-2")
},
wantFn: func(t *testing.T, value InvokeResult) {
equal(t, len(value.Values()), 3)
equal(t, value.Values()[0], nil)
equal(t, value.Values()[1].(error).Error(), "error-1")
equal(t, value.Values()[2].(error).Error(), "error-2")
equal(t, value.Error().Error(), "error-2")
equal(t, value.Error(), value.Values()[2])
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
container, err := New(
NewFactory(func() string { return "string" }),
NewFactory(func() int { return 123 }),
)
equal(t, err, nil)
equal(t, container == nil, false)
defer func() {
equal(t, container.Close(), nil)
}()

result, err := container.Invoker().Invoke(tt.haveFn)
if (err != nil) != tt.wantErr {
t.Errorf("Invoke() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantFn != nil {
tt.wantFn(t, result)
}
})
}
}

0 comments on commit 4b045bc

Please sign in to comment.