diff --git a/README.md b/README.md index 4c7d29c..4609d0e 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,85 @@ With this package, you can chain a series of functions to build a pipeline -### Example 1 +## Using thennable for error handling / control flow + +Consider the following example + +```go +func main() { + resultOfDoingThis, err := DoThis() + if err != nil { + //log the error + return + } + + resultOfDoingThat err := DoThat(resultOfDoingThis) + if err != nil { + //log the error + return + } + + resultOfDoingAnother err := DoAnother(resultOfDoingThat) + if err != nil { + //log the error + return + } +} +``` + +The above example is the idiomatic way of error handling in go. +Some like it, but most hate it because we need to manually check and return from error one by one. + +Now consider the following example using thennable + +```go +func main() { + result, err := thennable.Start(). + Then(DoThis). + Then(DoThat). + Then(DoAnother). + End() +} +``` + +In the above example, `err` will either be from `DoThis`, `DoThat`, or `DoAnother` function. + +* `DoThat` and `DoAnother` won't be executed if `DoThis` produce an error. +* `DoAnother` won't be executed if `DoThat` produce an error. +* Or you can bypass the error and keep running next function by setting `BreakOnError(true)` in the thennable pipeline. () + +To handle error can either write extra lines like: + +```go + if err != nil { + //log the error + return + } +``` + +or better yet, you can use the built in `Handle` function so the code will looks like: + +```go +func GlobalErrorHandler(err error) { + log.Println(err) +} + +func main() { + result, err := thennable.Start(). + Then(DoThis). + Then(DoThat). + Then(DoAnother). + Handle(GlobalErrorHandler) + End() +} +``` + +## Example 1 ```go import ( "fmt" - "thennable" + thennable "github.com/bastianrob/go-thennable" ) func AddOne(num int) (int, error) { @@ -16,9 +89,9 @@ func AddOne(num int) (int, error) { func Decide(num int) (string, error) { if num == 1 { - return "one" + return "one", nil else { - return "not one" + return "not one", nil } } @@ -33,12 +106,12 @@ func main() { } ``` -### Example 2: Inlining +## Example 2: Inlining ```go import ( "fmt" - "thennable" + thennable "github.com/bastianrob/go-thennable" ) func main() { @@ -55,13 +128,13 @@ func main() { } ``` -### Example 3: Error Occurred +## Example 3: Error Occurred ```go import ( "fmt" "errors" - "thennable" + thennable "github.com/bastianrob/go-thennable" ) func AddOne(num int) (int, error) { @@ -83,14 +156,13 @@ func main() { } ``` - -### Example 4: Recover from error +## Example 4: Recover from error ```go import ( "fmt" "errors" - "thennable" + thennable "github.com/bastianrob/go-thennable" ) func AddOne(num int) (int, error) { @@ -108,9 +180,8 @@ func main() { Then(AddOne). //8 + 1 Then(AddOne). //9 + 1 End() - + fmt.Printf("Res: %v, Str: %s, Err: %v", result, str, err) //Res: [10], Err: nil } ``` - diff --git a/thennable.go b/thennable.go index 4723a02..b64a3c9 100644 --- a/thennable.go +++ b/thennable.go @@ -1,13 +1,14 @@ package thennable import ( - "errors" - "reflect" + "errors" + "reflect" ) +//Error collection var ( - ErrNotFunction = errors.New("Argument supplied must be a function") - ErrNoErrorHandling = errors.New("Function last return value must be an error") + ErrNotFunction = errors.New("Argument supplied must be a function") + ErrNoErrorHandling = errors.New("Function last return value must be an error") ) /* @@ -15,11 +16,11 @@ IThennable is an interface for thennable The main purpose to to be able to chain execution of functions */ type IThennable interface { - BreakOnError(bool) IThennable - Supply(...interface{}) IThennable - Then(interface{}) IThennable - Handle(FErrorHandler) IThennable - End() ([]interface{}, error) + BreakOnError(bool) IThennable + Supply(...interface{}) IThennable + Then(interface{}) IThennable + Handle(FErrorHandler) IThennable + End() ([]interface{}, error) } //FErrorHandler function @@ -27,90 +28,89 @@ type FErrorHandler func(error) //private implementation of IThennable type thennable struct { - throw error - state []reflect.Value - runnable reflect.Value - breakOnError bool + throw error + state []reflect.Value + runnable reflect.Value + breakOnError bool } //Start creating a new IThennable with and initial value/state func Start(params ...interface{}) IThennable { - state := []reflect.Value{} - for _, param := range params { - state = append(state, reflect.ValueOf(param)) - } - return &thennable{state: state, breakOnError: true} + state := []reflect.Value{} + for _, param := range params { + state = append(state, reflect.ValueOf(param)) + } + return &thennable{state: state, breakOnError: true} } //Runnable function is a function that have at least error return value func newRunnable(fn interface{}) (runnable reflect.Value, err error) { - t := reflect.TypeOf(fn) - if t.Kind() != reflect.Func { - return runnable, ErrNotFunction - } - - out := t.NumOut() - if out <= 0 || t.Out(out-1).Name() != "error" { - return runnable, ErrNoErrorHandling - } - - return reflect.ValueOf(fn), nil -} + t := reflect.TypeOf(fn) + if t.Kind() != reflect.Func { + return runnable, ErrNotFunction + } + + out := t.NumOut() + if out <= 0 || t.Out(out-1).Name() != "error" { + return runnable, ErrNoErrorHandling + } + return reflect.ValueOf(fn), nil +} //BreakOnError stops function chaining when error occured func (tnb *thennable) BreakOnError(breakOnError bool) IThennable { - tnb.breakOnError = breakOnError - return tnb + tnb.breakOnError = breakOnError + return tnb } //Supply next runnable function with a value func (tnb *thennable) Supply(params ...interface{}) IThennable { - state := []reflect.Value{} - for _, param := range params { - state = append(state, reflect.ValueOf(param)) - } - - return &thennable{state: state, breakOnError: tnb.breakOnError} + state := []reflect.Value{} + for _, param := range params { + state = append(state, reflect.ValueOf(param)) + } + + return &thennable{state: state, breakOnError: tnb.breakOnError} } func (tnb *thennable) Then(next interface{}) IThennable { - if tnb.breakOnError && tnb.throw != nil { - return tnb - } - - //if not runnable func - runnable, err := newRunnable(next) - if err != nil { - tnb.state, tnb.throw = make([]reflect.Value, 0), err - return tnb - } - - //else - result := runnable.Call(tnb.state) - retCount := len(result) - errIdx := retCount - 1 - - tnb.state, tnb.throw = result[0:errIdx], nil - lastOutput := result[errIdx].Interface() - if lastOutput != nil { - tnb.throw = lastOutput.(error) - } - - return tnb + if tnb.breakOnError && tnb.throw != nil { + return tnb + } + + //if not runnable func + runnable, err := newRunnable(next) + if err != nil { + tnb.state, tnb.throw = make([]reflect.Value, 0), err + return tnb + } + + //else + result := runnable.Call(tnb.state) + retCount := len(result) + errIdx := retCount - 1 + + tnb.state, tnb.throw = result[0:errIdx], nil + lastOutput := result[errIdx].Interface() + if lastOutput != nil { + tnb.throw = lastOutput.(error) + } + + return tnb } //Handle error func (tnb *thennable) Handle(handle FErrorHandler) IThennable { - handle(tnb.throw) - return tnb + handle(tnb.throw) + return tnb } //End execution and collect the result func (tnb *thennable) End() ([]interface{}, error) { - end := make([]interface{}, 0) - for _, state := range tnb.state { - end = append(end, state.Interface()) - } - return end, tnb.throw -} \ No newline at end of file + end := make([]interface{}, 0) + for _, state := range tnb.state { + end = append(end, state.Interface()) + } + return end, tnb.throw +} diff --git a/thennable_test.go b/thennable_test.go index 21c9b5c..d98657b 100644 --- a/thennable_test.go +++ b/thennable_test.go @@ -1,122 +1,127 @@ package thennable_test import ( - "fmt" - "strconv" - "thennable" - "testing" + "fmt" + "strconv" + "testing" + + thennable "github.com/bastianrob/go-thennable" ) func AddOne(val int) (int, error) { - return val + 1, nil + return val + 1, nil } -func Throwable(val interface{}) (error) { - return fmt.Errorf("Exception occurred with supplied value: %+v", val) +func Throwable(val interface{}) error { + return fmt.Errorf("Exception occurred with supplied value: %+v", val) } func LogError(err error) { - fmt.Printf("Log Error: %+v\n", err) + fmt.Printf("Log Error: %+v\n", err) } func Test_IThennable(t *testing.T) { - fmt.Println("\nStarting Test_IThennable.go") - - addOne := AddOne - throw := Throwable - - //Test 1 - fmt.Println("Start Test 1: Expect result = 5 and err is nil") - result, err := thennable.Start(1).//starts with 1 - Then(addOne). //add 1 with 1 = 2 - Then(addOne). //result = 3 - Then(addOne). //result = 4 - Then(addOne). //result = 5 - End() - - if result[0].(int) != 5 { - t.Errorf("Test 1 result should be = 5, actual value: %d", result[0]) - } - if err != nil { - t.Errorf("Test 1 error should be = nil, actual value: %+v", err) - } - - //Test 2 - fmt.Println("\nStart Test 2: Expect result is nil and err is not nil") - result, err = thennable.Start(1).//starts with 1 - Then(addOne). //add 1 with 1 = 2 - Then(addOne). //add 2 with 1 = 3 - Then(throw). //err = exception - Handle(LogError). - Then(addOne). //skipped - Then(addOne). //skipped - End() - - if len(result) != 0 { - t.Errorf("Test 2 result should be = empty, actual value: %d", len(result)) - } - if err == nil { - t.Errorf("Test 2 error should not be = nil, actual value: %+v", err) - } - - //Test 3 - fmt.Println("\nStart Test 3: Expect result = 2 and err is nil") - result, err = thennable.Start(1).//starts with 1 - BreakOnError(false). //keep propagating the function chain - Then(addOne). //add 1 with 1 = 2 - Then(addOne). //result = 3 - Then(throw). //result = 0, err = exception ignored - Supply(0). //resupply with 0 - Then(addOne). //add 0 with 1 = 1, err = nil - Then(addOne). //result = 2, err = nil - End() - - if result[0].(int) != 2 { - t.Errorf("Test 3 result should be = 2, actual value: %d", result[0]) - } - if err != nil { - t.Errorf("Test 3 error should be = nil, actual value: %+v", err) - } - - //Test 4 - fmt.Println("\nStart Test 4: Expect recover from error, result is 10 and err is nil") - result, err = thennable.Start(1).//starts with 1 - Then(addOne). //add 1 with 1 = 2 - Then(throw). //result = nil, err = exception - Then(addOne). //skipped - Handle(LogError). //log the error - BreakOnError(false). //recover from error - Supply(8). //resuply the value with 8 - Then(addOne). //result = 9 - Then(addOne). //result = 10 - Handle(LogError). //log error should be nil - End() - - if result[0].(int) != 10 { - t.Errorf("Test 4 result should be = 10, actual value: %d", result[0]) - } - if err != nil { - t.Errorf("Test 4 error should be nil, actual value: %+v", err) - } - - //Test 5 - fmt.Println("\nStart Test 5: Expect result is '6' and err is nil") - result, err = thennable.Start(1). - Then(func (one int) (int, int, error) { - return one, 2, nil - }). - Then(func (one, two int) (int, int, int, error) { - return one, two, 3, nil - }). - Then(func (one, two, three int) (string, error) { - return strconv.Itoa(one + two + three), nil - }). - End() - - if result[0].(string) != "6" { - t.Errorf("Test 5 result should be = '6', actual value: %v\n", result) - } - if err != nil { - t.Errorf("Test 5 error should be nil, actual value: %+v\n", err) - } -} \ No newline at end of file + fmt.Println("\nStarting Test_IThennable.go") + + addOne := AddOne + throw := Throwable + + //Test 1 + fmt.Println("Start Test 1: Expect result = 5 and err is nil") + result, err := thennable. + Start(1). //starts with 1 + Then(addOne). //add 1 with 1 = 2 + Then(addOne). //result = 3 + Then(addOne). //result = 4 + Then(addOne). //result = 5 + End() + + if result[0].(int) != 5 { + t.Errorf("Test 1 result should be = 5, actual value: %d", result[0]) + } + if err != nil { + t.Errorf("Test 1 error should be = nil, actual value: %+v", err) + } + + //Test 2 + fmt.Println("\nStart Test 2: Expect result is nil and err is not nil") + result, err = thennable. + Start(1). //starts with 1 + Then(addOne). //add 1 with 1 = 2 + Then(addOne). //add 2 with 1 = 3 + Then(throw). //err = exception + Handle(LogError). + Then(addOne). //skipped + Then(addOne). //skipped + End() + + if len(result) != 0 { + t.Errorf("Test 2 result should be = empty, actual value: %d", len(result)) + } + if err == nil { + t.Errorf("Test 2 error should not be = nil, actual value: %+v", err) + } + + //Test 3 + fmt.Println("\nStart Test 3: Expect result = 2 and err is nil") + result, err = thennable. + Start(1). //starts with 1 + BreakOnError(false). //keep propagating the function chain + Then(addOne). //add 1 with 1 = 2 + Then(addOne). //result = 3 + Then(throw). //result = 0, err = exception ignored + Supply(0). //resupply with 0 + Then(addOne). //add 0 with 1 = 1, err = nil + Then(addOne). //result = 2, err = nil + End() + + if result[0].(int) != 2 { + t.Errorf("Test 3 result should be = 2, actual value: %d", result[0]) + } + if err != nil { + t.Errorf("Test 3 error should be = nil, actual value: %+v", err) + } + + //Test 4 + fmt.Println("\nStart Test 4: Expect recover from error, result is 10 and err is nil") + result, err = thennable. + Start(1). //starts with 1 + Then(addOne). //add 1 with 1 = 2 + Then(throw). //result = nil, err = exception + Then(addOne). //skipped + Handle(LogError). //log the error + BreakOnError(false). //recover from error + Supply(8). //resuply the value with 8 + Then(addOne). //result = 9 + Then(addOne). //result = 10 + Handle(LogError). //log error should be nil + End() + + if result[0].(int) != 10 { + t.Errorf("Test 4 result should be = 10, actual value: %d", result[0]) + } + if err != nil { + t.Errorf("Test 4 error should be nil, actual value: %+v", err) + } + + //Test 5 + fmt.Println("\nStart Test 5: Expect result is '6' and err is nil") + result, err = thennable.Start(1). + Then(func(one int) (int, int, error) { + return one, 2, nil + }). + Then(func(one, two int) (int, int, int, error) { + return one, two, 3, nil + }). + Then(func(one, two, three int) (string, error) { + return strconv.Itoa(one + two + three), nil + }). + End() + + if result[0].(string) != "6" { + t.Errorf("Test 5 result should be = '6', actual value: %v\n", result) + } + if err != nil { + t.Errorf("Test 5 error should be nil, actual value: %+v\n", err) + } +}