From 3c6ef613f522911a090e7aaee3244926652d4236 Mon Sep 17 00:00:00 2001 From: Gregory Eremin Date: Wed, 14 Oct 2015 00:08:45 +0300 Subject: [PATCH] Add caller package --- LICENCE | 22 +++++ caller/README.md | 30 +++++++ caller/caller.go | 81 +++++++++++++++++ caller/caller_test.go | 198 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 331 insertions(+) create mode 100644 LICENCE create mode 100644 caller/README.md create mode 100644 caller/caller.go create mode 100644 caller/caller_test.go diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..4c7b6b8 --- /dev/null +++ b/LICENCE @@ -0,0 +1,22 @@ +Copyright (c) 2015 Gregory Eremin + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/caller/README.md b/caller/README.md new file mode 100644 index 0000000..5a7450b --- /dev/null +++ b/caller/README.md @@ -0,0 +1,30 @@ +# Caller + +Package caller is used to dynamicly call functions with data unmarshalled +into the functions' first argument. It's main purpose is to hide common +unmarshalling code from each function's implementation thus reducing +boilerplate and making the code sexier. + +[Documentation](https://godoc.org/github.com/localhots/uberdaemon/caller) + +```go +package main + +import ( + "github.com/localhots/uberdaemon/caller" +) + +type message struct { + Title string `json:"title"` + Body string `json:"body"` +} + +func processMessage(m message) { + fmt.Printf("Title: %s\nBody: %s\n", m.Title, m.Body) +} + +func main() { + c, _ := caller.New(processMessage) + c.Call(`{"title": "Hello", "body": "World"}`) +} +``` diff --git a/caller/caller.go b/caller/caller.go new file mode 100644 index 0000000..3585ed9 --- /dev/null +++ b/caller/caller.go @@ -0,0 +1,81 @@ +// Package caller is used to dynamicly call functions with data unmarshalled +// into the functions' first argument. It's main purpose is to hide common +// unmarshalling code from each function's implementation thus reducing +// boilerplate and making the code sexier. +package caller + +import ( + "encoding/json" + "errors" + "reflect" +) + +// Caller wraps a function and makes it ready to be dynamically called. +type Caller struct { + 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{ + fun: fval, + argtyp: ftyp.In(0), + } + + return c, nil +} + +// Call creates an instance of the Caller function's argument type, unmarshalls +// the JSON 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 = json.Unmarshal(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/caller_test.go b/caller/caller_test.go new file mode 100644 index 0000000..65a8a8b --- /dev/null +++ b/caller/caller_test.go @@ -0,0 +1,198 @@ +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, _ := ioutil.ReadAll(r) + 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) + } +}