diff --git a/README.md b/README.md index 89fb90a..f52bcdd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/protoman92/gocompose)](https://goreportcard.com/report/github.com/protoman92/gocompose) [![Build Status](https://travis-ci.org/protoman92/gocompose.svg?branch=master)](https://travis-ci.org/protoman92/gocompose) -[![Coverage Status](https://coveralls.io/repos/github/protoman92/gocompose/badge.svg?branch=master)](https://coveralls.io/github/protoman92/gocompose?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/protoman92/gocompose/badge.svg?branch=master&dummy=true)](https://coveralls.io/github/protoman92/gocompose?branch=master) Composable functions for Go. This is an experimental library that I use mainly for personal projects. diff --git a/compose/callbackFunc.go b/compose/callbackFunc.go new file mode 100644 index 0000000..3cdc71c --- /dev/null +++ b/compose/callbackFunc.go @@ -0,0 +1,38 @@ +package compose + +// CallbackFunc represents an function that accepts a value and perform side. +// effects. This is a specialized form of Func, and thus is constructible from +// a Func. +type CallbackFunc func(interface{}) error + +// CallbackFuncConvertible represents an object that can be converted to a +// CallbackFunc. +type CallbackFuncConvertible interface { + ToCallbackFunc() CallbackFunc +} + +// ToCallbackFunc returns the current CallbackFunc. +func (cf CallbackFunc) ToCallbackFunc() CallbackFunc { + return cf +} + +// Invoke invokes the underlying function. +func (cf CallbackFunc) Invoke(value interface{}) error { + return cf(value) +} + +// ToFunc converts the current CallbackFunc into a ToFunc. +func (cf CallbackFunc) ToFunc() Func { + return func(value interface{}) (interface{}, error) { + err := cf(value) + return nil, err + } +} + +// ToCallbackFunc converts the current Func into an ToCallbackFunc. +func (f Func) ToCallbackFunc() CallbackFunc { + return func(value interface{}) error { + _, err := f.Invoke(value) + return err + } +} diff --git a/compose/callbackFuncF.go b/compose/callbackFuncF.go new file mode 100644 index 0000000..49631f8 --- /dev/null +++ b/compose/callbackFuncF.go @@ -0,0 +1,45 @@ +package compose + +// CallbackFuncF converts an CallbackFunc to another CallbackFunc. +type CallbackFuncF func(CallbackFunc) CallbackFunc + +// CallbackFuncFConvertible represents an object that can be converted to a +// CallbackFuncF. +type CallbackFuncFConvertible interface { + ToCallbackFuncF() CallbackFuncF +} + +// ToCallbackFuncF returns the current CallbackFuncF. +func (cff CallbackFuncF) ToCallbackFuncF() CallbackFuncF { + return cff +} + +// Wrap is a convenience method that calls the underlying CallbackFuncF. +func (cff CallbackFuncF) Wrap(ef CallbackFunc) CallbackFunc { + return cff(ef) +} + +// ToFuncF converts the current CallbackF into a ToFuncF. +func (cff CallbackFuncF) ToFuncF() FuncF { + return func(f Func) Func { + callback := func(value interface{}) error { + _, err := f.Invoke(value) + return err + } + + return cff.Wrap(callback).ToFunc() + } +} + +// ToCallbackFuncF converts a FuncF to an ToCallbackFuncF. This method should be +// used at the last stage of a chain so we do not need to redefine other higher +// order functions. +func (ff FuncF) ToCallbackFuncF() CallbackFuncF { + return func(cf CallbackFunc) CallbackFunc { + var function Func = func(value interface{}) (interface{}, error) { + return nil, cf(value) + } + + return ff.Wrap(function).ToCallbackFunc() + } +} diff --git a/compose/conversion_test.go b/compose/conversion_test.go new file mode 100644 index 0000000..a4942c9 --- /dev/null +++ b/compose/conversion_test.go @@ -0,0 +1,52 @@ +package compose + +import ( + "testing" +) + +func TestConvertBetweenFuncTypes(t *testing.T) { + /// Setup + var errF Func = func(value interface{}) (interface{}, error) { + return valueOp, errOp + } + + /// When + value, err := errF. + ToCallbackFunc(). + ToCallbackFunc(). + ToFunc(). + ToFunc(). + ToSupplyFunc(). + ToSupplyFunc(). + ToFunc(). + Invoke(0) + + /// Then + if err != errOp || value != nil { + t.Errorf("Expected %v, got %v", errOp, err) + } +} + +func TestCovertBetweenFuncF(t *testing.T) { + /// Setup + var errF Func = func(value interface{}) (interface{}, error) { + return valueOp, errOp + } + + /// When + value, err := RetryF(retries). + ToCallbackFuncF(). + ToCallbackFuncF(). + ToFuncF(). + ToFuncF(). + ToSupplyFuncF(). + ToSupplyFuncF(). + ToFuncF(). + Wrap(errF). + Invoke(0) + + /// Then + if err != errOp || value != nil { + t.Errorf("Expected %v, got %v", errOp, err) + } +} diff --git a/compose/errorFunc.go b/compose/errorFunc.go deleted file mode 100644 index b97b36b..0000000 --- a/compose/errorFunc.go +++ /dev/null @@ -1,10 +0,0 @@ -package compose - -// ErrorFunc represents an error-returning function. This is a specialized -// form of Func, and thus is constructible from a Func. -type ErrorFunc func() error - -// Invoke invokes the underlying error function. -func (ef ErrorFunc) Invoke() error { - return ef() -} diff --git a/compose/errorFuncF.go b/compose/errorFuncF.go deleted file mode 100644 index cb77845..0000000 --- a/compose/errorFuncF.go +++ /dev/null @@ -1,27 +0,0 @@ -package compose - -// ErrorFuncF converts an ErrorFunc to another ErrorFunc. -type ErrorFuncF func(ErrorFunc) ErrorFunc - -// Wrap is a convenience method that calls the underlying ErrorFuncF. -func (eff ErrorFuncF) Wrap(ef ErrorFunc) ErrorFunc { - return eff(ef) -} - -// ErrorFuncF converts a FuncF to an ErrorFuncF. This method should be used at -// the last stage of a chain so we do not need to redefine other higher-order -// functions. -func (ff FuncF) ErrorFuncF() ErrorFuncF { - return func(ef ErrorFunc) ErrorFunc { - var function Func = func() (interface{}, error) { - return nil, ef() - } - - wrapped := ff.Wrap(function) - - return func() error { - _, err := wrapped.Invoke() - return err - } - } -} diff --git a/compose/function.go b/compose/function.go index dad1438..f1e2614 100644 --- a/compose/function.go +++ b/compose/function.go @@ -1,18 +1,20 @@ package compose // Func represents an operation that could return an error. -type Func func() (interface{}, error) +type Func func(interface{}) (interface{}, error) -// Invoke is a convenience method to call a Func. Although it is the same as if -// we call the function normally, this may look nicer in a chain. -func (f Func) Invoke() (interface{}, error) { - return f() +// FuncConvertible represents an object that can be converted to a Func. +type FuncConvertible interface { + ToFunc() Func +} + +// ToFunc returns the current Func. +func (f Func) ToFunc() Func { + return f } -// ErrorFunc converts the current function into an ErrorFunc. -func (f Func) ErrorFunc() ErrorFunc { - return func() error { - _, err := f.Invoke() - return err - } +// Invoke is a convenience method to call a Func. Although it is the same as if +// we call the function normally, this may look nicer in a chain. +func (f Func) Invoke(value interface{}) (interface{}, error) { + return f(value) } diff --git a/compose/functionF.go b/compose/functionF.go index 32e9c7d..bf8e863 100644 --- a/compose/functionF.go +++ b/compose/functionF.go @@ -3,20 +3,24 @@ package compose // FuncF transforms a Func into another Func. type FuncF func(Func) Func +// FuncFConvertible represents an object that can be converted to a FuncF. +type FuncFConvertible interface { + ToFuncF() FuncF +} + +// ToFuncF returns the current FuncF. +func (ff FuncF) ToFuncF() FuncF { + return ff +} + // Compose composes the functionalities of both FuncF. We can use this to chain // enhance a base Func without exposing implementation details. -func (ff FuncF) Compose(selector FuncF) FuncF { +func (ff FuncF) Compose(selector FuncFConvertible) FuncF { return func(f Func) Func { - return ff(selector(f)) + return ff(selector.ToFuncF().Wrap(f)) } } -// ComposeFn is similar to Compose, but it is more convenient when we deal with -// functions that return FuncF. -func (ff FuncF) ComposeFn(selectorFn func() FuncF) FuncF { - return ff.Compose(selectorFn()) -} - // Wrap is a convenience method to invoke the wrap on a Func. This may look // nicer in a function chain. func (ff FuncF) Wrap(f Func) Func { diff --git a/compose/functionF_test.go b/compose/functionF_test.go index 67938ce..e2ce261 100644 --- a/compose/functionF_test.go +++ b/compose/functionF_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestCompose(t *testing.T) { +func TestComposeFuncF(t *testing.T) { published := 0 retryF := RetryF(retries) @@ -18,7 +18,7 @@ func TestCompose(t *testing.T) { } /// When && Then 1 - retryF.Compose(publishF).ComposeFn(NoopF)(errF)() + retryF.Compose(publishF).Compose(NoopF()).ToSupplyFuncF().Wrap(errF).Invoke() if uint(published) != retries+1 { t.Errorf("Expected %d, got %d", retries+1, published) @@ -26,29 +26,30 @@ func TestCompose(t *testing.T) { /// When && Then 2 published = 0 - publishF.Compose(retryF).ComposeFn(NoopF)(errF)() + + publishF.Compose(retryF).Compose(NoopF()).ToSupplyFuncF().Wrap(errF).Invoke() if published != 1 { t.Errorf("Expected %d, got %d", 1, published) } } -func TestComposeConvertToErrorFuncF(t *testing.T) { +func TestComposeConvertToCallbackFuncF(t *testing.T) { /// Setup - errF := func() error { + errF := func(value interface{}) error { return errOp } - retryF := RetryF(retries).ErrorFuncF() + retryF := RetryF(retries).ToCallbackFuncF() /// When & Then - if err := retryF.Wrap(errF).Invoke(); err != errOp { + if err := retryF.Wrap(errF).Invoke(nil); err != errOp { t.Errorf("Expected %v, got %v", errOp, err) } } func BenchmarkComposition(b *testing.B) { - errF := func() (interface{}, error) { + errF := func(value interface{}) (interface{}, error) { return valueOp, errOp } @@ -62,7 +63,7 @@ func BenchmarkComposition(b *testing.B) { Compose(composeF)(errF) for i := 0; i < b.N; i++ { - composed() + composed(nil) } } diff --git a/compose/function_test.go b/compose/function_test.go index 32d0362..51c7675 100644 --- a/compose/function_test.go +++ b/compose/function_test.go @@ -2,14 +2,14 @@ package compose import "testing" -func TestConvertToErrorFunc(t *testing.T) { +func TestConvertToCallbackFunc(t *testing.T) { /// Setup - var errF Func = func() (interface{}, error) { + var errF Func = func(value interface{}) (interface{}, error) { return valueOp, errOp } /// When & Then - if err := errF.ErrorFunc().Invoke(); err != errOp { + if err := errF.ToCallbackFunc().Invoke(nil); err != errOp { t.Errorf("Expected %v, got %v", errOp, err) } } diff --git a/compose/noop_test.go b/compose/noop_test.go index b72fd33..e41af87 100644 --- a/compose/noop_test.go +++ b/compose/noop_test.go @@ -4,12 +4,12 @@ import "testing" func TestNoop(t *testing.T) { /// Setup - var errF Func = func() (interface{}, error) { + var errF Func = func(value interface{}) (interface{}, error) { return valueOp, errOp } /// When & Then - if _, err := errF.Noop().Invoke(); err != errOp { + if _, err := errF.Noop().Invoke(nil); err != errOp { t.Errorf("Expected %v, got %v", errOp, err) } } diff --git a/compose/publish.go b/compose/publish.go index 1712a48..ad06eab 100644 --- a/compose/publish.go +++ b/compose/publish.go @@ -3,8 +3,8 @@ package compose // PublishF publishes the result of a Func for side effects. func PublishF(callback func(interface{}, error)) FuncF { return func(f Func) Func { - return func() (interface{}, error) { - value, err := f() + return func(value interface{}) (interface{}, error) { + value, err := f(value) callback(value, err) return value, err } diff --git a/compose/publish_test.go b/compose/publish_test.go index 17fa5b7..709c65d 100644 --- a/compose/publish_test.go +++ b/compose/publish_test.go @@ -10,7 +10,7 @@ func TestPublish(t *testing.T) { var publishedValue interface{} var publishedErr error - var errF Func = func() (interface{}, error) { + var errF Func = func(value interface{}) (interface{}, error) { return valueOp, errOp } @@ -21,7 +21,7 @@ func TestPublish(t *testing.T) { } /// When & Then - value, err := errF.Publish(publishF).Retry(retries).Invoke() + value, err := errF.Publish(publishF).Retry(retries).ToSupplyFunc().Invoke() if err != errOp || value != nil { t.Errorf("Expected %v, got %v", errOp, err) diff --git a/compose/retry.go b/compose/retry.go index 855871f..c9f74d6 100644 --- a/compose/retry.go +++ b/compose/retry.go @@ -5,16 +5,16 @@ import "time" // CountRetryF composes a Func with retry capabilities. The error function has // access to the current retry count in its first parameter, which is useful // e.g when we are implementing a delay mechanism. -func CountRetryF(retryCount uint) func(func(uint) (interface{}, error)) Func { - return func(f func(uint) (interface{}, error)) Func { - var retryF func(uint) (interface{}, error) +func CountRetryF(retryCount uint) func(func(uint, interface{}) (interface{}, error)) Func { + return func(f func(uint, interface{}) (interface{}, error)) Func { + var retryF func(uint, interface{}) (interface{}, error) - retryF = func(current uint) (interface{}, error) { - value, err := f(current) + retryF = func(current uint, value interface{}) (interface{}, error) { + value, err := f(current, value) if err != nil { if current < retryCount { - return retryF(current + 1) + return retryF(current+1, value) } return nil, err @@ -23,8 +23,8 @@ func CountRetryF(retryCount uint) func(func(uint) (interface{}, error)) Func { return value, nil } - return func() (interface{}, error) { - return retryF(0) + return func(value interface{}) (interface{}, error) { + return retryF(0, value) } } } @@ -33,8 +33,8 @@ func CountRetryF(retryCount uint) func(func(uint) (interface{}, error)) Func { // count. func RetryF(retryCount uint) FuncF { return func(f Func) Func { - return CountRetryF(retryCount)(func(retry uint) (interface{}, error) { - return f() + return CountRetryF(retryCount)(func(retry uint, value interface{}) (interface{}, error) { + return f(value) }) } } @@ -47,14 +47,14 @@ func (f Func) Retry(retryCount uint) Func { // delayRetry composes a function with retry-delaying capabilities. The output // of the return function can be fed to a CountRetry composition. -func delayRetry(d time.Duration) func(Func) func(uint) (interface{}, error) { - return func(f Func) func(uint) (interface{}, error) { - return func(retry uint) (interface{}, error) { +func delayRetry(d time.Duration) func(Func) func(uint, interface{}) (interface{}, error) { + return func(f Func) func(uint, interface{}) (interface{}, error) { + return func(retry uint, value interface{}) (interface{}, error) { if retry > 0 { time.Sleep(d) } - return f() + return f(value) } } } diff --git a/compose/retry_test.go b/compose/retry_test.go index 8c0d24b..cffa110 100644 --- a/compose/retry_test.go +++ b/compose/retry_test.go @@ -9,13 +9,13 @@ func TestCountRetryCompose(t *testing.T) { /// Setup var currentRetry uint - errF := func(retry uint) (interface{}, error) { + errF := func(retry uint, value interface{}) (interface{}, error) { currentRetry = retry return valueOp, errOp } /// When & Then - if _, err := CountRetryF(retries)(errF)(); err != errOp { + if _, err := CountRetryF(retries)(errF).ToSupplyFunc().Invoke(); err != errOp { t.Errorf("Expected %v, got %v", errOp, err) } @@ -28,7 +28,7 @@ func TestRetryComposeWithInitialError(t *testing.T) { /// Setup invoked := uint(0) - errF := func() (interface{}, error) { + errF := func(value interface{}) (interface{}, error) { defer func() { invoked++ }() @@ -41,7 +41,9 @@ func TestRetryComposeWithInitialError(t *testing.T) { } /// When & Then - if value, err := RetryF(retries)(errF)(); err != nil || value != valueOp { + value, err := RetryF(retries)(errF).ToSupplyFunc().Invoke() + + if err != nil || value != valueOp { t.Errorf("Should not error, but got %v", err) } @@ -54,13 +56,13 @@ func TestRetryComposeWithAllErrors(t *testing.T) { /// Setup invoked := uint(0) - var errF Func = func() (interface{}, error) { + var errF Func = func(value interface{}) (interface{}, error) { invoked++ return valueOp, errOp } /// When & Then - if _, err := errF.Retry(retries).Invoke(); err != errOp { + if _, err := errF.Retry(retries).ToSupplyFunc().Invoke(); err != errOp { t.Errorf("Expected %v, got %v", errOp, err) } @@ -73,14 +75,14 @@ func TestDelayRetry(t *testing.T) { /// Setup currentRetry := uint(2) - errF := func() (interface{}, error) { + errF := func(value interface{}) (interface{}, error) { return valueOp, errOp } /// When & Then start := time.Now() - if _, err := delayRetry(delay)(errF)(currentRetry); err != errOp { + if _, err := delayRetry(delay)(errF)(currentRetry, nil); err != errOp { t.Errorf("Expected %v, got %v", errOp, err) } @@ -93,14 +95,14 @@ func TestDelayRetry(t *testing.T) { func TestDelayRetryForFirstInvocation(t *testing.T) { /// Setup - errF := func() (interface{}, error) { + errF := func(value interface{}) (interface{}, error) { return valueOp, errOp } /// When & Then start := time.Now() - if _, err := delayRetry(delay)(errF)(0); err != errOp { + if _, err := delayRetry(delay)(errF)(0, nil); err != errOp { t.Errorf("Expected %v, got %v", errOp, err) } @@ -113,14 +115,16 @@ func TestDelayRetryForFirstInvocation(t *testing.T) { func TestDelayedRetry(t *testing.T) { /// Setup - var errF Func = func() (interface{}, error) { + var errF Func = func(value interface{}) (interface{}, error) { return valueOp, errOp } /// When & Then start := time.Now() - if _, err := errF.DelayRetry(retries)(delay).Invoke(); err != errOp { + _, err := errF.DelayRetry(retries)(delay).ToSupplyFunc().Invoke() + + if err != errOp { t.Errorf("Expected %v, got %v", errOp, err) } diff --git a/compose/supplyFunc.go b/compose/supplyFunc.go new file mode 100644 index 0000000..3c7e25d --- /dev/null +++ b/compose/supplyFunc.go @@ -0,0 +1,33 @@ +package compose + +// SupplyFunc is a specialized form of Func that produces a value. +type SupplyFunc func() (interface{}, error) + +// SupplyFuncConvertible represents an object that can be converted to a SupplyFunc. +type SupplyFuncConvertible interface { + ToSupplyFunc() SupplyFunc +} + +// ToSupplyFunc returns the current SupplyFunc. +func (sf SupplyFunc) ToSupplyFunc() SupplyFunc { + return sf +} + +// Invoke is a convenience method that calls the underlying supply function. +func (sf SupplyFunc) Invoke() (interface{}, error) { + return sf() +} + +// ToFunc converts the current SupplyFunc into a ToFunc. +func (sf SupplyFunc) ToFunc() Func { + return func(value interface{}) (interface{}, error) { + return sf.Invoke() + } +} + +// ToSupplyFunc converts the current Func into a ToSupplyFunc. +func (f Func) ToSupplyFunc() SupplyFunc { + return func() (interface{}, error) { + return f.Invoke(nil) + } +} diff --git a/compose/supplyFuncF.go b/compose/supplyFuncF.go new file mode 100644 index 0000000..43c10ed --- /dev/null +++ b/compose/supplyFuncF.go @@ -0,0 +1,41 @@ +package compose + +// SupplyFuncF maps a SupplyFunc to another SupplyFunc. +type SupplyFuncF func(SupplyFunc) SupplyFunc + +// SupplyFuncFConvertible represents an object that can be converted to a SupplyFuncF. +type SupplyFuncFConvertible interface { + ToSupplyFuncF() SupplyFuncF +} + +// ToSupplyFuncF returns the current SupplyFuncF. +func (sff SupplyFuncF) ToSupplyFuncF() SupplyFuncF { + return sff +} + +// Wrap is a convenience method to call the underlying SupplyFuncF. +func (sff SupplyFuncF) Wrap(sf SupplyFunc) SupplyFunc { + return sff(sf) +} + +// ToFuncF converts the current SupplyFuncF into a ToFuncF. +func (sff SupplyFuncF) ToFuncF() FuncF { + return func(f Func) Func { + sf := func() (interface{}, error) { + return f(nil) + } + + return sff.Wrap(sf).ToFunc() + } +} + +// ToSupplyFuncF converts the current FuncF to a ToSupplyFuncF. +func (ff FuncF) ToSupplyFuncF() SupplyFuncF { + return func(sf SupplyFunc) SupplyFunc { + f := func(value interface{}) (interface{}, error) { + return sf.Invoke() + } + + return ff.Wrap(f).ToSupplyFunc() + } +}