commit 069b5b865e6d9a41665bb99acbf12edb49fd66b2 Author: Gregory Eremin Date: Tue Jul 26 21:56:50 2016 +0200 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e628f8 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Caller + +Package caller is used to dynamically call functions with data unmarshalled +into the functions' first argument. Its main purpose is to hide common +unmarshalling code from each function implementation thus reducing +boilerplate and making package interaction code sexier. + +[Documentation](https://godoc.org/github.com/localhots/shezmu/caller) + +Caller abstracts away the process of unmarshaling data before processing. + +```go +type PriceUpdate struct { + Product string `json:"product"` + Amount float32 `json:"amount"` +} + +func PriceUpdatePrinter(p PriceUpdate) { + log.Printf("Price for %q is now $%.2f", p.Product, p.Amount) +} + +// Error handling is skipped for clarity +func main() { + printer, _ := caller.New(PriceUpdatePrinter) + _ = printer.Call([]byte(`{"product": "Paperclip", "amount": 0.01}`)) +} +``` diff --git a/caller.go b/caller.go new file mode 100644 index 0000000..76584bd --- /dev/null +++ b/caller.go @@ -0,0 +1,84 @@ +// Package caller is used to dynamically call functions with data unmarshalled +// into the functions' first argument. Its main purpose is to hide common +// unmarshalling code from each function implementation thus reducing +// boilerplate and making package interaction code sexier. +package caller + +import ( + "encoding/json" + "errors" + "reflect" +) + +// Caller wraps a function and makes it ready to be dynamically called. +type Caller struct { + // Unmarshaller is a BYOB unmarshaller function. By default it uses JSON. + Unmarshaller func(data []byte, v interface{}) error + fun reflect.Value + argtyp reflect.Type +} + +var ( + // ErrInvalidFunctionType is an error that is returned by the New function + // when its argument is not a function. + ErrInvalidFunctionType = errors.New("argument must be function") + // ErrInvalidFunctionInArguments is an error that is returned by the New + // function when its argument-function has a number of input arguments other + // than 1. + ErrInvalidFunctionInArguments = errors.New("function must have only one input argument") + // ErrInvalidFunctionOutArguments is an error that is returned by the New + // function when its argument-function returs any values. + ErrInvalidFunctionOutArguments = errors.New("function must not have output arguments") +) + +// New creates a new Caller instance using the function given as an argument. +// It returns the Caller instance and an error if something is wrong with the +// argument-function. +func New(fun interface{}) (c *Caller, err error) { + fval := reflect.ValueOf(fun) + ftyp := reflect.TypeOf(fun) + if ftyp.Kind() != reflect.Func { + return nil, ErrInvalidFunctionType + } + if ftyp.NumIn() != 1 { + return nil, ErrInvalidFunctionInArguments + } + if ftyp.NumOut() != 0 { + return nil, ErrInvalidFunctionOutArguments + } + + c = &Caller{ + Unmarshaller: json.Unmarshal, + fun: fval, + argtyp: ftyp.In(0), + } + + return c, nil +} + +// Call creates an instance of the Caller function's argument type, unmarshalls +// the payload into it and dynamically calls the Caller function with this +// instance. +func (c *Caller) Call(data []byte) error { + val, err := c.unmarshal(data) + if err != nil { + return err + } + + c.makeDynamicCall(val) + return nil +} + +func (c *Caller) unmarshal(data []byte) (val reflect.Value, err error) { + val = c.newValue() + err = c.Unmarshaller(data, val.Interface()) + return +} + +func (c *Caller) makeDynamicCall(val reflect.Value) { + c.fun.Call([]reflect.Value{val.Elem()}) +} + +func (c *Caller) newValue() reflect.Value { + return reflect.New(c.argtyp) +} diff --git a/caller_test.go b/caller_test.go new file mode 100644 index 0000000..bf74670 --- /dev/null +++ b/caller_test.go @@ -0,0 +1,202 @@ +package caller + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "testing" +) + +// +// Testing targets +// + +type testMessage struct { + Body string `json:"body"` +} + +const testPayload = `{"body":"Success!"}` + +func testFun(m testMessage) { + fmt.Print(m.Body) +} + +func testFunSilent(_ testMessage) {} + +// +// Tests +// + +func TestNewCallerSuccess(t *testing.T) { + c, err := New(testFun) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + if c == nil { + t.Error("Expected an instance of Caller, got nil") + } +} + +func TestNewCallerWithNonFunc(t *testing.T) { + c, err := New(1) + if err != ErrInvalidFunctionType { + t.Errorf("Expected ErrInvalidFunctionType, got: %v", err) + } + if c != nil { + t.Error("Expected nil, got an instance of Caller") + } +} + +func TestNewCallerWithFuncMultipleArgs(t *testing.T) { + fun := func(a, b int) {} + c, err := New(fun) + if err != ErrInvalidFunctionInArguments { + t.Errorf("Expected ErrInvalidFunctionInArguments, got: %v", err) + } + if c != nil { + t.Error("Expected nil, got an instance of Caller") + } +} + +func TestNewCallerWithFuncReturnValue(t *testing.T) { + fun := func(a int) int { return 0 } + c, err := New(fun) + if err != ErrInvalidFunctionOutArguments { + t.Errorf("Expected ErrInvalidFunctionOutArguments, got: %v", err) + } + if c != nil { + t.Error("Expected nil, got an instance of Caller") + } +} + +func TestCallSuccess(t *testing.T) { + c, err := New(testFun) + if err != nil { + t.Fatal(err.Error()) + } + + out := captureStdoutAround(func() { + if err := c.Call([]byte(testPayload)); err != nil { + t.Fatal(err.Error()) + } + }) + + if string(out) != "Success!" { + t.Errorf("Expected output to be %q, got %q", "Success!", out) + } +} + +func TestCallFalure(t *testing.T) { + c, _ := New(testFunSilent) + + err := c.Call([]byte("{")) + if err == nil { + t.Error("Expected unmarshalling error, got nil") + } +} + +func TestUnmarshalSuccess(t *testing.T) { + c, _ := New(testFunSilent) + + _, err := c.unmarshal([]byte(testPayload)) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } +} + +func TestUnmarshalFailure(t *testing.T) { + c, _ := New(testFunSilent) + + _, err := c.unmarshal([]byte("{")) + if err == nil { + t.Error("Expected unmarshalling error, got nil") + } +} + +func captureStdoutAround(f func()) []byte { + origStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + f() + + w.Close() + out, err := ioutil.ReadAll(r) + if err != nil { + os.Stdout = origStdout + panic(err) + } + r.Close() + os.Stdout = origStdout + + return out +} + +// +// Benchmarks +// + +func BenchmarkCaller(b *testing.B) { + c, _ := New(testFunSilent) + + for i := 0; i < b.N; i++ { + c.Call([]byte(testPayload)) + } +} + +func BenchmarkNoCaller(b *testing.B) { + for i := 0; i < b.N; i++ { + var msg testMessage + json.Unmarshal([]byte(testPayload), &msg) + testFunSilent(msg) + } +} + +func BenchmarkDynamicNew(b *testing.B) { + c, _ := New(testFunSilent) + + for i := 0; i < b.N; i++ { + _ = c.newValue() + } +} + +func BenchmarkStaticNew(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = testMessage{} + } +} + +func BenchmarkDynamicCall(b *testing.B) { + c, _ := New(testFunSilent) + val, _ := c.unmarshal([]byte(testPayload)) + + for i := 0; i < b.N; i++ { + c.makeDynamicCall(val) + } +} + +func BenchmarkStaticCall(b *testing.B) { + var msg testMessage + + for i := 0; i < b.N; i++ { + testFunSilent(msg) + } +} + +func BenchmarkUnmarshalIntoInterface(b *testing.B) { + c, _ := New(testFunSilent) + val := c.newValue() + + for i := 0; i < b.N; i++ { + json.Unmarshal([]byte(testPayload), val.Interface()) + } +} + +func BenchmarkUnmarshalIntoTypedValue(b *testing.B) { + var msg testMessage + + for i := 0; i < b.N; i++ { + json.Unmarshal([]byte(testPayload), &msg) + } +}