From de69b2fba2989dc2c4085f54e32b7efef7e3013d Mon Sep 17 00:00:00 2001 From: kataras Date: Thu, 23 Nov 2017 12:43:29 +0200 Subject: [PATCH] start the new mvc - binder Former-commit-id: 37e56f409ca136700452fb8fbff740fcca3e98bf --- mvc2/binder.go | 127 +++++++++++++++++++++++++++++++++++++ mvc2/binder_test.go | 150 ++++++++++++++++++++++++++++++++++++++++++++ mvc2/mvc.go | 11 ++++ mvc2/reflect.go | 34 ++++++++++ 4 files changed, 322 insertions(+) create mode 100644 mvc2/binder.go create mode 100644 mvc2/binder_test.go create mode 100644 mvc2/mvc.go create mode 100644 mvc2/reflect.go diff --git a/mvc2/binder.go b/mvc2/binder.go new file mode 100644 index 00000000..24665766 --- /dev/null +++ b/mvc2/binder.go @@ -0,0 +1,127 @@ +package mvc2 + +import ( + "reflect" + + "github.com/kataras/iris/context" +) + +// InputBinder is the result of `MakeBinder`. +// It contains the binder wrapped information, like the +// type that is responsible to bind +// and a function which will accept a context and returns a value of something. +type InputBinder struct { + BindType reflect.Type + BindFunc func(context.Context) reflect.Value +} + +// MustMakeBinder calls the `MakeBinder` and returns its first result, see its docs. +// It panics on error. +func MustMakeBinder(binder interface{}) *InputBinder { + b, err := MakeBinder(binder) + if err != nil { + panic(err) + } + return b +} + +// MakeBinder takes a binder function or a struct which contains a "Bind" +// function and returns an `InputBinder`, which Iris uses to +// resolve and set the input parameters when a handler is executed. +// +// The "binder" can have the following form: +// `func(iris.Context) UserViewModel` +// and a struct which contains a "Bind" method +// of the same binder form that was described above. +// +// The return type of the "binder" should be a value instance, not a pointer, for your own protection. +// The binder function should return only one value and +// it can accept only one input argument, the Iris' Context (`context.Context` or `iris.Context`). +func MakeBinder(binder interface{}) (*InputBinder, error) { + v := reflect.ValueOf(binder) + + // check if it's a struct or a pointer to a struct + // and contains a "Bind" method, if yes use that as the binder func. + if indirectTyp(v.Type()).Kind() == reflect.Struct { + if m := v.MethodByName("Bind"); m.IsValid() && m.CanInterface() { + v = m + } + } + + return makeBinder(v) +} + +func makeBinder(fn reflect.Value) (*InputBinder, error) { + typ := indirectTyp(fn.Type()) + + // invalid if not a func. + if typ.Kind() != reflect.Func { + return nil, errBad + } + + // invalid if not returns one single value. + if typ.NumOut() != 1 { + return nil, errBad + } + + // invalid if input args length is not one. + if typ.NumIn() != 1 { + return nil, errBad + } + + // invalid if that single input arg is not a typeof context.Context. + if !isContext(typ.In(0)) { + return nil, errBad + } + + outTyp := typ.Out(0) + zeroOutVal := reflect.New(outTyp).Elem() + + bf := func(ctx context.Context) reflect.Value { + results := fn.Call([]reflect.Value{reflect.ValueOf(ctx)}) + if len(results) == 0 { + return zeroOutVal + } + + v := results[0] + if !v.IsValid() { + return zeroOutVal + } + return v + } + + return &InputBinder{ + BindType: outTyp, + BindFunc: bf, + }, nil +} + +// searchBinders returns a map of the responsible binders for the "expected" types, +// which are the expected input parameters' types, +// based on the available "binders" collection. +// +// It returns a map which its key is the index of the "expected" which +// a valid binder for that in's type found, +// the value is the pointer of the responsible `InputBinder`. +// +// Check of "a nothing responsible for those expected types" +// should be done using the `len(m) == 0`. +func searchBinders(binders []*InputBinder, expected ...reflect.Type) map[int]*InputBinder { + var m map[int]*InputBinder + + for idx, in := range expected { + for _, b := range binders { + // if same type or the result of binder implements the expected in's type. + if b.BindType == in || (in.Kind() == reflect.Interface && b.BindType.Implements(in)) { + if m == nil { + m = make(map[int]*InputBinder) + } + // fmt.Printf("set index: %d to type: %s where input type is: %s\n", idx, b.BindType.String(), in.String()) + m[idx] = b + break + } + } + } + + return m +} diff --git a/mvc2/binder_test.go b/mvc2/binder_test.go new file mode 100644 index 00000000..a26a97e3 --- /dev/null +++ b/mvc2/binder_test.go @@ -0,0 +1,150 @@ +package mvc2 + +import ( + "fmt" + "reflect" + "testing" + + "github.com/kataras/iris/context" +) + +type testUserStruct struct { + ID int64 + Username string +} + +func testBinderFunc(ctx context.Context) testUserStruct { + id, _ := ctx.Params().GetInt64("id") + username := ctx.Params().Get("username") + return testUserStruct{ + ID: id, + Username: username, + } +} + +type testBinderStruct struct{} + +func (t *testBinderStruct) Bind(ctx context.Context) testUserStruct { + return testBinderFunc(ctx) +} + +func TestMakeBinder(t *testing.T) { + testMakeBinder(t, testBinderFunc) + testMakeBinder(t, new(testBinderStruct)) +} + +func testMakeBinder(t *testing.T, binder interface{}) { + b, err := MakeBinder(binder) + if err != nil { + t.Fatalf("failed to make binder: %v", err) + } + + if b == nil { + t.Fatalf("excepted non-nil *InputBinder but got nil") + } + + if expected, got := reflect.TypeOf(testUserStruct{}), b.BindType; expected != got { + t.Fatalf("expected type of the binder's return value to be: %T but got: %T", expected, got) + } + + expected := testUserStruct{ + ID: 42, + Username: "kataras", + } + ctx := context.NewContext(nil) + ctx.Params().Set("id", fmt.Sprintf("%v", expected.ID)) + ctx.Params().Set("username", expected.Username) + + v := b.BindFunc(ctx) + if !v.CanInterface() { + t.Fatalf("result of binder func cannot be interfaced: %#+v", v) + } + + got, ok := v.Interface().(testUserStruct) + if !ok { + t.Fatalf("result of binder func should be a type of 'testUserStruct' but got: %#+v", v.Interface()) + } + + if got != expected { + t.Fatalf("invalid result of binder func, expected: %v but got: %v", expected, got) + } +} + +// TestSearchBinders will test two available binders, one for int +// and other for a string, +// the first input will contains both of them in the same order, +// the second will contain both of them as well but with a different order, +// the third will contain only the int input and should fail, +// the forth one will contain only the string input and should fail, +// the fifth one will contain two integers and should fail, +// the last one will contain a struct and should fail, +// that no of othe available binders will support it, +// so no len of the result should be zero there. +func TestSearchBinders(t *testing.T) { + // binders + var ( + stringBinder = MustMakeBinder(func(ctx context.Context) string { + return "a string" + }) + intBinder = MustMakeBinder(func(ctx context.Context) int { + return 42 + }) + ) + // in + var ( + stringType = reflect.TypeOf("string") + intType = reflect.TypeOf(1) + ) + + check := func(testName string, shouldPass bool, errString string) { + if shouldPass && errString != "" { + t.Fatalf("[%s] %s", testName, errString) + } + if !shouldPass && errString == "" { + t.Fatalf("[%s] expected not to pass", testName) + } + } + + // 1 + check("test1", true, testSearchBinders(t, []*InputBinder{intBinder, stringBinder}, + []interface{}{"a string", 42}, stringType, intType)) + availableBinders := []*InputBinder{stringBinder, intBinder} // different order than the fist test. + // 2 + check("test2", true, testSearchBinders(t, availableBinders, + []interface{}{"a string", 42}, stringType, intType)) + // 3 + check("test-3-fail", false, testSearchBinders(t, availableBinders, + []interface{}{42}, stringType, intType)) + // 4 + check("test-4-fail", false, testSearchBinders(t, availableBinders, + []interface{}{"a string"}, stringType, intType)) + // 5 + check("test-5-fail", false, testSearchBinders(t, availableBinders, + []interface{}{42, 42}, stringType, intType)) + // 6 + check("test-6-fail", false, testSearchBinders(t, availableBinders, + []interface{}{testUserStruct{}}, stringType, intType)) + +} + +func testSearchBinders(t *testing.T, binders []*InputBinder, expectingResults []interface{}, in ...reflect.Type) (errString string) { + m := searchBinders(binders, in...) + + if len(m) != len(expectingResults) { + return "expected results length and valid binders to be equal, so each input has one binder" + } + + ctx := context.NewContext(nil) + for idx, expected := range expectingResults { + if m[idx] != nil { + v := m[idx].BindFunc(ctx) + if got := v.Interface(); got != expected { + return fmt.Sprintf("expected result[%d] to be: %v but got: %v", idx, expected, got) + } + } else { + t.Logf("m[%d] = nil on input = %v\n", idx, expected) + } + } + + return "" +} diff --git a/mvc2/mvc.go b/mvc2/mvc.go new file mode 100644 index 00000000..c9f39118 --- /dev/null +++ b/mvc2/mvc.go @@ -0,0 +1,11 @@ +package mvc2 + +import ( + "errors" +) + +var ( + errNil = errors.New("nil") + errBad = errors.New("bad") + errAlreadyExists = errors.New("already exists") +) diff --git a/mvc2/reflect.go b/mvc2/reflect.go new file mode 100644 index 00000000..ff293d76 --- /dev/null +++ b/mvc2/reflect.go @@ -0,0 +1,34 @@ +package mvc2 + +import "reflect" + +func isContext(inTyp reflect.Type) bool { + return inTyp.String() == "context.Context" // I couldn't find another way; context/context.go is not exported. +} + +func indirectVal(v reflect.Value) reflect.Value { + return reflect.Indirect(v) +} + +func indirectTyp(typ reflect.Type) reflect.Type { + switch typ.Kind() { + case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + return typ.Elem() + } + return typ +} + +func goodVal(v reflect.Value) bool { + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice: + if v.IsNil() { + return false + } + } + + return v.IsValid() +} + +func isFunc(typ reflect.Type) bool { + return typ.Kind() == reflect.Func +}