Skip to content

Commit

Permalink
Improvement and refactoring of service functions (#52)
Browse files Browse the repository at this point in the history
* Improvement and refactoring of service functions.

1. Additional simplified signature of service functions added `func()` in addition to signature `func() error`.
2. New service function requirement added: it must have no receiver methods to distinguish it from user types based on a func kind. Otherwise, this function will be treated as a regular service, will not be wrapped and started in background.
3. Test for disallowed underlying type lookup in container.
  • Loading branch information
pavelpatrin authored Feb 12, 2025
1 parent a4ef112 commit af88996
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 49 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func (s *MyService) Close() error {
### Service Functions

The **Service Function** is a specialized form of service optimized for simpler tasks. Instead of returning a concrete
type object or an interface, the service factory returns a function that conforms to `func() error` type.
type object or an interface, the service factory returns a function that conforms to `func() error` or `func()` type.

The function serves two primary roles:

Expand All @@ -196,8 +196,8 @@ 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 {
func MyServiceFactory(ctx context.Context) func() error {
return func() error {
// Await its order in container close.
<-ctx.Done()

Expand Down
40 changes: 34 additions & 6 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package gontainer

import (
"context"
"fmt"
"sync/atomic"
"testing"
"time"
Expand All @@ -34,6 +35,9 @@ func TestContainerLifecycle(t *testing.T) {
svc2 := &testService2{}
svc3 := &testService3{}
svc4 := &testService4{}
svc5 := testService5(func() error {
return fmt.Errorf("svc5 error")
})

container, err := New(
NewService(float64(100500)),
Expand All @@ -42,19 +46,35 @@ func TestContainerLifecycle(t *testing.T) {
NewFactory(func() *testService1 { return svc1 }),
NewFactory(func() *testService2 { return svc2 }),
NewFactory(func() (*testService3, *testService4) { return svc3, svc4 }),
NewFactory(func() testService5 { return svc5 }),
NewFactory(func(
ctx context.Context,
dep1 float64, dep2 string,
dep1 float64,
dep2 string,
dep3 Optional[int],
dep4 Optional[bool],
dep5 Multiple[interface{ Do2() }],
dep6 testService5,
dep7 interface{ Do5() error },
dep8 Optional[testService5],
dep9 Optional[interface{ Do5() error }],
dep10 Optional[func() error],
) 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})
equal(t, dep6().Error(), "svc5 error")
equal(t, dep6.Do5().Error(), "svc5 error")
equal(t, dep7.Do5().Error(), "svc5 error")
equal(t, dep8.Get()().Error(), "svc5 error")
equal(t, dep8.Get().Do5().Error(), "svc5 error")
equal(t, dep9.Get().Do5().Error(), "svc5 error")
equal(t, dep10.Get(), (func() error)(nil))
factoryStarted.Store(true)

// Service function.
return func() error {
serviceStarted.Store(true)
<-ctx.Done()
Expand All @@ -67,7 +87,7 @@ func TestContainerLifecycle(t *testing.T) {
equal(t, container == nil, false)

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

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

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

// Let factory function start executing in the background.
time.Sleep(time.Millisecond)
Expand All @@ -90,10 +110,14 @@ func TestContainerLifecycle(t *testing.T) {
equal(t, serviceClosed.Load(), true)

// Assert context is closed.
<-container.Done()
select {
case <-container.Done():
default:
t.Fatalf("context is not closed")
}

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

Expand All @@ -115,3 +139,7 @@ func (t *testService3) Do1() {}
type testService4 struct{}

func (t *testService4) Do1() {}

type testService5 func() error

func (t testService5) Do5() error { return t() }
1 change: 1 addition & 0 deletions events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func TestEvents(t *testing.T) {
}

func equal(t *testing.T, a, b any) {
t.Helper()
if !reflect.DeepEqual(a, b) {
t.Fatalf("equal failed: '%v' != '%v'", a, b)
}
Expand Down
85 changes: 55 additions & 30 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,12 @@ func (r *registry) spawnFactory(factory *Factory) error {

// Handle factory out functions as regular objects.
for factoryOutIndex, factoryOutValue := range factoryOutValues {
var err error
factoryOutValues[factoryOutIndex], err = wrapFactoryFunc(factoryOutValue)
if err != nil {
return fmt.Errorf("failed to wrap factory func: %w", err)
if serviceFuncValue, ok := isServiceFunc(factoryOutValue); ok {
serviceFuncResult, err := startServiceFunc(serviceFuncValue)
if err != nil {
return fmt.Errorf("failed to start factory func: %w", err)
}
factoryOutValues[factoryOutIndex] = serviceFuncResult
}
}

Expand All @@ -395,11 +397,11 @@ func (r *registry) spawnFactory(factory *Factory) error {
return nil
}

// function represents service function return value wrapper.
type function chan error
// function represents service func return value wrapper.
type funcResult chan error

// Close awaits service function and returns result error.
func (f function) Close() error {
func (f funcResult) Close() error {
return <-f
}

Expand All @@ -420,36 +422,59 @@ func isContextInterface(typ reflect.Type) bool {
return typ.Kind() == reflect.Interface && typ.Implements(ctxType)
}

// wrapFactoryFunc wraps specified function to the regular service object.
func wrapFactoryFunc(factoryOutValue reflect.Value) (reflect.Value, error) {
// Check specified factory out value elem is a function.
// The factoryOutValue can be an `any` type with a function in the value,
// when the factory declares any return type, or directly a function type,
// when the factory declares explicit func return type. Both cases are OK.
if factoryOutValue.Kind() == reflect.Interface {
if factoryOutValue.Elem().Kind() == reflect.Func {
factoryOutValue = factoryOutValue.Elem()
}
}
if factoryOutValue.Kind() != reflect.Func {
return factoryOutValue, nil
// isServiceFunc returns true when the argument is a service function.
// The `factoryOutValue` can be an `any` type with a function in the value
// (when the factory declares `any` return type), or the `function` type
// (when the factory declares explicit return type).
func isServiceFunc(factoryOutValue reflect.Value) (reflect.Value, bool) {
// Unbox the value if it is an any interface.
if isEmptyInterface(factoryOutValue.Type()) {
factoryOutValue = factoryOutValue.Elem()
}

// Check specified factory out value is a service function.
// It is a programming error, if the function has wrong interface.
factoryOutServiceFn, ok := factoryOutValue.Interface().(func() error)
if !ok {
return factoryOutValue, fmt.Errorf("unexpected signature '%s'", factoryOutValue.Type())
// Check if the result value kind is a func kind.
// The func type must have no user-defined methods,
// because otherwise it could be a service instance
// based on the func type but implements an interface.
if factoryOutValue.Kind() == reflect.Func {
if factoryOutValue.NumMethod() == 0 {
return factoryOutValue, true
}
}

// Prepare a regular object from the function.
fnResult := function(make(chan error))
return reflect.Value{}, false
}

// Run specified function in background and await for return.
go func() { fnResult <- factoryOutServiceFn() }()
// startServiceFunc wraps service function to the regular service object.
func startServiceFunc(serviceFuncValue reflect.Value) (reflect.Value, error) {
// Prepare closable result chan as a service function replacement.
// The service function result error will be returned from the
// result wrapper chan `Close() error` method right to the
// service container on the termination stage.
funcResultChan := funcResult(make(chan error))

// Start the service function in background.
serviceFunc := serviceFuncValue.Interface()
switch serviceFunc := serviceFunc.(type) {
case func() error:
go func() {
err := serviceFunc()
funcResultChan <- err
}()
case func():
go func() {
serviceFunc()
funcResultChan <- nil
}()
default:
return reflect.Value{}, fmt.Errorf(
"unexpected service function signature: %T",
serviceFuncValue.Interface(),
)
}

// Return reflected value of the wrapper.
return reflect.ValueOf(fnResult), nil
return reflect.ValueOf(funcResultChan), nil
}

// errorType contains reflection type for error variable.
Expand Down
126 changes: 116 additions & 10 deletions registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,17 +323,123 @@ func TestIsContextInterface(t *testing.T) {
equal(t, isContextInterface(reflect.TypeOf(&t5).Elem()), true)
}

// TestWrapFactoryFunc tests wrapping of factory functions.
func TestWrapFactoryFunc(t *testing.T) {
var result = errors.New("test")
var svcfunc any = func() error {
return result
// TestIsServiceFunc tests checking of service functions.
func TestIsServiceFunc(t *testing.T) {
svcErr := errors.New("test")
svcFunc := func() error { return svcErr }

tests := []struct {
name string
arg1 func() reflect.Value
want1 reflect.Value
want2 bool
}{{
name: "AnyTypeVarWithFunc",
arg1: func() reflect.Value {
var svcFuncAny any = svcFunc
return reflect.ValueOf(&svcFuncAny).Elem()
},
want1: reflect.ValueOf(svcFunc),
want2: true,
}, {
name: "FuncTypeVarWithFunc",
arg1: func() reflect.Value {
var svcFuncTyped = svcFunc
return reflect.ValueOf(&svcFuncTyped).Elem()
},
want1: reflect.ValueOf(&svcFunc).Elem(),
want2: true,
}, {
name: "FuncWithReceivers",
arg1: func() reflect.Value {
var svcFuncWrapped funcWithReceivers = svcFunc
return reflect.ValueOf(&svcFuncWrapped).Elem()
},
want1: reflect.Value{},
want2: false,
}, {
name: "AnyTypeVarWithInt",
arg1: func() reflect.Value {
var intValue any = 5
return reflect.ValueOf(&intValue).Elem()
},
want1: reflect.Value{},
want2: false,
}, {
name: "IntTypeVarWithInt",
arg1: func() reflect.Value {
intValue := 5
return reflect.ValueOf(&intValue).Elem()
},
want1: reflect.Value{},
want2: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got1, got2 := isServiceFunc(tt.arg1())
if (got1.IsValid() || tt.want1.IsValid()) && (got1.Pointer() != tt.want1.Pointer()) {
t.Errorf("isServiceFunc() got1 = %s, want1 %s", got1, tt.want1)
}
if got2 != tt.want2 {
t.Errorf("isServiceFunc() got2 = %v, want2 %v", got2, tt.want2)
}
})
}
}

// TestWrapServiceFunc tests wrapping of service functions.
func TestWrapServiceFunc(t *testing.T) {
svcErr := errors.New("test")
svcFunc1 := func() error { return svcErr }
svcFunc2 := func() {}

tests := []struct {
name string
arg1 func() reflect.Value
want1 error
want2 error
}{{
name: "AnyTypeVarWithFunc1",
arg1: func() reflect.Value {
var svcFuncAny any = svcFunc1
return reflect.ValueOf(&svcFuncAny).Elem()
},
want1: svcErr,
want2: nil,
}, {
name: "FuncTypeVarWithFunc1",
arg1: func() reflect.Value {
var svcFuncTyped = svcFunc1
return reflect.ValueOf(&svcFuncTyped).Elem()
},
want1: svcErr,
want2: nil,
}, {
name: "FuncTypeVarWithFunc2",
arg1: func() reflect.Value {
var svcFuncTyped = svcFunc2
return reflect.ValueOf(&svcFuncTyped).Elem()
},
want1: nil,
want2: nil,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got1, got2 := startServiceFunc(tt.arg1())
if !reflect.DeepEqual(got1.Interface().(funcResult).Close(), tt.want1) {
t.Errorf("startServiceFunc() got1 = %v, want1 %v", got1, tt.want1)
}
if !reflect.DeepEqual(got2, tt.want2) {
t.Errorf("startServiceFunc() got2 = %v, want2 %v", got2, tt.want2)
}
})
}
}

svcvalue := reflect.ValueOf(&svcfunc).Elem()
wrapper, err := wrapFactoryFunc(svcvalue)
equal(t, err, nil)
// funcWithReceivers is a function type with receivers.
type funcWithReceivers func() error

service := wrapper.Interface().(function)
equal(t, service.Close(), result)
// Error defines example method on the service func.
func (f funcWithReceivers) Error() string {
return "error"
}

0 comments on commit af88996

Please sign in to comment.