From 1f4a337233ce2a9a02d602fed6ffc023fa00c244 Mon Sep 17 00:00:00 2001 From: kataras Date: Thu, 23 Nov 2017 12:43:29 +0200 Subject: [PATCH 01/79] start the new mvc - binder --- 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 0000000000..2466576690 --- /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 0000000000..a26a97e3d5 --- /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 0000000000..c9f39118d5 --- /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 0000000000..ff293d7679 --- /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 +} From 5a11ae801af1d68147c76ecb7a8605a68574238f Mon Sep 17 00:00:00 2001 From: kataras Date: Thu, 23 Nov 2017 22:36:47 +0200 Subject: [PATCH 02/79] I, think, that binders are done, both dynamic functions with different results every time (based on the context) and static services (interface as input(to give the devs the chance make better and most testable code) and struct or both are structs) --- mvc2/binder.go | 83 ++++++++++++++++++++------------------- mvc2/binder_test.go | 64 +++++++++++++++--------------- mvc2/handler.go | 21 ++++++++++ mvc2/reflect.go | 12 ++++++ mvc2/service.go | 92 ++++++++++++++++++++++++++++++++++++++++++++ mvc2/service_test.go | 46 ++++++++++++++++++++++ 6 files changed, 244 insertions(+), 74 deletions(-) create mode 100644 mvc2/handler.go create mode 100644 mvc2/service.go create mode 100644 mvc2/service_test.go diff --git a/mvc2/binder.go b/mvc2/binder.go index 2466576690..1e0b9c005a 100644 --- a/mvc2/binder.go +++ b/mvc2/binder.go @@ -2,8 +2,6 @@ package mvc2 import ( "reflect" - - "github.com/kataras/iris/context" ) // InputBinder is the result of `MakeBinder`. @@ -12,20 +10,50 @@ import ( // 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 + BindFunc func(ctx []reflect.Value) reflect.Value +} + +// getBindersForInput 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 getBindersForInput(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 equalTypes(b.BindType, 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 } -// MustMakeBinder calls the `MakeBinder` and returns its first result, see its docs. +// MustMakeFuncInputBinder calls the `MakeFuncInputBinder` and returns its first result, see its docs. // It panics on error. -func MustMakeBinder(binder interface{}) *InputBinder { - b, err := MakeBinder(binder) +func MustMakeFuncInputBinder(binder interface{}) *InputBinder { + b, err := MakeFuncInputBinder(binder) if err != nil { panic(err) } return b } -// MakeBinder takes a binder function or a struct which contains a "Bind" +// MakeFuncInputBinder 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. // @@ -37,7 +65,7 @@ func MustMakeBinder(binder interface{}) *InputBinder { // 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) { +func MakeFuncInputBinder(binder interface{}) (*InputBinder, error) { v := reflect.ValueOf(binder) // check if it's a struct or a pointer to a struct @@ -48,10 +76,10 @@ func MakeBinder(binder interface{}) (*InputBinder, error) { } } - return makeBinder(v) + return makeFuncInputBinder(v) } -func makeBinder(fn reflect.Value) (*InputBinder, error) { +func makeFuncInputBinder(fn reflect.Value) (*InputBinder, error) { typ := indirectTyp(fn.Type()) // invalid if not a func. @@ -77,8 +105,9 @@ func makeBinder(fn reflect.Value) (*InputBinder, error) { outTyp := typ.Out(0) zeroOutVal := reflect.New(outTyp).Elem() - bf := func(ctx context.Context) reflect.Value { - results := fn.Call([]reflect.Value{reflect.ValueOf(ctx)}) + bf := func(ctxValue []reflect.Value) reflect.Value { + // []reflect.Value{reflect.ValueOf(ctx)} + results := fn.Call(ctxValue) if len(results) == 0 { return zeroOutVal } @@ -95,33 +124,3 @@ func makeBinder(fn reflect.Value) (*InputBinder, error) { 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 index a26a97e3d5..daabda8f5a 100644 --- a/mvc2/binder_test.go +++ b/mvc2/binder_test.go @@ -28,13 +28,13 @@ func (t *testBinderStruct) Bind(ctx context.Context) testUserStruct { return testBinderFunc(ctx) } -func TestMakeBinder(t *testing.T) { - testMakeBinder(t, testBinderFunc) - testMakeBinder(t, new(testBinderStruct)) +func TestMakeFuncInputBinder(t *testing.T) { + testMakeFuncInputBinder(t, testBinderFunc) + testMakeFuncInputBinder(t, new(testBinderStruct)) } -func testMakeBinder(t *testing.T, binder interface{}) { - b, err := MakeBinder(binder) +func testMakeFuncInputBinder(t *testing.T, binder interface{}) { + b, err := MakeFuncInputBinder(binder) if err != nil { t.Fatalf("failed to make binder: %v", err) } @@ -54,8 +54,8 @@ func testMakeBinder(t *testing.T, binder interface{}) { ctx := context.NewContext(nil) ctx.Params().Set("id", fmt.Sprintf("%v", expected.ID)) ctx.Params().Set("username", expected.Username) - - v := b.BindFunc(ctx) + ctxValue := []reflect.Value{reflect.ValueOf(ctx)} + v := b.BindFunc(ctxValue) if !v.CanInterface() { t.Fatalf("result of binder func cannot be interfaced: %#+v", v) } @@ -70,7 +70,16 @@ func testMakeBinder(t *testing.T, binder interface{}) { } } -// TestSearchBinders will test two available binders, one for int +func testCheck(t *testing.T, 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) + } +} + +// TestGetBindersForInput 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, @@ -80,13 +89,13 @@ func testMakeBinder(t *testing.T, binder interface{}) { // 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) { +func TestGetBindersForInput(t *testing.T) { // binders var ( - stringBinder = MustMakeBinder(func(ctx context.Context) string { + stringBinder = MustMakeFuncInputBinder(func(ctx context.Context) string { return "a string" }) - intBinder = MustMakeBinder(func(ctx context.Context) int { + intBinder = MustMakeFuncInputBinder(func(ctx context.Context) int { return 42 }) ) @@ -96,48 +105,39 @@ func TestSearchBinders(t *testing.T) { 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}, + testCheck(t, "test1", true, testGetBindersForInput(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, + testCheck(t, "test2", true, testGetBindersForInput(t, availableBinders, []interface{}{"a string", 42}, stringType, intType)) // 3 - check("test-3-fail", false, testSearchBinders(t, availableBinders, + testCheck(t, "test-3-fail", false, testGetBindersForInput(t, availableBinders, []interface{}{42}, stringType, intType)) // 4 - check("test-4-fail", false, testSearchBinders(t, availableBinders, + testCheck(t, "test-4-fail", false, testGetBindersForInput(t, availableBinders, []interface{}{"a string"}, stringType, intType)) // 5 - check("test-5-fail", false, testSearchBinders(t, availableBinders, + testCheck(t, "test-5-fail", false, testGetBindersForInput(t, availableBinders, []interface{}{42, 42}, stringType, intType)) // 6 - check("test-6-fail", false, testSearchBinders(t, availableBinders, + testCheck(t, "test-6-fail", false, testGetBindersForInput(t, availableBinders, []interface{}{testUserStruct{}}, stringType, intType)) } -func testSearchBinders(t *testing.T, binders []*InputBinder, expectingResults []interface{}, in ...reflect.Type) (errString string) { - m := searchBinders(binders, in...) +func testGetBindersForInput(t *testing.T, binders []*InputBinder, expectingResults []interface{}, in ...reflect.Type) (errString string) { + m := getBindersForInput(binders, in...) - if len(m) != len(expectingResults) { - return "expected results length and valid binders to be equal, so each input has one binder" + if expected, got := len(expectingResults), len(m); expected != got { + return fmt.Sprintf("expected results length(%d) and valid binders length(%d) to be equal, so each input has one binder", expected, got) } - ctx := context.NewContext(nil) + ctxValue := []reflect.Value{reflect.ValueOf(context.NewContext(nil))} for idx, expected := range expectingResults { if m[idx] != nil { - v := m[idx].BindFunc(ctx) + v := m[idx].BindFunc(ctxValue) if got := v.Interface(); got != expected { return fmt.Sprintf("expected result[%d] to be: %v but got: %v", idx, expected, got) } diff --git a/mvc2/handler.go b/mvc2/handler.go new file mode 100644 index 0000000000..a40293c1ec --- /dev/null +++ b/mvc2/handler.go @@ -0,0 +1,21 @@ +package mvc2 + +import ( + "fmt" + "reflect" + + "github.com/kataras/iris/context" +) + +// checks if "handler" is context.Handler; func(context.Context). +func isContextHandler(handler interface{}) bool { + _, is := handler.(context.Handler) + return is +} + +func validateHandler(handler interface{}) error { + if typ := reflect.TypeOf(handler); !isFunc(typ) { + return fmt.Errorf("handler expected to be a kind of func but got typeof(%s)", typ.String()) + } + return nil +} diff --git a/mvc2/reflect.go b/mvc2/reflect.go index ff293d7679..0661464d91 100644 --- a/mvc2/reflect.go +++ b/mvc2/reflect.go @@ -32,3 +32,15 @@ func goodVal(v reflect.Value) bool { func isFunc(typ reflect.Type) bool { return typ.Kind() == reflect.Func } + +func equalTypes(in reflect.Type, v reflect.Type) bool { + if in == v { + return true + } + // if accepts an interface, check if the given "v" type does + // implement this. + if in.Kind() == reflect.Interface { + return v.Implements(in) + } + return false +} diff --git a/mvc2/service.go b/mvc2/service.go new file mode 100644 index 0000000000..f3155b8fed --- /dev/null +++ b/mvc2/service.go @@ -0,0 +1,92 @@ +package mvc2 + +import ( + "reflect" +) + +// // Service is a `reflect.Value` value. +// // We keep that type here, +// // if we ever need to change this type we will not have +// // to refactor the whole mvc's codebase. +// type Service struct { +// reflect.Value +// typ reflect.Type +// } + +// // Valid checks if the service's Value's Value is valid for set or get. +// func (s Service) Valid() bool { +// return goodVal(s.Value) +// } + +// // Equal returns if the +// func (s Service) Equal(other Service) bool { +// return equalTypes(s.typ, other.typ) +// } + +// func (s Service) String() string { +// return s.Type().String() +// } + +// func wrapService(service interface{}) Service { +// if s, ok := service.(Service); ok { +// return s // if it's a Service already. +// } +// return Service{ +// Value: reflect.ValueOf(service), +// typ: reflect.TypeOf(service), +// } +// } + +// // WrapServices wrap a generic services into structured Service slice. +// func WrapServices(services ...interface{}) []Service { +// if l := len(services); l > 0 { +// out := make([]Service, l, l) +// for i, s := range services { +// out[i] = wrapService(s) +// } +// return out +// } +// return nil +// } + +// MustMakeServiceInputBinder calls the `MakeServiceInputBinder` and returns its first result, see its docs. +// It panics on error. +func MustMakeServiceInputBinder(service interface{}) *InputBinder { + s, err := MakeServiceInputBinder(service) + if err != nil { + panic(err) + } + return s +} + +// MakeServiceInputBinder uses a difference/or strange approach, +// we make the services as bind functions +// in order to keep the rest of the code simpler, however we have +// a performance penalty when calling the function instead +// of just put the responsible service to the certain handler's input argument. +func MakeServiceInputBinder(service interface{}) (*InputBinder, error) { + if service == nil { + return nil, errNil + } + + var ( + val = reflect.ValueOf(service) + typ = val.Type() + ) + + if !goodVal(val) { + return nil, errBad + } + + if indirectTyp(typ).Kind() != reflect.Struct { + // if the pointer's struct is not a struct then return err bad. + return nil, errBad + } + + return &InputBinder{ + BindType: typ, + BindFunc: func(_ []reflect.Value) reflect.Value { + return val + }, + }, nil +} diff --git a/mvc2/service_test.go b/mvc2/service_test.go new file mode 100644 index 0000000000..88779dce8d --- /dev/null +++ b/mvc2/service_test.go @@ -0,0 +1,46 @@ +package mvc2 + +import ( + "reflect" + "testing" +) + +type ( + testService interface { + say(string) + } + testServiceImpl struct { + prefix string + } +) + +func (s *testServiceImpl) say(message string) string { + return s.prefix + ": " + message +} + +func TestMakeServiceInputBinder(t *testing.T) { + expectedService := &testServiceImpl{"say"} + b := MustMakeServiceInputBinder(expectedService) + // in + var ( + intType = reflect.TypeOf(1) + availableBinders = []*InputBinder{b} + ) + + // 1 + testCheck(t, "test1", true, testGetBindersForInput(t, availableBinders, + []interface{}{expectedService}, reflect.TypeOf(expectedService))) + // 2 + testCheck(t, "test2-fail", false, testGetBindersForInput(t, availableBinders, + []interface{}{42})) + // 3 + testCheck(t, "test3-fail", false, testGetBindersForInput(t, availableBinders, + []interface{}{42}, intType)) + // 4 + testCheck(t, "test4-fail", false, testGetBindersForInput(t, availableBinders, + []interface{}{42})) + // 5 - check if nothing passed, so no valid binders at all. + testCheck(t, "test5", true, testGetBindersForInput(t, availableBinders, + []interface{}{})) + +} From ab09f1ecdcf8a4da81db621e786b6ea3dd2ca0c0 Mon Sep 17 00:00:00 2001 From: kataras Date: Fri, 24 Nov 2017 12:32:35 +0200 Subject: [PATCH 03/79] implement the makeHandler and structure the high-level mvc's API --- mvc2/binder.go | 15 ++++++++++ mvc2/handler.go | 69 ++++++++++++++++++++++++++++++++++++++++++-- mvc2/handler_test.go | 11 +++++++ mvc2/mvc.go | 38 ++++++++++++++++++++++++ mvc2/mvc_test.go | 30 +++++++++++++++++++ 5 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 mvc2/handler_test.go create mode 100644 mvc2/mvc_test.go diff --git a/mvc2/binder.go b/mvc2/binder.go index 1e0b9c005a..c4b20648f1 100644 --- a/mvc2/binder.go +++ b/mvc2/binder.go @@ -10,6 +10,10 @@ import ( // and a function which will accept a context and returns a value of something. type InputBinder struct { BindType reflect.Type + // ctx is slice because all binder functions called by + // their `.Call` method which accepts a slice of reflect.Value, + // so on the handler maker we will allocate a slice of a single ctx once + // and used to all binders. BindFunc func(ctx []reflect.Value) reflect.Value } @@ -27,6 +31,17 @@ func getBindersForInput(binders []*InputBinder, expected ...reflect.Type) map[in var m map[int]*InputBinder for idx, in := range expected { + if idx == 0 && isContext(in) { + // if the first is context then set it directly here. + m = make(map[int]*InputBinder) + m[0] = &InputBinder{ + BindType: contextTyp, + BindFunc: func(ctxValues []reflect.Value) reflect.Value { + return ctxValues[0] + }, + } + continue + } for _, b := range binders { // if same type or the result of binder implements the expected in's type. if equalTypes(b.BindType, in) { diff --git a/mvc2/handler.go b/mvc2/handler.go index a40293c1ec..1b15f105ef 100644 --- a/mvc2/handler.go +++ b/mvc2/handler.go @@ -4,13 +4,21 @@ import ( "fmt" "reflect" + "github.com/kataras/golog" "github.com/kataras/iris/context" + "github.com/kataras/iris/mvc/activator/methodfunc" ) // checks if "handler" is context.Handler; func(context.Context). -func isContextHandler(handler interface{}) bool { - _, is := handler.(context.Handler) - return is +func isContextHandler(handler interface{}) (context.Handler, bool) { + h, is := handler.(context.Handler) + if !is { + fh, is := handler.(func(context.Context)) + if is { + return fh, is + } + } + return h, is } func validateHandler(handler interface{}) error { @@ -19,3 +27,58 @@ func validateHandler(handler interface{}) error { } return nil } + +var ( + contextTyp = reflect.TypeOf(context.NewContext(nil)) + emptyIn = []reflect.Value{} +) + +func makeHandler(handler interface{}, binders []*InputBinder) context.Handler { + if err := validateHandler(handler); err != nil { + golog.Errorf("mvc handler: %v", err) + return nil + } + + if h, is := isContextHandler(handler); is { + golog.Warnf("mvc handler: you could just use the low-level API to register a context handler instead") + return h + } + + typ := indirectTyp(reflect.TypeOf(handler)) + n := typ.NumIn() + typIn := make([]reflect.Type, n, n) + for i := 0; i < n; i++ { + typIn[i] = typ.In(i) + } + + m := getBindersForInput(binders, typIn...) + if len(m) != n { + golog.Errorf("mvc handler: input arguments length(%d) and valid binders length(%d) are not equal", n, len(m)) + return nil + } + + hasIn := len(m) > 0 + fn := reflect.ValueOf(handler) + + return func(ctx context.Context) { + if !hasIn { + methodfunc.DispatchFuncResult(ctx, fn.Call(emptyIn)) + return + } + + // we could use other tricks for "in" + // here but let's stick to that which is clearly + // that it doesn't keep any previous state + // and it allocates exactly what we need, + // so we can set via index instead of append. + // The other method we could use is to + // declare the in on the build state (before the return) + // and use in[0:0] with append later on. + in := make([]reflect.Value, n, n) + ctxValues := []reflect.Value{reflect.ValueOf(ctx)} + for k, v := range m { + in[k] = v.BindFunc(ctxValues) + } + methodfunc.DispatchFuncResult(ctx, fn.Call(in)) + } +} diff --git a/mvc2/handler_test.go b/mvc2/handler_test.go new file mode 100644 index 0000000000..3564426c57 --- /dev/null +++ b/mvc2/handler_test.go @@ -0,0 +1,11 @@ +package mvc2 + +/* +TODO: + +Test that as we test the rest, with +a fake context, and after move again to +the mvc_test.go which will contain +the overall high-level (black-box) tests. + +*/ diff --git a/mvc2/mvc.go b/mvc2/mvc.go index c9f39118d5..814d6a7d12 100644 --- a/mvc2/mvc.go +++ b/mvc2/mvc.go @@ -2,6 +2,8 @@ package mvc2 import ( "errors" + + "github.com/kataras/iris/context" ) var ( @@ -9,3 +11,39 @@ var ( errBad = errors.New("bad") errAlreadyExists = errors.New("already exists") ) + +type Mvc struct { + binders []*InputBinder +} + +func New() *Mvc { + return new(Mvc) +} + +func (m *Mvc) RegisterBinder(binders ...interface{}) error { + for _, binder := range binders { + b, err := MakeFuncInputBinder(binder) + if err != nil { + return err + } + m.binders = append(m.binders, b) + } + + return nil +} + +func (m *Mvc) RegisterService(services ...interface{}) error { + for _, service := range services { + b, err := MakeServiceInputBinder(service) + if err != nil { + return err + } + m.binders = append(m.binders, b) + } + + return nil +} + +func (m *Mvc) Handler(handler interface{}) context.Handler { + return makeHandler(handler, m.binders) +} diff --git a/mvc2/mvc_test.go b/mvc2/mvc_test.go new file mode 100644 index 0000000000..2a611370be --- /dev/null +++ b/mvc2/mvc_test.go @@ -0,0 +1,30 @@ +package mvc2_test + +import ( + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/httptest" + "github.com/kataras/iris/mvc2" +) + +var ( + lowLevelHandler = func(ctx iris.Context) { + ctx.Writef("low-level handler") + } +) + +func TestHandler(t *testing.T) { + app := iris.New() + m := mvc2.New() + + // should just return a context.Handler + // without performance penalties. + app.Get("/", m.Handler(lowLevelHandler)) + + e := httptest.New(t, app, httptest.LogLevel("debug")) + // 1 + e.GET("/").Expect().Status(httptest.StatusOK). + Body().Equal("low-level handler") + +} From 339ff7ef54147a4423b3fb1757d1fcc0a8dae6f0 Mon Sep 17 00:00:00 2001 From: kataras Date: Fri, 24 Nov 2017 15:10:30 +0200 Subject: [PATCH 04/79] black-box the MakeHandler, works perfectly. --- mvc2/handler.go | 30 ++++++++++--- mvc2/handler_test.go | 101 +++++++++++++++++++++++++++++++++++++++---- mvc2/mvc.go | 3 +- mvc2/mvc_test.go | 30 ------------- mvc2/reflect.go | 13 +++--- 5 files changed, 126 insertions(+), 51 deletions(-) delete mode 100644 mvc2/mvc_test.go diff --git a/mvc2/handler.go b/mvc2/handler.go index 1b15f105ef..a0ea70cc9e 100644 --- a/mvc2/handler.go +++ b/mvc2/handler.go @@ -33,15 +33,30 @@ var ( emptyIn = []reflect.Value{} ) -func makeHandler(handler interface{}, binders []*InputBinder) context.Handler { +// MustMakeHandler calls the `MakeHandler` and returns its first resultthe low-level handler), see its docs. +// It panics on error. +func MustMakeHandler(handler interface{}, binders []*InputBinder) context.Handler { + h, err := MakeHandler(handler, binders) + if err != nil { + panic(err) + } + return h +} + +// MakeHandler accepts a "handler" function which can accept any input that matches +// with the "binders" and any output, that matches the mvc types, like string, int (string,int), +// custom structs, Result(View | Response) and anything that you already know that mvc implementation supports, +// and returns a low-level `context/iris.Handler` which can be used anywhere in the Iris Application, +// as middleware or as simple route handler or party handler or subdomain handler-router. +func MakeHandler(handler interface{}, binders []*InputBinder) (context.Handler, error) { if err := validateHandler(handler); err != nil { golog.Errorf("mvc handler: %v", err) - return nil + return nil, err } if h, is := isContextHandler(handler); is { golog.Warnf("mvc handler: you could just use the low-level API to register a context handler instead") - return h + return h, nil } typ := indirectTyp(reflect.TypeOf(handler)) @@ -53,14 +68,15 @@ func makeHandler(handler interface{}, binders []*InputBinder) context.Handler { m := getBindersForInput(binders, typIn...) if len(m) != n { - golog.Errorf("mvc handler: input arguments length(%d) and valid binders length(%d) are not equal", n, len(m)) - return nil + err := fmt.Errorf("input arguments length(%d) of types(%s) and valid binders length(%d) are not equal", n, typIn, len(m)) + golog.Errorf("mvc handler: %v", err) + return nil, err } hasIn := len(m) > 0 fn := reflect.ValueOf(handler) - return func(ctx context.Context) { + resultHandler := func(ctx context.Context) { if !hasIn { methodfunc.DispatchFuncResult(ctx, fn.Call(emptyIn)) return @@ -81,4 +97,6 @@ func makeHandler(handler interface{}, binders []*InputBinder) context.Handler { } methodfunc.DispatchFuncResult(ctx, fn.Call(in)) } + + return resultHandler, nil } diff --git a/mvc2/handler_test.go b/mvc2/handler_test.go index 3564426c57..046c075491 100644 --- a/mvc2/handler_test.go +++ b/mvc2/handler_test.go @@ -1,11 +1,96 @@ -package mvc2 +package mvc2_test -/* -TODO: +// black-box -Test that as we test the rest, with -a fake context, and after move again to -the mvc_test.go which will contain -the overall high-level (black-box) tests. +import ( + "fmt" + "testing" -*/ + "github.com/kataras/iris" + "github.com/kataras/iris/httptest" + . "github.com/kataras/iris/mvc2" +) + +// dynamic func +type testUserStruct struct { + ID int64 + Username string +} + +func testBinderFunc(ctx iris.Context) testUserStruct { + id, _ := ctx.Params().GetInt64("id") + username := ctx.Params().Get("username") + return testUserStruct{ + ID: id, + Username: username, + } +} + +// service +type ( + testService interface { + Say(string) string + } + testServiceImpl struct { + prefix string + } +) + +func (s *testServiceImpl) Say(message string) string { + return s.prefix + " " + message +} + +func TestMakeHandler(t *testing.T) { + binders := []*InputBinder{ + // #1 + MustMakeFuncInputBinder(testBinderFunc), + // #2 + MustMakeServiceInputBinder(&testServiceImpl{prefix: "say"}), + // #3 + MustMakeFuncInputBinder(func(ctx iris.Context) string { + return ctx.Params().Get("param") + }), + } + + var ( + // a context as first input arg, which is not needed to be binded manually, + // and a user struct which is binded to the input arg by the #1 func(ctx) any binder. + consumeUserHandler = func(ctx iris.Context, user testUserStruct) { + ctx.JSON(user) + } + h1 = MustMakeHandler(consumeUserHandler, binders) + + // just one input arg, the service which is binded by the #2 service binder. + consumeServiceHandler = func(service testService) string { + return service.Say("something") + } + h2 = MustMakeHandler(consumeServiceHandler, binders) + + // just one input arg, a standar string which is binded by the #3 func(ctx) any binder. + consumeParamHandler = func(myParam string) string { + return "param is: " + myParam + } + h3 = MustMakeHandler(consumeParamHandler, binders) + ) + + app := iris.New() + app.Get("/{id:long}/{username:string}", h1) + app.Get("/service", h2) + app.Get("/param/{param:string}", h3) + + expectedUser := testUserStruct{ + ID: 42, + Username: "kataras", + } + + e := httptest.New(t, app) + // 1 + e.GET(fmt.Sprintf("/%d/%s", expectedUser.ID, expectedUser.Username)).Expect().Status(httptest.StatusOK). + JSON().Equal(expectedUser) + // 2 + e.GET("/service").Expect().Status(httptest.StatusOK). + Body().Equal("say something") + // 3 + e.GET("/param/the_param_value").Expect().Status(httptest.StatusOK). + Body().Equal("param is: the_param_value") +} diff --git a/mvc2/mvc.go b/mvc2/mvc.go index 814d6a7d12..a737aa3494 100644 --- a/mvc2/mvc.go +++ b/mvc2/mvc.go @@ -45,5 +45,6 @@ func (m *Mvc) RegisterService(services ...interface{}) error { } func (m *Mvc) Handler(handler interface{}) context.Handler { - return makeHandler(handler, m.binders) + h, _ := MakeHandler(handler, m.binders) // it logs errors already, so on any error the "h" will be nil. + return h } diff --git a/mvc2/mvc_test.go b/mvc2/mvc_test.go deleted file mode 100644 index 2a611370be..0000000000 --- a/mvc2/mvc_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package mvc2_test - -import ( - "testing" - - "github.com/kataras/iris" - "github.com/kataras/iris/httptest" - "github.com/kataras/iris/mvc2" -) - -var ( - lowLevelHandler = func(ctx iris.Context) { - ctx.Writef("low-level handler") - } -) - -func TestHandler(t *testing.T) { - app := iris.New() - m := mvc2.New() - - // should just return a context.Handler - // without performance penalties. - app.Get("/", m.Handler(lowLevelHandler)) - - e := httptest.New(t, app, httptest.LogLevel("debug")) - // 1 - e.GET("/").Expect().Status(httptest.StatusOK). - Body().Equal("low-level handler") - -} diff --git a/mvc2/reflect.go b/mvc2/reflect.go index 0661464d91..3e45cc1a93 100644 --- a/mvc2/reflect.go +++ b/mvc2/reflect.go @@ -33,14 +33,15 @@ func isFunc(typ reflect.Type) bool { return typ.Kind() == reflect.Func } -func equalTypes(in reflect.Type, v reflect.Type) bool { - if in == v { +func equalTypes(got reflect.Type, expected reflect.Type) bool { + if got == expected { return true } - // if accepts an interface, check if the given "v" type does - // implement this. - if in.Kind() == reflect.Interface { - return v.Implements(in) + // if accepts an interface, check if the given "got" type does + // implement this "expected" user handler's input argument. + if expected.Kind() == reflect.Interface { + // fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String()) + return got.Implements(expected) } return false } From 7e782ef64e737c71b895d0bdfbefe93df44d14d2 Mon Sep 17 00:00:00 2001 From: kataras Date: Fri, 24 Nov 2017 17:34:35 +0200 Subject: [PATCH 05/79] simplify by join the bind registration(ctx-transformer-to-something-func-binder and service one, which just sets the struct as it's) to one named 'In' and create a 'Child' which will return a new mvc instance with binders inheritanced from the parent one and add a simple test to the mvc_test.go - will have more later on --- mvc2/binder.go | 36 ++++++++++++++++++--------- mvc2/binder_test.go | 7 ------ mvc2/handler_test.go | 58 ++++++++++++++++++++++++++------------------ mvc2/mvc.go | 42 ++++++++++++++++++++++---------- mvc2/mvc_test.go | 22 +++++++++++++++++ 5 files changed, 110 insertions(+), 55 deletions(-) create mode 100644 mvc2/mvc_test.go diff --git a/mvc2/binder.go b/mvc2/binder.go index c4b20648f1..1b87e141a0 100644 --- a/mvc2/binder.go +++ b/mvc2/binder.go @@ -68,29 +68,41 @@ func MustMakeFuncInputBinder(binder interface{}) *InputBinder { return b } +type binderType uint32 + +const ( + functionType binderType = iota + serviceType + invalidType +) + +func resolveBinderType(binder interface{}) binderType { + if binder == nil { + return invalidType + } + + switch indirectTyp(reflect.TypeOf(binder)).Kind() { + case reflect.Func: + return functionType + case reflect.Struct: + return serviceType + } + + return invalidType +} + // MakeFuncInputBinder 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. +// `func(iris.Context) UserViewModel`. // // 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 MakeFuncInputBinder(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 makeFuncInputBinder(v) } diff --git a/mvc2/binder_test.go b/mvc2/binder_test.go index daabda8f5a..099a3578f7 100644 --- a/mvc2/binder_test.go +++ b/mvc2/binder_test.go @@ -22,15 +22,8 @@ func testBinderFunc(ctx context.Context) testUserStruct { } } -type testBinderStruct struct{} - -func (t *testBinderStruct) Bind(ctx context.Context) testUserStruct { - return testBinderFunc(ctx) -} - func TestMakeFuncInputBinder(t *testing.T) { testMakeFuncInputBinder(t, testBinderFunc) - testMakeFuncInputBinder(t, new(testBinderStruct)) } func testMakeFuncInputBinder(t *testing.T, binder interface{}) { diff --git a/mvc2/handler_test.go b/mvc2/handler_test.go index 046c075491..3a80672882 100644 --- a/mvc2/handler_test.go +++ b/mvc2/handler_test.go @@ -40,39 +40,51 @@ func (s *testServiceImpl) Say(message string) string { return s.prefix + " " + message } +var ( + // binders, as user-defined + testBinderFuncUserStruct = testBinderFunc + testBinderService = &testServiceImpl{prefix: "say"} + testBinderFuncParam = func(ctx iris.Context) string { + return ctx.Params().Get("param") + } + + // consumers + // a context as first input arg, which is not needed to be binded manually, + // and a user struct which is binded to the input arg by the #1 func(ctx) any binder. + testConsumeUserHandler = func(ctx iris.Context, user testUserStruct) { + ctx.JSON(user) + } + + // just one input arg, the service which is binded by the #2 service binder. + testConsumeServiceHandler = func(service testService) string { + return service.Say("something") + } + // just one input arg, a standar string which is binded by the #3 func(ctx) any binder. + testConsumeParamHandler = func(myParam string) string { + return "param is: " + myParam + } +) + func TestMakeHandler(t *testing.T) { binders := []*InputBinder{ // #1 - MustMakeFuncInputBinder(testBinderFunc), + MustMakeFuncInputBinder(testBinderFuncUserStruct), // #2 - MustMakeServiceInputBinder(&testServiceImpl{prefix: "say"}), + MustMakeServiceInputBinder(testBinderService), // #3 - MustMakeFuncInputBinder(func(ctx iris.Context) string { - return ctx.Params().Get("param") - }), + MustMakeFuncInputBinder(testBinderFuncParam), } var ( - // a context as first input arg, which is not needed to be binded manually, - // and a user struct which is binded to the input arg by the #1 func(ctx) any binder. - consumeUserHandler = func(ctx iris.Context, user testUserStruct) { - ctx.JSON(user) - } - h1 = MustMakeHandler(consumeUserHandler, binders) - - // just one input arg, the service which is binded by the #2 service binder. - consumeServiceHandler = func(service testService) string { - return service.Say("something") - } - h2 = MustMakeHandler(consumeServiceHandler, binders) - - // just one input arg, a standar string which is binded by the #3 func(ctx) any binder. - consumeParamHandler = func(myParam string) string { - return "param is: " + myParam - } - h3 = MustMakeHandler(consumeParamHandler, binders) + h1 = MustMakeHandler(testConsumeUserHandler, binders) + h2 = MustMakeHandler(testConsumeServiceHandler, binders) + h3 = MustMakeHandler(testConsumeParamHandler, binders) ) + testAppWithMvcHandlers(t, h1, h2, h3) +} + +func testAppWithMvcHandlers(t *testing.T, h1, h2, h3 iris.Handler) { app := iris.New() app.Get("/{id:long}/{username:string}", h1) app.Get("/service", h2) diff --git a/mvc2/mvc.go b/mvc2/mvc.go index a737aa3494..9ae844502f 100644 --- a/mvc2/mvc.go +++ b/mvc2/mvc.go @@ -20,28 +20,44 @@ func New() *Mvc { return new(Mvc) } -func (m *Mvc) RegisterBinder(binders ...interface{}) error { - for _, binder := range binders { - b, err := MakeFuncInputBinder(binder) - if err != nil { - return err +func (m *Mvc) Child() *Mvc { + child := New() + + // copy the current parent's ctx func binders and services to this new child. + if len(m.binders) > 0 { + binders := make([]*InputBinder, len(m.binders), len(m.binders)) + for i, v := range m.binders { + binders[i] = v } - m.binders = append(m.binders, b) + child.binders = binders } - return nil + return child } -func (m *Mvc) RegisterService(services ...interface{}) error { - for _, service := range services { - b, err := MakeServiceInputBinder(service) +func (m *Mvc) In(binders ...interface{}) { + for _, binder := range binders { + typ := resolveBinderType(binder) + + var ( + b *InputBinder + err error + ) + + if typ == functionType { + b, err = MakeFuncInputBinder(binder) + } else if typ == serviceType { + b, err = MakeServiceInputBinder(binder) + } else { + err = errBad + } + if err != nil { - return err + continue } + m.binders = append(m.binders, b) } - - return nil } func (m *Mvc) Handler(handler interface{}) context.Handler { diff --git a/mvc2/mvc_test.go b/mvc2/mvc_test.go new file mode 100644 index 0000000000..e8ab81b7f4 --- /dev/null +++ b/mvc2/mvc_test.go @@ -0,0 +1,22 @@ +package mvc2_test + +// black-box in combination with the handler_test + +import ( + "testing" + + . "github.com/kataras/iris/mvc2" +) + +func TestMvcInAndHandler(t *testing.T) { + m := New() + m.In(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) + + var ( + h1 = m.Handler(testConsumeUserHandler) + h2 = m.Handler(testConsumeServiceHandler) + h3 = m.Handler(testConsumeParamHandler) + ) + + testAppWithMvcHandlers(t, h1, h2, h3) +} From 8bec05ffd421fd8ef071f0cd284040ddcdcc5b3a Mon Sep 17 00:00:00 2001 From: kataras Date: Fri, 24 Nov 2017 20:11:32 +0200 Subject: [PATCH 06/79] remove codesponsor as http://mailchi.mp/f9b57b5ea377/code-sponsor-is-shutting-down-on-december-8 --- README.md | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/README.md b/README.md index 9a4bbcf524..bb656678d3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # [![Logo created by @santoshanand](logo_white_35_24.png)](https://iris-go.com) Iris -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)[![Backers on Open Collective](https://opencollective.com/iris/backers/badge.svg?style=flat-square)](#backers)[![Sponsors on Open Collective](https://opencollective.com/iris/sponsors/badge.svg?style=flat-square)](#sponsors)[![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris)[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)[![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)[![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples)[![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris)[![CLA assistant](https://cla-assistant.io/readme/badge/kataras/iris?style=flat-square)](https://cla-assistant.io/kataras/iris) - - - Sponsor - +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)[![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris)[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)[![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)[![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples)[![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris)[![CLA assistant](https://cla-assistant.io/readme/badge/kataras/iris?style=flat-square)](https://cla-assistant.io/kataras/iris) Iris is a fast, simple and efficient web framework for Go. @@ -1051,17 +1047,6 @@ The form contains some questions that you may need to answer in order to learn m https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link -## Contributors - -This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md). - - -## Backers - -Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/iris#backer) - - - ## Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/iris#sponsor) From ee6f108be85d04e7bc2ae27ae785cd2739a9589b Mon Sep 17 00:00:00 2001 From: kataras Date: Sat, 25 Nov 2017 14:04:35 +0200 Subject: [PATCH 07/79] implement a simple path param binder --- mvc2/binder.go | 13 +++++++- mvc2/handler.go | 49 +++++++++++++++++++++++++----- mvc2/path_param.go | 44 +++++++++++++++++++++++++++ mvc2/path_param_test.go | 66 +++++++++++++++++++++++++++++++++++++++++ mvc2/reflect.go | 13 ++++++++ 5 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 mvc2/path_param.go create mode 100644 mvc2/path_param_test.go diff --git a/mvc2/binder.go b/mvc2/binder.go index 1b87e141a0..baba2a9d48 100644 --- a/mvc2/binder.go +++ b/mvc2/binder.go @@ -42,8 +42,19 @@ func getBindersForInput(binders []*InputBinder, expected ...reflect.Type) map[in } continue } + for _, b := range binders { // if same type or the result of binder implements the expected in's type. + /* + // no f. this, it's too complicated and it will be harder to maintain later on: + // if has slice we can't know the returning len from now + // so the expected input length and the len(m) are impossible to guess. + if isSliceAndExpectedItem(b.BindType, expected, idx) { + hasSlice = true + m[idx] = b + continue + } + */ if equalTypes(b.BindType, in) { if m == nil { m = make(map[int]*InputBinder) @@ -134,7 +145,7 @@ func makeFuncInputBinder(fn reflect.Value) (*InputBinder, error) { bf := func(ctxValue []reflect.Value) reflect.Value { // []reflect.Value{reflect.ValueOf(ctx)} - results := fn.Call(ctxValue) + results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler. if len(results) == 0 { return zeroOutVal } diff --git a/mvc2/handler.go b/mvc2/handler.go index a0ea70cc9e..bda3dc05a5 100644 --- a/mvc2/handler.go +++ b/mvc2/handler.go @@ -67,6 +67,12 @@ func MakeHandler(handler interface{}, binders []*InputBinder) (context.Handler, } m := getBindersForInput(binders, typIn...) + + /* + // no f. this, it's too complicated and it will be harder to maintain later on: + // the only case that these are not equal is when + // binder returns a slice and input contains one or more inputs. + */ if len(m) != n { err := fmt.Errorf("input arguments length(%d) of types(%s) and valid binders length(%d) are not equal", n, typIn, len(m)) golog.Errorf("mvc handler: %v", err) @@ -76,12 +82,15 @@ func MakeHandler(handler interface{}, binders []*InputBinder) (context.Handler, hasIn := len(m) > 0 fn := reflect.ValueOf(handler) - resultHandler := func(ctx context.Context) { - if !hasIn { + // if has no input to bind then execute the "handler" using the mvc style + // for any output parameters. + if !hasIn { + return func(ctx context.Context) { methodfunc.DispatchFuncResult(ctx, fn.Call(emptyIn)) - return - } + }, nil + } + return func(ctx context.Context) { // we could use other tricks for "in" // here but let's stick to that which is clearly // that it doesn't keep any previous state @@ -94,9 +103,35 @@ func MakeHandler(handler interface{}, binders []*InputBinder) (context.Handler, ctxValues := []reflect.Value{reflect.ValueOf(ctx)} for k, v := range m { in[k] = v.BindFunc(ctxValues) + /* + // no f. this, it's too complicated and it will be harder to maintain later on: + // now an additional check if it's array and has more inputs of the same type + // and all these results to the expected inputs. + // n-1: if has more to set. + result := v.BindFunc(ctxValues) + if isSliceAndExpectedItem(result.Type(), in, k) { + // if kind := result.Kind(); (kind == reflect.Slice || kind == reflect.Array) && n-1 > k { + prev := 0 + for j, nn := 1, result.Len(); j < nn; j++ { + item := result.Slice(prev, j) + prev++ + // remember; we already set the inputs type, so we know + // what the function expected to have. + if !equalTypes(item.Type(), in[k+1].Type()) { + break + } + + in[k+1] = item + } + } else { + in[k] = result + } + */ + + if ctx.IsStopped() { + return + } } methodfunc.DispatchFuncResult(ctx, fn.Call(in)) - } - - return resultHandler, nil + }, nil } diff --git a/mvc2/path_param.go b/mvc2/path_param.go new file mode 100644 index 0000000000..ea0da3f735 --- /dev/null +++ b/mvc2/path_param.go @@ -0,0 +1,44 @@ +package mvc2 + +import ( + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/memstore" +) + +// PathParams is the context's named path parameters, see `PathParamsBinder` too. +type PathParams = context.RequestParams + +// PathParamsBinder is the binder which will bind the `PathParams` type value to the specific +// handler's input argument, see `PathParams` as well. +func PathParamsBinder(ctx context.Context) PathParams { + return *ctx.Params() +} + +// PathParam describes a named path parameter, it's the result of the PathParamBinder and the expected +// handler func's input argument's type, see `PathParamBinder` too. +type PathParam struct { + memstore.Entry + Empty bool +} + +// PathParamBinder is the binder which binds a handler func's input argument to a named path parameter +// based on its name, see `PathParam` as well. +func PathParamBinder(name string) func(ctx context.Context) PathParam { + return func(ctx context.Context) PathParam { + e, found := ctx.Params().GetEntry(name) + if !found { + + // useless check here but it doesn't hurt, + // useful only when white-box tests run. + if ctx.Application() != nil { + ctx.Application().Logger().Warnf(ctx.HandlerName()+": expected parameter name '%s' to be described in the route's path in order to be received by the `ParamBinder`, please fix it.\n The main handler will not be executed for your own protection.", name) + } + + ctx.StopExecution() + return PathParam{ + Empty: true, + } + } + return PathParam{e, false} + } +} diff --git a/mvc2/path_param_test.go b/mvc2/path_param_test.go new file mode 100644 index 0000000000..c10e47c5d2 --- /dev/null +++ b/mvc2/path_param_test.go @@ -0,0 +1,66 @@ +package mvc2 + +import ( + "testing" + + "github.com/kataras/iris/context" +) + +func TestPathParamsBinder(t *testing.T) { + m := New() + m.In(PathParamsBinder) + + got := "" + + h := m.Handler(func(params PathParams) { + got = params.Get("firstname") + params.Get("lastname") + }) + + ctx := context.NewContext(nil) + ctx.Params().Set("firstname", "Gerasimos") + ctx.Params().Set("lastname", "Maropoulos") + h(ctx) + expected := "GerasimosMaropoulos" + if got != expected { + t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got) + } +} +func TestPathParamBinder(t *testing.T) { + m := New() + m.In(PathParamBinder("username")) + + got := "" + executed := false + h := m.Handler(func(username PathParam) { + // this should not be fired at all if "username" param wasn't found at all. + // although router is responsible for that but the `ParamBinder` makes that check as well because + // the end-developer may put a param as input argument on her/his function but + // on its route's path didn't describe the path parameter, + // the handler fires a warning and stops the execution for the invalid handler to protect the user. + executed = true + got = username.String() + }) + + expectedUsername := "kataras" + ctx := context.NewContext(nil) + ctx.Params().Set("username", expectedUsername) + h(ctx) + + if got != expectedUsername { + t.Fatalf("expected the param 'username' to be '%s' but got '%s'", expectedUsername, got) + } + + // test the non executed if param not found. + executed = false + got = "" + + ctx2 := context.NewContext(nil) + h(ctx2) + + if got != "" { + t.Fatalf("expected the param 'username' to be entirely empty but got '%s'", got) + } + if executed { + t.Fatalf("expected the handler to not be executed") + } +} diff --git a/mvc2/reflect.go b/mvc2/reflect.go index 3e45cc1a93..931aa4b8e5 100644 --- a/mvc2/reflect.go +++ b/mvc2/reflect.go @@ -33,6 +33,19 @@ func isFunc(typ reflect.Type) bool { return typ.Kind() == reflect.Func } +/* +// no f. this, it's too complicated and it will be harder to maintain later on: +func isSliceAndExpectedItem(got reflect.Type, in []reflect.Type, currentBindersIdx int) bool { + kind := got.Kind() + // if got result is slice or array. + return (kind == reflect.Slice || kind == reflect.Array) && + // if has expected next input. + len(in)-1 > currentBindersIdx && + // if the current input's type is not the same as got (if it's not a slice of that types or anything else). + equalTypes(got, in[currentBindersIdx]) +} +*/ + func equalTypes(got reflect.Type, expected reflect.Type) bool { if got == expected { return true From 9113471684edb08fac5d78efd3f7efa8f2223d05 Mon Sep 17 00:00:00 2001 From: kataras Date: Mon, 27 Nov 2017 21:39:57 +0200 Subject: [PATCH 08/79] implement a way to add controller functions as handlers with the existing rules respected but it's a bit dirty I will change the implementation and move the mvc2 to mvc and make the api builder's PartyFunc to be a critical part of the controller and the mvc2.Mvc bind values should be also respected to the controller and more --- core/router/api_builder.go | 10 +- core/router/party.go | 7 + mvc/activator/activate_listener.go | 60 +------ mvc/activator/activator.go | 208 +++++++++++++++++++----- mvc/activator/binder.go | 91 ++++++----- mvc/activator/methodfunc/func_info.go | 57 ++++--- mvc/activator/methodfunc/func_parser.go | 1 + mvc/activator/methodfunc/methodfunc.go | 36 +++- mvc/controller_test.go | 5 +- mvc/go19.go | 11 +- mvc/session_controller.go | 5 +- mvc2/controller.go | 96 +++++++++++ mvc2/controller_handler_test.go | 81 +++++++++ mvc2/handler_test.go | 13 +- mvc2/mvc.go | 4 +- mvc2/mvc_test.go | 3 +- 16 files changed, 489 insertions(+), 199 deletions(-) create mode 100644 mvc2/controller.go create mode 100644 mvc2/controller_handler_test.go diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 475b68d749..5ce9150bd2 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -190,11 +190,11 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co // otherwise use `Party` which can handle many paths with different handlers and middlewares. // // Usage: -// app.HandleMany(iris.MethodGet, "/user /user/{id:int} /user/me", userHandler) +// app.HandleMany("GET", "/user /user/{id:int} /user/me", genericUserHandler) // At the other side, with `Handle` we've had to write: -// app.Handle(iris.MethodGet, "/user", userHandler) -// app.Handle(iris.MethodGet, "/user/{id:int}", userHandler) -// app.Handle(iris.MethodGet, "/user/me", userHandler) +// app.Handle("GET", "/user", userHandler) +// app.Handle("GET", "/user/{id:int}", userByIDHandler) +// app.Handle("GET", "/user/me", userMeHandler) // // This method is used behind the scenes at the `Controller` function // in order to handle more than one paths for the same controller instance. @@ -536,7 +536,7 @@ func (api *APIBuilder) Any(relativePath string, handlers ...context.Handler) (ro func (api *APIBuilder) Controller(relativePath string, controller activator.BaseController, bindValues ...interface{}) (routes []*Route) { - registerFunc := func(ifRelPath string, method string, handlers ...context.Handler) { + registerFunc := func(method string, ifRelPath string, handlers ...context.Handler) { relPath := relativePath + ifRelPath r := api.HandleMany(method, relPath, handlers...) routes = append(routes, r...) diff --git a/core/router/party.go b/core/router/party.go index 1683446820..ea0fecc6f1 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -2,6 +2,7 @@ package router import ( "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router/macro" "github.com/kataras/iris/mvc/activator" ) @@ -13,6 +14,12 @@ import ( // // Look the "APIBuilder" for its implementation. type Party interface { + // Macros returns the macro map which is responsible + // to register custom macro functions for all routes. + // + // Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path + Macros() *macro.Map + // Party groups routes which may have the same prefix and share same handlers, // returns that new rich subrouter. // diff --git a/mvc/activator/activate_listener.go b/mvc/activator/activate_listener.go index 708d08fcfb..3e2a6120f0 100644 --- a/mvc/activator/activate_listener.go +++ b/mvc/activator/activate_listener.go @@ -1,22 +1,13 @@ package activator -import ( - "reflect" -) - -// CallOnActivate simply calls the "controller"'s `OnActivate(*ActivatePayload)` function, +// CallOnActivate simply calls the "controller"'s `OnActivate(*TController)` function, // if any. // // Look `activator.go#Register` and `ActivateListener` for more. -func CallOnActivate(controller interface{}, - bindValues *[]interface{}, registerFunc RegisterFunc) { +func CallOnActivate(controller interface{}, tController *TController) { if ac, ok := controller.(ActivateListener); ok { - p := &ActivatePayload{ - BindValues: bindValues, - Handle: registerFunc, - } - ac.OnActivate(p) + ac.OnActivate(tController) } } @@ -27,52 +18,13 @@ func CallOnActivate(controller interface{}, // then the `OnActivate` function will be called ONCE, NOT in every request // but ONCE at the application's lifecycle. type ActivateListener interface { - // OnActivate accepts a pointer to the `ActivatePayload`. + // OnActivate accepts a pointer to the `TController`. // // The `Controller` can make use of the `OnActivate` function // to register custom routes // or modify the provided values that will be binded to the // controller later on. // - // Look `ActivatePayload` for more. - OnActivate(*ActivatePayload) -} - -// ActivatePayload contains the necessary information and the ability -// to alt a controller's registration options, i.e the binder. -// -// With `ActivatePayload` the `Controller` can register custom routes -// or modify the provided values that will be binded to the -// controller later on. -type ActivatePayload struct { - BindValues *[]interface{} - Handle RegisterFunc -} - -// EnsureBindValue will make sure that this "bindValue" -// will be registered to the controller's binder -// if its type is not already passed by the caller.. -// -// For example, on `SessionController` it looks if *sessions.Sessions -// has been binded from the caller and if not then the "bindValue" -// will be binded and used as a default sessions manager instead. -// -// At general, if the caller has already provided a value with the same Type -// then the "bindValue" will be ignored and not be added to the controller's bind values. -// -// Returns true if the caller has NOT already provided a value with the same Type -// and "bindValue" is NOT ignored therefore is appended to the controller's bind values. -func (i *ActivatePayload) EnsureBindValue(bindValue interface{}) bool { - valueTyp := reflect.TypeOf(bindValue) - localBindValues := *i.BindValues - - for _, bindedValue := range localBindValues { - // type already exists, remember: binding here is per-type. - if reflect.TypeOf(bindedValue) == valueTyp { - return false - } - } - - *i.BindValues = append(localBindValues, bindValue) - return true + // Look `TController` for more. + OnActivate(*TController) } diff --git a/mvc/activator/activator.go b/mvc/activator/activator.go index 67ac4a39b3..4be839a4f5 100644 --- a/mvc/activator/activator.go +++ b/mvc/activator/activator.go @@ -4,6 +4,7 @@ import ( "reflect" "strings" + "github.com/kataras/iris/core/router/macro" "github.com/kataras/iris/mvc/activator/methodfunc" "github.com/kataras/iris/mvc/activator/model" "github.com/kataras/iris/mvc/activator/persistence" @@ -32,6 +33,12 @@ type ( // we need this to collect and save the persistence fields' values. Value reflect.Value + valuePtr reflect.Value + // // Methods and handlers, available after the Activate, can be seted `OnActivate` event as well. + // Methods []methodfunc.MethodFunc + + Router RegisterFunc + binder *binder // executed even before the BeginRequest if not nil. modelController *model.Controller persistenceController *persistence.Controller @@ -69,37 +76,33 @@ type BaseController interface { } // ActivateController returns a new controller type info description. -func ActivateController(base BaseController, bindValues []interface{}) (TController, error) { +func newController(base BaseController, router RegisterFunc) (*TController, error) { // get and save the type. typ := reflect.TypeOf(base) if typ.Kind() != reflect.Ptr { typ = reflect.PtrTo(typ) } + valPointer := reflect.ValueOf(base) // or value raw + // first instance value, needed to validate // the actual type of the controller field // and to collect and save the instance's persistence fields' // values later on. - val := reflect.Indirect(reflect.ValueOf(base)) + val := reflect.Indirect(valPointer) + ctrlName := val.Type().Name() pkgPath := val.Type().PkgPath() fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName - // set the binder, can be nil this check at made at runtime. - binder := newBinder(typ.Elem(), bindValues) - if binder != nil { - for _, bf := range binder.fields { - golog.Debugf("MVC %s: binder loaded for '%s' with value:\n%#v", - fullName, bf.GetFullName(), bf.GetValue()) - } - } - - t := TController{ + t := &TController{ Name: ctrlName, FullName: fullName, Type: typ, Value: val, - binder: binder, + valuePtr: valPointer, + Router: router, + binder: &binder{elemType: typ.Elem()}, modelController: model.Load(typ), persistenceController: persistence.Load(typ, val), } @@ -107,12 +110,35 @@ func ActivateController(base BaseController, bindValues []interface{}) (TControl return t, nil } +// BindValueTypeExists returns true if at least one type of "bindValue" +// is already binded to this `TController`. +func (t *TController) BindValueTypeExists(bindValue interface{}) bool { + valueTyp := reflect.TypeOf(bindValue) + for _, bindedValue := range t.binder.values { + // type already exists, remember: binding here is per-type. + if typ := reflect.TypeOf(bindedValue); typ == valueTyp || + (valueTyp.Kind() == reflect.Interface && typ.Implements(valueTyp)) { + return true + } + } + + return false +} + +// BindValue binds a value to a controller's field when request is served. +func (t *TController) BindValue(bindValues ...interface{}) { + for _, bindValue := range bindValues { + t.binder.bind(bindValue) + } +} + // HandlerOf builds the handler for a type based on the specific method func. -func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler { +func (t *TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler { var ( // shared, per-controller - elem = t.Type.Elem() - ctrlName = t.Name + elem = t.Type.Elem() + ctrlName = t.Name + hasBinder = !t.binder.isEmpty() hasPersistenceData = t.persistenceController != nil hasModels = t.modelController != nil @@ -123,7 +149,7 @@ func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler return func(ctx context.Context) { // create a new controller instance of that type(>ptr). c := reflect.New(elem) - if t.binder != nil { + if hasBinder { t.binder.handle(c) } @@ -163,29 +189,38 @@ func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler } } -// RegisterFunc used by the caller to register the result routes. -type RegisterFunc func(relPath string, httpMethod string, handler ...context.Handler) - -// RegisterMethodHandlers receives a `TController`, description of the -// user's controller, and calls the "registerFunc" for each of its -// method handlers. -// -// Not useful for the end-developer, but may needed for debugging -// at the future. -func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) { +func (t *TController) registerMethodFunc(m methodfunc.MethodFunc) { var middleware context.Handlers - if t.binder != nil { + if !t.binder.isEmpty() { if m := t.binder.middleware; len(m) > 0 { middleware = m } } + + h := t.HandlerOf(m) + if h == nil { + golog.Warnf("MVC %s: nil method handler found for %s", t.FullName, m.Name) + return + } + + registeredHandlers := append(middleware, h) + t.Router(m.HTTPMethod, m.RelPath, registeredHandlers...) + + golog.Debugf("MVC %s: %s %s maps to function[%d] '%s'", t.FullName, + m.HTTPMethod, + m.RelPath, + m.Index, + m.Name) +} + +func (t *TController) resolveAndRegisterMethods() { // the actual method functions // i.e for "GET" it's the `Get()`. methods, err := methodfunc.Resolve(t.Type) if err != nil { golog.Errorf("MVC %s: %s", t.FullName, err.Error()) - // don't stop here. + return } // range over the type info's method funcs, // build a new handler for each of these @@ -194,35 +229,118 @@ func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) { // responsible to convert these into routes // and add them to router via the APIBuilder. for _, m := range methods { - h := t.HandlerOf(m) - if h == nil { - golog.Warnf("MVC %s: nil method handler found for %s", t.FullName, m.Name) - continue - } - registeredHandlers := append(middleware, h) - registerFunc(m.RelPath, m.HTTPMethod, registeredHandlers...) + t.registerMethodFunc(m) + } +} - golog.Debugf("MVC %s: %s %s maps to function[%d] '%s'", t.FullName, - m.HTTPMethod, - m.RelPath, - m.Index, - m.Name) +// Handle registers a method func but with a custom http method and relative route's path, +// it respects the rest of the controller's rules and guidelines. +func (t *TController) Handle(httpMethod, path, handlerFuncName string) bool { + cTyp := t.Type // with the pointer. + m, exists := cTyp.MethodByName(handlerFuncName) + if !exists { + golog.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", + handlerFuncName, t.FullName) + return false } + + info := methodfunc.FuncInfo{ + Name: m.Name, + Trailing: m.Name, + Type: m.Type, + Index: m.Index, + HTTPMethod: httpMethod, + } + + tmpl, err := macro.Parse(path, macro.NewMap()) + if err != nil { + golog.Errorf("MVC: fail to parse the path for '%s.%s': %v", t.FullName, handlerFuncName, err) + return false + } + + paramKeys := make([]string, len(tmpl.Params), len(tmpl.Params)) + for i, param := range tmpl.Params { + paramKeys[i] = param.Name + } + + methodFunc, err := methodfunc.ResolveMethodFunc(info, paramKeys...) + if err != nil { + golog.Errorf("MVC: function '%s' inside the '%s' controller: %v", handlerFuncName, t.FullName, err) + return false + } + + methodFunc.RelPath = path + + t.registerMethodFunc(methodFunc) + return true } +// func (t *TController) getMethodFuncByName(funcName string) (methodfunc.MethodFunc, bool) { +// cVal := t.Value +// cTyp := t.Type // with the pointer. +// m, exists := cTyp.MethodByName(funcName) +// if !exists { +// golog.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", +// funcName, cTyp.String()) +// return methodfunc.MethodFunc{}, false +// } + +// fn := cVal.MethodByName(funcName) +// if !fn.IsValid() { +// golog.Errorf("MVC: function '%s' inside the '%s' controller has not a valid value", +// funcName, cTyp.String()) +// return methodfunc.MethodFunc{}, false +// } + +// info, ok := methodfunc.FetchFuncInfo(m) +// if !ok { +// golog.Errorf("MVC: could not resolve the func info from '%s'", funcName) +// return methodfunc.MethodFunc{}, false +// } + +// methodFunc, err := methodfunc.ResolveMethodFunc(info) +// if err != nil { +// golog.Errorf("MVC: %v", err) +// return methodfunc.MethodFunc{}, false +// } + +// return methodFunc, true +// } + +// // RegisterName registers a function by its name +// func (t *TController) RegisterName(funcName string) bool { +// methodFunc, ok := t.getMethodFuncByName(funcName) +// if !ok { +// return false +// } +// t.registerMethodFunc(methodFunc) +// return true +// } + +// RegisterFunc used by the caller to register the result routes. +type RegisterFunc func(httpMethod string, relPath string, handler ...context.Handler) + // Register receives a "controller", // a pointer of an instance which embeds the `Controller`, // the value of "baseControllerFieldName" should be `Controller`. func Register(controller BaseController, bindValues []interface{}, registerFunc RegisterFunc) error { - CallOnActivate(controller, &bindValues, registerFunc) - - t, err := ActivateController(controller, bindValues) + t, err := newController(controller, registerFunc) if err != nil { return err } - RegisterMethodHandlers(t, registerFunc) + t.BindValue(bindValues...) + + CallOnActivate(controller, t) + + for _, bf := range t.binder.fields { + golog.Debugf("MVC %s: binder loaded for '%s' with value:\n%#v", + t.FullName, bf.GetFullName(), bf.GetValue()) + } + + t.resolveAndRegisterMethods() + return nil } diff --git a/mvc/activator/binder.go b/mvc/activator/binder.go index b0aee3b171..81d64dd395 100644 --- a/mvc/activator/binder.go +++ b/mvc/activator/binder.go @@ -8,7 +8,15 @@ import ( "github.com/kataras/iris/context" ) +// binder accepts a value of something +// and tries to find its equalivent type +// inside the controller and sets that to it, +// after that each new instance of the controller will have +// this value on the specific field, like persistence data control does. + type binder struct { + elemType reflect.Type + // values and fields are matched on the `match`. values []interface{} fields []field.Field @@ -17,28 +25,24 @@ type binder struct { middleware context.Handlers } -// binder accepts a value of something -// and tries to find its equalivent type -// inside the controller and sets that to it, -// after that each new instance of the controller will have -// this value on the specific field, like persistence data control does. -// -// returns a nil binder if values are not valid bindable data to the controller type. -func newBinder(elemType reflect.Type, values []interface{}) *binder { - if len(values) == 0 { - return nil +func (b *binder) bind(value interface{}) { + if value == nil { + return } - b := &binder{values: values} - b.fields = b.lookup(elemType) + b.values = append(b.values, value) // keep values. + + b.match(value) +} +func (b *binder) isEmpty() bool { // if nothing valid found return nil, so the caller // can omit the binder. if len(b.fields) == 0 && len(b.middleware) == 0 { - return nil + return true } - return b + return false } func (b *binder) storeValueIfMiddleware(value reflect.Value) bool { @@ -55,41 +59,38 @@ func (b *binder) storeValueIfMiddleware(value reflect.Value) bool { return false } -func (b *binder) lookup(elem reflect.Type) (fields []field.Field) { - for _, v := range b.values { - value := reflect.ValueOf(v) - // handlers will be recognised as middleware, not struct fields. - // End-Developer has the option to call any handler inside - // the controller's `BeginRequest` and `EndRequest`, the - // state is respected from the method handler already. - if b.storeValueIfMiddleware(value) { - // stored as middleware, continue to the next field, we don't have - // to bind anything here. - continue - } - - matcher := func(elemField reflect.StructField) bool { - // If the controller's field is interface then check - // if the given binded value implements that interface. - // i.e MovieController { Service services.MovieService /* interface */ } - // app.Controller("/", new(MovieController), - // services.NewMovieMemoryService(...)) - // - // `services.NewMovieMemoryService` returns a `*MovieMemoryService` - // that implements the `MovieService` interface. - if elemField.Type.Kind() == reflect.Interface { - return value.Type().Implements(elemField.Type) - } - return elemField.Type == value.Type() - } +func (b *binder) match(v interface{}) { + value := reflect.ValueOf(v) + // handlers will be recognised as middleware, not struct fields. + // End-Developer has the option to call any handler inside + // the controller's `BeginRequest` and `EndRequest`, the + // state is respected from the method handler already. + if b.storeValueIfMiddleware(value) { + // stored as middleware, continue to the next field, we don't have + // to bind anything here. + return + } - handler := func(f *field.Field) { - f.Value = value + matcher := func(elemField reflect.StructField) bool { + // If the controller's field is interface then check + // if the given binded value implements that interface. + // i.e MovieController { Service services.MovieService /* interface */ } + // app.Controller("/", new(MovieController), + // services.NewMovieMemoryService(...)) + // + // `services.NewMovieMemoryService` returns a `*MovieMemoryService` + // that implements the `MovieService` interface. + if elemField.Type.Kind() == reflect.Interface { + return value.Type().Implements(elemField.Type) } + return elemField.Type == value.Type() + } - fields = append(fields, field.LookupFields(elem, matcher, handler)...) + handler := func(f *field.Field) { + f.Value = value } - return + + b.fields = append(b.fields, field.LookupFields(b.elemType, matcher, handler)...) } func (b *binder) handle(c reflect.Value) { diff --git a/mvc/activator/methodfunc/func_info.go b/mvc/activator/methodfunc/func_info.go index 836dfa6cd6..ca9672cc4e 100644 --- a/mvc/activator/methodfunc/func_info.go +++ b/mvc/activator/methodfunc/func_info.go @@ -53,36 +53,47 @@ func fetchInfos(typ reflect.Type) (methods []FuncInfo) { // and add that. for i, n := 0, typ.NumMethod(); i < n; i++ { m := typ.Method(i) - name := m.Name - for _, method := range availableMethods { - possibleMethodFuncName := methodTitle(method) + if method, ok := FetchFuncInfo(m); ok { + methods = append(methods, method) + } + } + return +} - if strings.Index(name, possibleMethodFuncName) == 0 { - trailing := "" - // if has chars after the method itself - if lname, lmethod := len(name), len(possibleMethodFuncName); lname > lmethod { - ch := rune(name[lmethod]) - // if the next char is upper, otherise just skip the whole func info. - if unicode.IsUpper(ch) { - trailing = name[lmethod:] - } else { - continue - } - } +// FetchFuncInfo returns a FuncInfo based on the method of the controller. +func FetchFuncInfo(m reflect.Method) (FuncInfo, bool) { + name := m.Name + + for _, method := range availableMethods { + possibleMethodFuncName := methodTitle(method) - methodInfo := FuncInfo{ - Name: name, - Trailing: trailing, - Type: m.Type, - HTTPMethod: method, - Index: m.Index, + if strings.Index(name, possibleMethodFuncName) == 0 { + trailing := "" + // if has chars after the method itself + if lname, lmethod := len(name), len(possibleMethodFuncName); lname > lmethod { + ch := rune(name[lmethod]) + // if the next char is upper, otherise just skip the whole func info. + if unicode.IsUpper(ch) { + trailing = name[lmethod:] + } else { + continue } - methods = append(methods, methodInfo) } + + info := FuncInfo{ + Name: name, + Trailing: trailing, + Type: m.Type, + HTTPMethod: method, + Index: m.Index, + } + return info, true + } } - return + + return FuncInfo{}, false } func methodTitle(httpMethod string) string { diff --git a/mvc/activator/methodfunc/func_parser.go b/mvc/activator/methodfunc/func_parser.go index 511b20909f..6ce24e7e90 100644 --- a/mvc/activator/methodfunc/func_parser.go +++ b/mvc/activator/methodfunc/func_parser.go @@ -182,6 +182,7 @@ func (a *ast) paramValues(ctx context.Context) []reflect.Value { l := len(a.paramKeys) values := make([]reflect.Value, l, l) + for i := 0; i < l; i++ { paramKey := a.paramKeys[i] paramType := a.paramTypes[i] diff --git a/mvc/activator/methodfunc/methodfunc.go b/mvc/activator/methodfunc/methodfunc.go index f76a9a9048..3a8f606552 100644 --- a/mvc/activator/methodfunc/methodfunc.go +++ b/mvc/activator/methodfunc/methodfunc.go @@ -31,20 +31,38 @@ func Resolve(typ reflect.Type) ([]MethodFunc, error) { var methodFuncs []MethodFunc infos := fetchInfos(typ) for _, info := range infos { - parser := newFuncParser(info) - a, err := parser.parse() + methodFunc, err := ResolveMethodFunc(info) if r.AddErr(err) { continue } - - methodFunc := MethodFunc{ - RelPath: a.relPath, - FuncInfo: info, - MethodCall: buildMethodCall(a), - } - methodFuncs = append(methodFuncs, methodFunc) } return methodFuncs, r.Return() } + +// ResolveMethodFunc resolves a single `MethodFunc` from a single `FuncInfo`. +func ResolveMethodFunc(info FuncInfo, paramKeys ...string) (MethodFunc, error) { + parser := newFuncParser(info) + a, err := parser.parse() + if err != nil { + return MethodFunc{}, err + } + + if len(paramKeys) > 0 { + a.paramKeys = paramKeys + } + + methodFunc := MethodFunc{ + RelPath: a.relPath, + FuncInfo: info, + MethodCall: buildMethodCall(a), + } + + /* TODO: split the method path and ast param keys, and all that + because now we want to use custom param keys but 'paramfirst' is set-ed. + + */ + + return methodFunc, nil +} diff --git a/mvc/controller_test.go b/mvc/controller_test.go index 67a1baa533..362249d5fa 100644 --- a/mvc/controller_test.go +++ b/mvc/controller_test.go @@ -306,6 +306,7 @@ func (t *testControllerBindDeep) Get() { } func TestControllerBind(t *testing.T) { app := iris.New() + // app.Logger().SetLevel("debug") t1, t2 := "my pointer title", "val title" // test bind pointer to pointer of the correct type @@ -505,8 +506,8 @@ type testControllerActivateListener struct { TitlePointer *testBindType } -func (c *testControllerActivateListener) OnActivate(p *activator.ActivatePayload) { - p.EnsureBindValue(&testBindType{ +func (c *testControllerActivateListener) OnActivate(t *activator.TController) { + t.BindValue(&testBindType{ title: "default title", }) } diff --git a/mvc/go19.go b/mvc/go19.go index 8473fa6931..7467090fae 100644 --- a/mvc/go19.go +++ b/mvc/go19.go @@ -13,15 +13,14 @@ type ( // in order to be marked as safe content, to be rendered as html and not escaped. HTML = template.HTML - // ActivatePayload contains the necessary information and the ability - // to alt a controller's registration options, i.e the binder. + // TController contains the necessary controller's pre-serve information. // - // With `ActivatePayload` the `Controller` can register custom routes + // With `TController` the `Controller` can register custom routes // or modify the provided values that will be binded to the // controller later on. // - // Look the `mvc/activator#ActivatePayload` for its implementation. + // Look the `mvc/activator#TController` for its implementation. // - // A shortcut for the `mvc/activator#ActivatePayload`, useful when `OnActivate` is being used. - ActivatePayload = activator.ActivatePayload + // A shortcut for the `mvc/activator#TController`, useful when `OnActivate` is being used. + TController = activator.TController ) diff --git a/mvc/session_controller.go b/mvc/session_controller.go index 06c17f6afd..240a88be81 100644 --- a/mvc/session_controller.go +++ b/mvc/session_controller.go @@ -24,8 +24,9 @@ type SessionController struct { // every single time the dev registers a specific SessionController-based controller. // It makes sure that its "Manager" field is filled // even if the caller didn't provide any sessions manager via the `app.Controller` function. -func (s *SessionController) OnActivate(p *activator.ActivatePayload) { - if p.EnsureBindValue(defaultManager) { +func (s *SessionController) OnActivate(t *activator.TController) { + if !t.BindValueTypeExists(defaultManager) { + t.BindValue(defaultManager) golog.Warnf(`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, therefore this controller is using the default sessions manager instead. Please refer to the documentation to learn how you can provide the session manager`) diff --git a/mvc2/controller.go b/mvc2/controller.go new file mode 100644 index 0000000000..452da4b6fc --- /dev/null +++ b/mvc2/controller.go @@ -0,0 +1,96 @@ +package mvc2 + +import ( +// "reflect" + +// "github.com/kataras/golog" +// "github.com/kataras/iris/context" +// // "github.com/kataras/iris/core/router" +// "github.com/kataras/iris/mvc/activator" +// "github.com/kataras/iris/mvc/activator/methodfunc" +) + +// no, we will not make any changes to the controller's implementation +// let's no re-write the godlike code I wrote two months ago +// , just improve it by implementing the only one missing feature: +// bind/map/handle custom controller's functions to a custom router path +// like regexed. +// +// // BaseController is the interface that all controllers should implement. +// type BaseController interface { +// BeginRequest(ctx context.Context) +// EndRequest(ctx context.Context) +// } + +// // type ControllerInitializer interface { +// // Init(r router.Party) +// // } + +// // type activator struct { +// // Router router.Party +// // container *Mvc +// // } + +// func registerController(m *Mvc, r router.Party, c BaseController) { + +// } + +// // ControllerHandler is responsible to dynamically bind a controller's functions +// // to the controller's http mechanism, can be used on the controller's `OnActivate` event. +// func ControllerHandler(controller activator.BaseController, funcName string) context.Handler { +// // we use funcName instead of an interface{} which can be safely binded with something like: +// // myController.HandleThis because we want to make sure that the end-developer +// // will make use a function of that controller that owns it because if not then +// // the BeginRequest and EndRequest will be called from other handler and also +// // the first input argument, which should be the controller itself may not be binded +// // to the current controller, all that are solved if end-dev knows what to do +// // but we can't bet on it. + +// cVal := reflect.ValueOf(controller) +// elemTyp := reflect.TypeOf(controller) // with the pointer. +// m, exists := elemTyp.MethodByName(funcName) +// if !exists { +// golog.Errorf("mvc controller handler: function '%s' doesn't exist inside the '%s' controller", +// funcName, elemTyp.String()) +// return nil +// } + +// fn := cVal.MethodByName(funcName) +// if !fn.IsValid() { +// golog.Errorf("mvc controller handler: function '%s' inside the '%s' controller has not a valid value", +// funcName, elemTyp.String()) +// return nil +// } + +// info, ok := methodfunc.FetchFuncInfo(m) +// if !ok { +// golog.Errorf("mvc controller handler: could not resolve the func info from '%s'", funcName) +// return nil +// } + +// methodFunc, err := methodfunc.ResolveMethodFunc(info) +// if err != nil { +// golog.Errorf("mvc controller handler: %v", err) +// return nil +// } + +// m := New() +// m.In(controller) // bind the controller itself? +// /// TODO: first we must enable interface{} to be used as 'servetime input binder' +// // because it will try to match the type and add to its input if the +// // func input is that, and this binder will be available to every handler after that, +// // so it will be included to its 'in'. +// // MakeFuncInputBinder(func(ctx context.Context) interface{} { + +// // // job here. + +// // return controller +// // }) + +// h := m.Handler(fn.Interface()) +// return func(ctx context.Context) { +// controller.BeginRequest(ctx) +// h(ctx) +// controller.EndRequest(ctx) +// } +// } diff --git a/mvc2/controller_handler_test.go b/mvc2/controller_handler_test.go new file mode 100644 index 0000000000..d3e11ea716 --- /dev/null +++ b/mvc2/controller_handler_test.go @@ -0,0 +1,81 @@ +package mvc2_test + +import ( + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/httptest" + "github.com/kataras/iris/mvc" + // "github.com/kataras/iris/mvc/activator/methodfunc" + //. "github.com/kataras/iris/mvc2" +) + +type testController struct { + mvc.C + Service *TestServiceImpl + + reqField string +} + +func (c *testController) Get() string { + return "index" +} + +func (c *testController) BeginRequest(ctx iris.Context) { + c.C.BeginRequest(ctx) + c.reqField = ctx.URLParam("reqfield") +} + +func (c *testController) OnActivate(t *mvc.TController) { + t.Handle("GET", "/histatic", "HiStatic") + t.Handle("GET", "/hiservice", "HiService") + t.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") + t.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") +} + +func (c *testController) HiStatic() string { + return c.reqField +} + +func (c *testController) HiService() string { + return c.Service.Say("hi") +} + +func (c *testController) HiParamBy(v string) string { + return v +} + +func (c *testController) HiParamEmptyInputBy() string { + return "empty in but served with ctx.Params.Get('ps')=" + c.Ctx.Params().Get("ps") +} + +func TestControllerHandler(t *testing.T) { + app := iris.New() + app.Controller("/", new(testController), &TestServiceImpl{prefix: "service:"}) + e := httptest.New(t, app, httptest.LogLevel("debug")) + + // test the index, is not part of the current package's implementation but do it. + e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("index") + + // the important things now. + + // this test ensures that the BeginRequest of the controller will be + // called correctly and also the controller is binded to the first input argument + // (which is the function's receiver, if any, in this case the *testController in go). + expectedReqField := "this is a request field filled by this url param" + e.GET("/histatic").WithQuery("reqfield", expectedReqField).Expect().Status(httptest.StatusOK). + Body().Equal(expectedReqField) + // this test makes sure that the binded values of the controller is handled correctly + // and can be used in a user-defined, dynamic "mvc handler". + e.GET("/hiservice").Expect().Status(httptest.StatusOK). + Body().Equal("service: hi") + + // this worked with a temporary variadic on the resolvemethodfunc which is not + // correct design, I should split the path and params with the rest of implementation + // in order a simple template.Src can be given. + e.GET("/hiparam/value").Expect().Status(httptest.StatusOK). + Body().Equal("value") + e.GET("/hiparamempyinput/value").Expect().Status(httptest.StatusOK). + Body().Equal("empty in but served with ctx.Params.Get('ps')=value") + +} diff --git a/mvc2/handler_test.go b/mvc2/handler_test.go index 3a80672882..bc8def58fd 100644 --- a/mvc2/handler_test.go +++ b/mvc2/handler_test.go @@ -28,22 +28,25 @@ func testBinderFunc(ctx iris.Context) testUserStruct { // service type ( - testService interface { + // these TestService and TestServiceImpl could be in lowercase, unexported + // but the `Say` method should be exported however we have those exported + // because of the controller handler test. + TestService interface { Say(string) string } - testServiceImpl struct { + TestServiceImpl struct { prefix string } ) -func (s *testServiceImpl) Say(message string) string { +func (s *TestServiceImpl) Say(message string) string { return s.prefix + " " + message } var ( // binders, as user-defined testBinderFuncUserStruct = testBinderFunc - testBinderService = &testServiceImpl{prefix: "say"} + testBinderService = &TestServiceImpl{prefix: "say"} testBinderFuncParam = func(ctx iris.Context) string { return ctx.Params().Get("param") } @@ -56,7 +59,7 @@ var ( } // just one input arg, the service which is binded by the #2 service binder. - testConsumeServiceHandler = func(service testService) string { + testConsumeServiceHandler = func(service TestService) string { return service.Say("something") } // just one input arg, a standar string which is binded by the #3 func(ctx) any binder. diff --git a/mvc2/mvc.go b/mvc2/mvc.go index 9ae844502f..e02bc43db7 100644 --- a/mvc2/mvc.go +++ b/mvc2/mvc.go @@ -35,7 +35,7 @@ func (m *Mvc) Child() *Mvc { return child } -func (m *Mvc) In(binders ...interface{}) { +func (m *Mvc) In(binders ...interface{}) *Mvc { for _, binder := range binders { typ := resolveBinderType(binder) @@ -58,6 +58,8 @@ func (m *Mvc) In(binders ...interface{}) { m.binders = append(m.binders, b) } + + return m } func (m *Mvc) Handler(handler interface{}) context.Handler { diff --git a/mvc2/mvc_test.go b/mvc2/mvc_test.go index e8ab81b7f4..4bcbfe062b 100644 --- a/mvc2/mvc_test.go +++ b/mvc2/mvc_test.go @@ -9,8 +9,7 @@ import ( ) func TestMvcInAndHandler(t *testing.T) { - m := New() - m.In(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) + m := New().In(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) var ( h1 = m.Handler(testConsumeUserHandler) From 57c798ff6ae500fe57f589a7165e3837b8b43bab Mon Sep 17 00:00:00 2001 From: kataras Date: Mon, 4 Dec 2017 05:06:03 +0200 Subject: [PATCH 09/79] made it work but looking for another approach --- README.md | 2 +- _examples/http-listening/README.md | 2 +- _examples/mvc/README.md | 2 +- core/router/macro/interpreter/ast/ast.go | 62 ++- core/router/party.go | 3 + core/router/route.go | 10 +- doc.go | 2 +- mvc2/binder.go | 164 ------- mvc2/binder_test.go | 143 ------ mvc2/controller.go | 573 +++++++++++++++++++---- mvc2/controller_handler_test.go | 21 +- mvc2/handler.go | 124 +++-- mvc2/handler_test.go | 22 +- mvc2/mvc.go | 68 --- mvc2/mvc_test.go | 2 +- mvc2/path_param.go | 44 -- mvc2/path_param_test.go | 66 --- mvc2/reflect.go | 47 ++ mvc2/service.go | 264 ++++++++--- mvc2/service_test.go | 46 -- 20 files changed, 865 insertions(+), 802 deletions(-) delete mode 100644 mvc2/binder.go delete mode 100644 mvc2/binder_test.go delete mode 100644 mvc2/mvc.go delete mode 100644 mvc2/path_param.go delete mode 100644 mvc2/path_param_test.go delete mode 100644 mvc2/service_test.go diff --git a/README.md b/README.md index bb656678d3..9739bf0fb2 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ func (c *HelloWorldController) GetWelcomeBy(name string, numTimes int) { } ``` -> The [_examples/mvc](_examples/mvc) and [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) files explain each feature with simple paradigms, they show how you can take advandage of the Iris MVC Binder, Iris MVC Models and many more... +> The [_examples/mvc](_examples/mvc) and [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) files explain each feature with simple paradigms, they show how you can take advantage of the Iris MVC Binder, Iris MVC Models and many more... Every `exported` func prefixed with an HTTP Method(`Get`, `Post`, `Put`, `Delete`...) in a controller is callable as an HTTP endpoint. In the sample above, all funcs writes a string to the response. Note the comments preceding each method. diff --git a/_examples/http-listening/README.md b/_examples/http-listening/README.md index c315b41d2f..e395f31c49 100644 --- a/_examples/http-listening/README.md +++ b/_examples/http-listening/README.md @@ -66,7 +66,7 @@ func main() { } ``` -UNIX and BSD hosts can take advandage of the reuse port feature +UNIX and BSD hosts can take advantage of the reuse port feature ```go package main diff --git a/_examples/mvc/README.md b/_examples/mvc/README.md index 1593996e1e..ad13f735f9 100644 --- a/_examples/mvc/README.md +++ b/_examples/mvc/README.md @@ -175,7 +175,7 @@ func (c *HelloWorldController) Any() {} handles All method requests */ ``` -> The [_examples/mvc](https://github.com/kataras/iris/tree/master/_examples/mvc) and [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) files explain each feature with simple paradigms, they show how you can take advandage of the Iris MVC Binder, Iris MVC Models and many more... +> The [_examples/mvc](https://github.com/kataras/iris/tree/master/_examples/mvc) and [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) files explain each feature with simple paradigms, they show how you can take advantage of the Iris MVC Binder, Iris MVC Models and many more... Every `exported` func prefixed with an HTTP Method(`Get`, `Post`, `Put`, `Delete`...) in a controller is callable as an HTTP endpoint. In the sample above, all funcs writes a string to the response. Note the comments preceding each method. diff --git a/core/router/macro/interpreter/ast/ast.go b/core/router/macro/interpreter/ast/ast.go index 1e39bc8557..23e1723628 100644 --- a/core/router/macro/interpreter/ast/ast.go +++ b/core/router/macro/interpreter/ast/ast.go @@ -51,21 +51,14 @@ const ( ParamTypePath ) -// ValidKind will return true if at least one param type is supported -// for this std kind. -func ValidKind(k reflect.Kind) bool { - switch k { - case reflect.String: - fallthrough - case reflect.Int: - fallthrough - case reflect.Int64: - fallthrough - case reflect.Bool: - return true - default: - return false +func (pt ParamType) String() string { + for k, v := range paramTypes { + if v == pt { + return k + } } + + return "unexpected" } // Not because for a single reason @@ -96,6 +89,23 @@ func (pt ParamType) Kind() reflect.Kind { return reflect.Invalid // 0 } +// ValidKind will return true if at least one param type is supported +// for this std kind. +func ValidKind(k reflect.Kind) bool { + switch k { + case reflect.String: + fallthrough + case reflect.Int: + fallthrough + case reflect.Int64: + fallthrough + case reflect.Bool: + return true + default: + return false + } +} + // Assignable returns true if the "k" standard type // is assignabled to this ParamType. func (pt ParamType) Assignable(k reflect.Kind) bool { @@ -133,6 +143,30 @@ func LookupParamType(ident string) ParamType { return ParamTypeUnExpected } +// LookupParamTypeFromStd accepts the string representation of a standard go type. +// It returns a ParamType, but it may differs for example +// the alphabetical, file, path and string are all string go types, so +// make sure that caller resolves these types before this call. +// +// string matches to string +// int matches to int +// int64 matches to long +// bool matches to boolean +func LookupParamTypeFromStd(goType string) ParamType { + switch goType { + case "string": + return ParamTypeString + case "int": + return ParamTypeInt + case "int64": + return ParamTypeLong + case "bool": + return ParamTypeBoolean + default: + return ParamTypeUnExpected + } +} + // ParamStatement is a struct // which holds all the necessary information about a macro parameter. // It holds its type (string, int, alphabetical, file, path), diff --git a/core/router/party.go b/core/router/party.go index ea0fecc6f1..84b17fb0e5 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -2,6 +2,7 @@ package router import ( "github.com/kataras/iris/context" + "github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/router/macro" "github.com/kataras/iris/mvc/activator" ) @@ -14,6 +15,8 @@ import ( // // Look the "APIBuilder" for its implementation. type Party interface { + // GetReporter returns the reporter for adding errors + GetReporter() *errors.Reporter // Macros returns the macro map which is responsible // to register custom macro functions for all routes. // diff --git a/core/router/route.go b/core/router/route.go index 755e66a646..205687c151 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -23,7 +23,7 @@ type Route struct { // Handlers are the main route's handlers, executed by order. // Cannot be empty. Handlers context.Handlers - mainHandlerName string + MainHandlerName string // temp storage, they're appended to the Handlers on build. // Execution happens after Begin and main Handler(s), can be empty. doneHandlers context.Handlers @@ -61,7 +61,7 @@ func NewRoute(method, subdomain, unparsedPath, mainHandlerName string, tmpl: tmpl, Path: path, Handlers: handlers, - mainHandlerName: mainHandlerName, + MainHandlerName: mainHandlerName, FormattedPath: formattedPath, } return route, nil @@ -214,12 +214,12 @@ func (r Route) Trace() string { } printfmt += fmt.Sprintf(" %s ", r.Tmpl().Src) if l := len(r.Handlers); l > 1 { - printfmt += fmt.Sprintf("-> %s() and %d more", r.mainHandlerName, l-1) + printfmt += fmt.Sprintf("-> %s() and %d more", r.MainHandlerName, l-1) } else { - printfmt += fmt.Sprintf("-> %s()", r.mainHandlerName) + printfmt += fmt.Sprintf("-> %s()", r.MainHandlerName) } - // printfmt := fmt.Sprintf("%s: %s >> %s", r.Method, r.Subdomain+r.Tmpl().Src, r.mainHandlerName) + // printfmt := fmt.Sprintf("%s: %s >> %s", r.Method, r.Subdomain+r.Tmpl().Src, r.MainHandlerName) // if l := len(r.Handlers); l > 0 { // printfmt += fmt.Sprintf(" and %d more", l) // } diff --git a/doc.go b/doc.go index 3eaed501d5..2a618b6e14 100644 --- a/doc.go +++ b/doc.go @@ -222,7 +222,7 @@ Below you'll see some useful examples: // ListenAndServe function of the `net/http` package. app.Run(iris.Raw(&http.Server{Addr:":8080"}).ListenAndServe) -UNIX and BSD hosts can take advandage of the reuse port feature. +UNIX and BSD hosts can take advantage of the reuse port feature. Example code: diff --git a/mvc2/binder.go b/mvc2/binder.go deleted file mode 100644 index baba2a9d48..0000000000 --- a/mvc2/binder.go +++ /dev/null @@ -1,164 +0,0 @@ -package mvc2 - -import ( - "reflect" -) - -// 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 - // ctx is slice because all binder functions called by - // their `.Call` method which accepts a slice of reflect.Value, - // so on the handler maker we will allocate a slice of a single ctx once - // and used to all binders. - BindFunc func(ctx []reflect.Value) reflect.Value -} - -// getBindersForInput 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 getBindersForInput(binders []*InputBinder, expected ...reflect.Type) map[int]*InputBinder { - var m map[int]*InputBinder - - for idx, in := range expected { - if idx == 0 && isContext(in) { - // if the first is context then set it directly here. - m = make(map[int]*InputBinder) - m[0] = &InputBinder{ - BindType: contextTyp, - BindFunc: func(ctxValues []reflect.Value) reflect.Value { - return ctxValues[0] - }, - } - continue - } - - for _, b := range binders { - // if same type or the result of binder implements the expected in's type. - /* - // no f. this, it's too complicated and it will be harder to maintain later on: - // if has slice we can't know the returning len from now - // so the expected input length and the len(m) are impossible to guess. - if isSliceAndExpectedItem(b.BindType, expected, idx) { - hasSlice = true - m[idx] = b - continue - } - */ - if equalTypes(b.BindType, 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 -} - -// MustMakeFuncInputBinder calls the `MakeFuncInputBinder` and returns its first result, see its docs. -// It panics on error. -func MustMakeFuncInputBinder(binder interface{}) *InputBinder { - b, err := MakeFuncInputBinder(binder) - if err != nil { - panic(err) - } - return b -} - -type binderType uint32 - -const ( - functionType binderType = iota - serviceType - invalidType -) - -func resolveBinderType(binder interface{}) binderType { - if binder == nil { - return invalidType - } - - switch indirectTyp(reflect.TypeOf(binder)).Kind() { - case reflect.Func: - return functionType - case reflect.Struct: - return serviceType - } - - return invalidType -} - -// MakeFuncInputBinder 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`. -// -// 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 MakeFuncInputBinder(binder interface{}) (*InputBinder, error) { - v := reflect.ValueOf(binder) - return makeFuncInputBinder(v) -} - -func makeFuncInputBinder(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(ctxValue []reflect.Value) reflect.Value { - // []reflect.Value{reflect.ValueOf(ctx)} - results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler. - if len(results) == 0 { - return zeroOutVal - } - - v := results[0] - if !v.IsValid() { - return zeroOutVal - } - return v - } - - return &InputBinder{ - BindType: outTyp, - BindFunc: bf, - }, nil -} diff --git a/mvc2/binder_test.go b/mvc2/binder_test.go deleted file mode 100644 index 099a3578f7..0000000000 --- a/mvc2/binder_test.go +++ /dev/null @@ -1,143 +0,0 @@ -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, - } -} - -func TestMakeFuncInputBinder(t *testing.T) { - testMakeFuncInputBinder(t, testBinderFunc) -} - -func testMakeFuncInputBinder(t *testing.T, binder interface{}) { - b, err := MakeFuncInputBinder(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) - ctxValue := []reflect.Value{reflect.ValueOf(ctx)} - v := b.BindFunc(ctxValue) - 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) - } -} - -func testCheck(t *testing.T, 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) - } -} - -// TestGetBindersForInput 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 TestGetBindersForInput(t *testing.T) { - // binders - var ( - stringBinder = MustMakeFuncInputBinder(func(ctx context.Context) string { - return "a string" - }) - intBinder = MustMakeFuncInputBinder(func(ctx context.Context) int { - return 42 - }) - ) - // in - var ( - stringType = reflect.TypeOf("string") - intType = reflect.TypeOf(1) - ) - - // 1 - testCheck(t, "test1", true, testGetBindersForInput(t, []*InputBinder{intBinder, stringBinder}, - []interface{}{"a string", 42}, stringType, intType)) - availableBinders := []*InputBinder{stringBinder, intBinder} // different order than the fist test. - // 2 - testCheck(t, "test2", true, testGetBindersForInput(t, availableBinders, - []interface{}{"a string", 42}, stringType, intType)) - // 3 - testCheck(t, "test-3-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{42}, stringType, intType)) - // 4 - testCheck(t, "test-4-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{"a string"}, stringType, intType)) - // 5 - testCheck(t, "test-5-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{42, 42}, stringType, intType)) - // 6 - testCheck(t, "test-6-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{testUserStruct{}}, stringType, intType)) - -} - -func testGetBindersForInput(t *testing.T, binders []*InputBinder, expectingResults []interface{}, in ...reflect.Type) (errString string) { - m := getBindersForInput(binders, in...) - - if expected, got := len(expectingResults), len(m); expected != got { - return fmt.Sprintf("expected results length(%d) and valid binders length(%d) to be equal, so each input has one binder", expected, got) - } - - ctxValue := []reflect.Value{reflect.ValueOf(context.NewContext(nil))} - for idx, expected := range expectingResults { - if m[idx] != nil { - v := m[idx].BindFunc(ctxValue) - 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/controller.go b/mvc2/controller.go index 452da4b6fc..9d7aad3d84 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -1,96 +1,491 @@ package mvc2 import ( -// "reflect" + "errors" + "fmt" + "reflect" + "strings" + "unicode" -// "github.com/kataras/golog" -// "github.com/kataras/iris/context" -// // "github.com/kataras/iris/core/router" -// "github.com/kataras/iris/mvc/activator" -// "github.com/kataras/iris/mvc/activator/methodfunc" + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router" + "github.com/kataras/iris/core/router/macro" + "github.com/kataras/iris/core/router/macro/interpreter/ast" + "github.com/kataras/iris/mvc/activator/methodfunc" ) -// no, we will not make any changes to the controller's implementation -// let's no re-write the godlike code I wrote two months ago -// , just improve it by implementing the only one missing feature: -// bind/map/handle custom controller's functions to a custom router path -// like regexed. +type BaseController interface { + BeginRequest(context.Context) + EndRequest(context.Context) +} + +// C is the basic BaseController type that can be used as an embedded anonymous field +// to custom end-dev controllers. +// +// func(c *ExampleController) Get() string | +// (string, string) | +// (string, int) | +// int | +// (int, string | +// (string, error) | +// bool | +// (any, bool) | +// error | +// (int, error) | +// (customStruct, error) | +// customStruct | +// (customStruct, int) | +// (customStruct, string) | +// Result or (Result, error) +// where Get is an HTTP Method func. +// +// Look `core/router#APIBuilder#Controller` method too. // -// // BaseController is the interface that all controllers should implement. -// type BaseController interface { -// BeginRequest(ctx context.Context) -// EndRequest(ctx context.Context) -// } - -// // type ControllerInitializer interface { -// // Init(r router.Party) -// // } - -// // type activator struct { -// // Router router.Party -// // container *Mvc -// // } - -// func registerController(m *Mvc, r router.Party, c BaseController) { - -// } - -// // ControllerHandler is responsible to dynamically bind a controller's functions -// // to the controller's http mechanism, can be used on the controller's `OnActivate` event. -// func ControllerHandler(controller activator.BaseController, funcName string) context.Handler { -// // we use funcName instead of an interface{} which can be safely binded with something like: -// // myController.HandleThis because we want to make sure that the end-developer -// // will make use a function of that controller that owns it because if not then -// // the BeginRequest and EndRequest will be called from other handler and also -// // the first input argument, which should be the controller itself may not be binded -// // to the current controller, all that are solved if end-dev knows what to do -// // but we can't bet on it. - -// cVal := reflect.ValueOf(controller) -// elemTyp := reflect.TypeOf(controller) // with the pointer. -// m, exists := elemTyp.MethodByName(funcName) -// if !exists { -// golog.Errorf("mvc controller handler: function '%s' doesn't exist inside the '%s' controller", -// funcName, elemTyp.String()) -// return nil -// } - -// fn := cVal.MethodByName(funcName) -// if !fn.IsValid() { -// golog.Errorf("mvc controller handler: function '%s' inside the '%s' controller has not a valid value", -// funcName, elemTyp.String()) -// return nil -// } - -// info, ok := methodfunc.FetchFuncInfo(m) -// if !ok { -// golog.Errorf("mvc controller handler: could not resolve the func info from '%s'", funcName) -// return nil -// } - -// methodFunc, err := methodfunc.ResolveMethodFunc(info) -// if err != nil { -// golog.Errorf("mvc controller handler: %v", err) -// return nil -// } - -// m := New() -// m.In(controller) // bind the controller itself? -// /// TODO: first we must enable interface{} to be used as 'servetime input binder' -// // because it will try to match the type and add to its input if the -// // func input is that, and this binder will be available to every handler after that, -// // so it will be included to its 'in'. -// // MakeFuncInputBinder(func(ctx context.Context) interface{} { - -// // // job here. - -// // return controller -// // }) - -// h := m.Handler(fn.Interface()) -// return func(ctx context.Context) { -// controller.BeginRequest(ctx) -// h(ctx) -// controller.EndRequest(ctx) -// } -// } +// It completes the `activator.BaseController` interface. +// +// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview/web/controllers. +// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go#L17. +type C struct { + // The current context.Context. + // + // we have to name it for two reasons: + // 1: can't ignore these via reflection, it doesn't give an option to + // see if the functions is derived from another type. + // 2: end-developer may want to use some method functions + // or any fields that could be conflict with the context's. + Ctx context.Context +} + +var _ BaseController = &C{} + +// BeginRequest starts the request by initializing the `Context` field. +func (c *C) BeginRequest(ctx context.Context) { c.Ctx = ctx } + +// EndRequest does nothing, is here to complete the `BaseController` interface. +func (c *C) EndRequest(ctx context.Context) {} + +type ControllerActivator struct { + Engine *Engine + // the router is used on the `Activate` and can be used by end-dev on the `OnActivate` + // to register any custom controller's functions as handlers but we will need it here + // in order to not create a new type like `ActivationPayload` for the `OnActivate`. + Router router.Party + + initRef BaseController // the BaseController as it's passed from the end-dev. + + // FullName it's the last package path segment + "." + the Name. + // i.e: if login-example/user/controller.go, the FullName is "user.Controller". + FullName string + + // key = the method's name. + methods map[string]reflect.Method + + // services []field + // bindServices func(elem reflect.Value) + s services +} + +func newControllerActivator(engine *Engine, router router.Party, controller BaseController) *ControllerActivator { + c := &ControllerActivator{ + Engine: engine, + Router: router, + initRef: controller, + } + + c.analyze() + return c +} + +var reservedMethodNames = []string{ + "BeginRequest", + "EndRequest", + "OnActivate", +} + +func isReservedMethod(name string) bool { + for _, s := range reservedMethodNames { + if s == name { + return true + } + } + + return false +} + +func (c *ControllerActivator) analyze() { + + // set full name. + { + // first instance value, needed to validate + // the actual type of the controller field + // and to collect and save the instance's persistence fields' + // values later on. + val := reflect.Indirect(reflect.ValueOf(c.initRef)) + + ctrlName := val.Type().Name() + pkgPath := val.Type().PkgPath() + fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName + c.FullName = fullName + } + + // set all available, exported methods. + { + typ := reflect.TypeOf(c.initRef) // typ, with pointer + n := typ.NumMethod() + c.methods = make(map[string]reflect.Method, n) + for i := 0; i < n; i++ { + m := typ.Method(i) + key := m.Name + + if !isReservedMethod(key) { + c.methods[key] = m + } + } + } + + // set field index with matching service binders, if any. + { + // typ := indirectTyp(reflect.TypeOf(c.initRef)) // element's typ. + + c.s = getServicesFor(reflect.ValueOf(c.initRef), c.Engine.Input) + // c.bindServices = getServicesBinderForStruct(c.Engine.binders, typ) + } + + c.analyzeAndRegisterMethods() +} + +func (c *ControllerActivator) Handle(method, path, funcName string, middleware ...context.Handler) error { + if method == "" || path == "" || funcName == "" || isReservedMethod(funcName) { + // isReservedMethod -> if it's already registered + // by a previous Handle or analyze methods internally. + return errSkip + } + + m, ok := c.methods[funcName] + if !ok { + err := fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", + funcName, c.FullName) + c.Router.GetReporter().AddErr(err) + return err + } + + tmpl, err := macro.Parse(path, c.Router.Macros()) + if err != nil { + err = fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.FullName, funcName, err) + c.Router.GetReporter().AddErr(err) + return err + } + + fmt.Printf("===============%s.%s==============\n", c.FullName, funcName) + funcIn := getInputArgsFromFunc(m.Type)[1:] // except the receiver, which is the controller pointer itself. + + // get any binders for this func, if any, and + // take param binders, we can bind them because we know the path here. + // binders := joinBindersMap( + // getBindersForInput(c.Engine.binders, funcIn...), + // getPathParamsBindersForInput(tmpl.Params, funcIn...)) + + s := getServicesFor(m.Func, getPathParamsForInput(tmpl.Params, funcIn...)) + // s.AddSource(indirectVal(reflect.ValueOf(c.initRef)), c.Engine.Input...) + + typ := reflect.TypeOf(c.initRef) + elem := indirectTyp(typ) // the value, not the pointer. + hasInputBinders := len(s) > 0 + hasStructBinders := len(c.s) > 0 + n := len(funcIn) + 1 + + // be, _ := typ.MethodByName("BeginRequest") + // en, _ := typ.MethodByName("EndRequest") + // beginIndex, endIndex := be.Index, en.Index + + handler := func(ctx context.Context) { + + // create a new controller instance of that type(>ptr). + ctrl := reflect.New(elem) + //ctrlAndCtxValues := []reflect.Value{ctrl, ctxValue[0]} + // ctrl.MethodByName("BeginRequest").Call(ctxValue) + //begin.Func.Call(ctrlAndCtxValues) + b := ctrl.Interface().(BaseController) // the Interface(). is faster than MethodByName or pre-selected methods. + // init the request. + b.BeginRequest(ctx) + //ctrl.Method(beginIndex).Call(ctxValue) + // if begin request stopped the execution. + if ctx.IsStopped() { + return + } + + if hasStructBinders { + elem := ctrl.Elem() + c.s.FillStructStaticValues(elem) + } + + if !hasInputBinders { + methodfunc.DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) + } else { + in := make([]reflect.Value, n, n) + // in[0] = ctrl.Elem() + in[0] = ctrl + s.FillFuncInput([]reflect.Value{reflect.ValueOf(ctx)}, &in) + methodfunc.DispatchFuncResult(ctx, m.Func.Call(in)) + // in := make([]reflect.Value, n, n) + // ctxValues := []reflect.Value{reflect.ValueOf(ctx)} + // for k, v := range binders { + // in[k] = v.BindFunc(ctxValues) + + // if ctx.IsStopped() { + // return + // } + // } + // methodfunc.DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(in)) + } + + // end the request, don't check for stopped because this does the actual writing + // if no response written already. + b.EndRequest(ctx) + // ctrl.MethodByName("EndRequest").Call(ctxValue) + // end.Func.Call(ctrlAndCtxValues) + //ctrl.Method(endIndex).Call(ctxValue) + } + + // register the handler now. + r := c.Router.Handle(method, path, append(middleware, handler)...) + // change the main handler's name in order to respect the controller's and give + // a proper debug message. + r.MainHandlerName = fmt.Sprintf("%s.%s", c.FullName, funcName) + // add this as a reserved method name in order to + // be sure that the same func will not be registered again, even if a custom .Handle later on. + reservedMethodNames = append(reservedMethodNames, funcName) + return nil +} + +func (c *ControllerActivator) analyzeAndRegisterMethods() { + for _, m := range c.methods { + funcName := m.Name + httpMethod, httpPath, err := parse(m) + if err != nil && err != errSkip { + err = fmt.Errorf("MVC: fail to parse the path and method for '%s.%s': %v", c.FullName, m.Name, err) + c.Router.GetReporter().AddErr(err) + continue + } + + c.Handle(httpMethod, httpPath, funcName) + } +} + +const ( + tokenBy = "By" + tokenWildcard = "Wildcard" // i.e ByWildcard +) + +// word lexer, not characters. +type lexer struct { + words []string + cur int +} + +func newLexer(s string) *lexer { + l := new(lexer) + l.reset(s) + return l +} + +func (l *lexer) reset(s string) { + l.cur = -1 + var words []string + if s != "" { + end := len(s) + start := -1 + + for i, n := 0, end; i < n; i++ { + c := rune(s[i]) + if unicode.IsUpper(c) { + // it doesn't count the last uppercase + if start != -1 { + end = i + words = append(words, s[start:end]) + } + start = i + continue + } + end = i + 1 + } + + if end > 0 && len(s) >= end { + words = append(words, s[start:end]) + } + } + + l.words = words +} + +func (l *lexer) next() (w string) { + cur := l.cur + 1 + + if w = l.peek(cur); w != "" { + l.cur++ + } + + return +} + +func (l *lexer) skip() { + if cur := l.cur + 1; cur < len(l.words) { + l.cur = cur + } else { + l.cur = len(l.words) - 1 + } +} + +func (l *lexer) peek(idx int) string { + if idx < len(l.words) { + return l.words[idx] + } + return "" +} + +func (l *lexer) peekNext() (w string) { + return l.peek(l.cur + 1) +} + +func (l *lexer) peekPrev() (w string) { + if l.cur > 0 { + cur := l.cur - 1 + w = l.words[cur] + } + + return w +} + +var posWords = map[int]string{ + 0: "", + 1: "first", + 2: "second", + 3: "third", + 4: "forth", + 5: "five", + 6: "sixth", + 7: "seventh", + 8: "eighth", + 9: "ninth", +} + +func genParamKey(argIdx int) string { + return "param" + posWords[argIdx] // paramfirst, paramsecond... +} + +type parser struct { + lexer *lexer + fn reflect.Method +} + +func parse(fn reflect.Method) (method, path string, err error) { + p := &parser{ + fn: fn, + lexer: newLexer(fn.Name), + } + return p.parse() +} + +func methodTitle(httpMethod string) string { + httpMethodFuncName := strings.Title(strings.ToLower(httpMethod)) + return httpMethodFuncName +} + +var errSkip = errors.New("skip") + +func (p *parser) parse() (method, path string, err error) { + funcArgPos := 0 + path = "/" + // take the first word and check for the method. + w := p.lexer.next() + + for _, httpMethod := range router.AllMethods { + possibleMethodFuncName := methodTitle(httpMethod) + if strings.Index(w, possibleMethodFuncName) == 0 { + method = httpMethod + break + } + } + + if method == "" { + // this is not a valid method to parse, we just skip it, + // it may be used for end-dev's use cases. + return "", "", errSkip + } + + for { + w := p.lexer.next() + if w == "" { + break + } + + if w == tokenBy { + funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver. + + // No need for these: + // ByBy will act like /{param:type}/{param:type} as users expected + // if func input arguments are there, else act By like normal path /by. + // + // if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path + // a.relPath += "/" + strings.ToLower(w) + // continue + // } + + if path, err = p.parsePathParam(path, w, funcArgPos); err != nil { + return "", "", err + } + + continue + } + + // static path. + path += "/" + strings.ToLower(w) + } + + return +} + +func (p *parser) parsePathParam(path string, w string, funcArgPos int) (string, error) { + typ := p.fn.Type + + if typ.NumIn() <= funcArgPos { + + // By found but input arguments are not there, so act like /by path without restricts. + path += "/" + strings.ToLower(w) + return path, nil + } + + var ( + paramKey = genParamKey(funcArgPos) // paramfirst, paramsecond... + paramType = ast.ParamTypeString // default string + ) + + // string, int... + goType := typ.In(funcArgPos).Name() + nextWord := p.lexer.peekNext() + + if nextWord == tokenWildcard { + p.lexer.skip() // skip the Wildcard word. + paramType = ast.ParamTypePath + } else if pType := ast.LookupParamTypeFromStd(goType); pType != ast.ParamTypeUnExpected { + // it's not wildcard, so check base on our available macro types. + paramType = pType + } else { + return "", errors.New("invalid syntax for " + p.fn.Name) + } + + // /{paramfirst:path}, /{paramfirst:long}... + path += fmt.Sprintf("/{%s:%s}", paramKey, paramType.String()) + + if nextWord == "" && typ.NumIn() > funcArgPos+1 { + // By is the latest word but func is expected + // more path parameters values, i.e: + // GetBy(name string, age int) + // The caller (parse) doesn't need to know + // about the incremental funcArgPos because + // it will not need it. + return p.parsePathParam(path, nextWord, funcArgPos+1) + } + + return path, nil +} diff --git a/mvc2/controller_handler_test.go b/mvc2/controller_handler_test.go index d3e11ea716..3a48f8b4c1 100644 --- a/mvc2/controller_handler_test.go +++ b/mvc2/controller_handler_test.go @@ -1,18 +1,20 @@ package mvc2_test import ( + "fmt" "testing" + "time" "github.com/kataras/iris" "github.com/kataras/iris/httptest" - "github.com/kataras/iris/mvc" + // "github.com/kataras/iris/mvc" // "github.com/kataras/iris/mvc/activator/methodfunc" - //. "github.com/kataras/iris/mvc2" + . "github.com/kataras/iris/mvc2" ) type testController struct { - mvc.C - Service *TestServiceImpl + C + Service TestService reqField string } @@ -26,7 +28,8 @@ func (c *testController) BeginRequest(ctx iris.Context) { c.reqField = ctx.URLParam("reqfield") } -func (c *testController) OnActivate(t *mvc.TController) { +func (c *testController) OnActivate(t *ControllerActivator) { // OnActivate(t *mvc.TController) { + // t.Handle("GET", "/", "Get") t.Handle("GET", "/histatic", "HiStatic") t.Handle("GET", "/hiservice", "HiService") t.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") @@ -51,9 +54,13 @@ func (c *testController) HiParamEmptyInputBy() string { func TestControllerHandler(t *testing.T) { app := iris.New() - app.Controller("/", new(testController), &TestServiceImpl{prefix: "service:"}) + // app.Controller("/", new(testController), &TestServiceImpl{prefix: "service:"}) + m := New() + m.Bind(&TestServiceImpl{prefix: "service:"}).Controller(app, new(testController)) e := httptest.New(t, app, httptest.LogLevel("debug")) + fmt.Printf("\n\n\n") + now := time.Now() // test the index, is not part of the current package's implementation but do it. e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("index") @@ -78,4 +85,6 @@ func TestControllerHandler(t *testing.T) { e.GET("/hiparamempyinput/value").Expect().Status(httptest.StatusOK). Body().Equal("empty in but served with ctx.Params.Get('ps')=value") + endTime := time.Now().Sub(now) + fmt.Printf("end at %dns\n", endTime.Nanoseconds()) } diff --git a/mvc2/handler.go b/mvc2/handler.go index bda3dc05a5..85be1d523c 100644 --- a/mvc2/handler.go +++ b/mvc2/handler.go @@ -35,8 +35,8 @@ var ( // MustMakeHandler calls the `MakeHandler` and returns its first resultthe low-level handler), see its docs. // It panics on error. -func MustMakeHandler(handler interface{}, binders []*InputBinder) context.Handler { - h, err := MakeHandler(handler, binders) +func MustMakeHandler(handler interface{}, binders ...interface{}) context.Handler { + h, err := MakeHandler(handler, binders...) if err != nil { panic(err) } @@ -48,7 +48,7 @@ func MustMakeHandler(handler interface{}, binders []*InputBinder) context.Handle // custom structs, Result(View | Response) and anything that you already know that mvc implementation supports, // and returns a low-level `context/iris.Handler` which can be used anywhere in the Iris Application, // as middleware or as simple route handler or party handler or subdomain handler-router. -func MakeHandler(handler interface{}, binders []*InputBinder) (context.Handler, error) { +func MakeHandler(handler interface{}, binders ...interface{}) (context.Handler, error) { if err := validateHandler(handler); err != nil { golog.Errorf("mvc handler: %v", err) return nil, err @@ -59,79 +59,71 @@ func MakeHandler(handler interface{}, binders []*InputBinder) (context.Handler, return h, nil } - typ := indirectTyp(reflect.TypeOf(handler)) - n := typ.NumIn() - typIn := make([]reflect.Type, n, n) - for i := 0; i < n; i++ { - typIn[i] = typ.In(i) + inputBinders := make([]reflect.Value, len(binders), len(binders)) + + for i := range binders { + inputBinders[i] = reflect.ValueOf(binders[i]) } - m := getBindersForInput(binders, typIn...) + return makeHandler(reflect.ValueOf(handler), inputBinders), nil - /* - // no f. this, it's too complicated and it will be harder to maintain later on: - // the only case that these are not equal is when - // binder returns a slice and input contains one or more inputs. - */ - if len(m) != n { - err := fmt.Errorf("input arguments length(%d) of types(%s) and valid binders length(%d) are not equal", n, typIn, len(m)) - golog.Errorf("mvc handler: %v", err) - return nil, err - } + // typ := indirectTyp(reflect.TypeOf(handler)) + // n := typ.NumIn() + // typIn := make([]reflect.Type, n, n) + // for i := 0; i < n; i++ { + // typIn[i] = typ.In(i) + // } + + // m := getBindersForInput(binders, typIn...) + // if len(m) != n { + // err := fmt.Errorf("input arguments length(%d) of types(%s) and valid binders length(%d) are not equal", n, typIn, len(m)) + // golog.Errorf("mvc handler: %v", err) + // return nil, err + // } + + // return makeHandler(reflect.ValueOf(handler), m), nil +} - hasIn := len(m) > 0 - fn := reflect.ValueOf(handler) +func makeHandler(fn reflect.Value, inputBinders []reflect.Value) context.Handler { + inLen := fn.Type().NumIn() - // if has no input to bind then execute the "handler" using the mvc style - // for any output parameters. - if !hasIn { + if inLen == 0 { return func(ctx context.Context) { methodfunc.DispatchFuncResult(ctx, fn.Call(emptyIn)) - }, nil + } + } + + s := getServicesFor(fn, inputBinders) + if len(s) == 0 { + golog.Errorf("mvc handler: input arguments length(%d) and valid binders length(%d) are not equal", inLen, len(s)) + return nil } + n := fn.Type().NumIn() + // contextIndex := -1 + // if n > 0 { + // if isContext(fn.Type().In(0)) { + // contextIndex = 0 + // } + // } return func(ctx context.Context) { - // we could use other tricks for "in" - // here but let's stick to that which is clearly - // that it doesn't keep any previous state - // and it allocates exactly what we need, - // so we can set via index instead of append. - // The other method we could use is to - // declare the in on the build state (before the return) - // and use in[0:0] with append later on. + ctxValue := []reflect.Value{reflect.ValueOf(ctx)} + in := make([]reflect.Value, n, n) - ctxValues := []reflect.Value{reflect.ValueOf(ctx)} - for k, v := range m { - in[k] = v.BindFunc(ctxValues) - /* - // no f. this, it's too complicated and it will be harder to maintain later on: - // now an additional check if it's array and has more inputs of the same type - // and all these results to the expected inputs. - // n-1: if has more to set. - result := v.BindFunc(ctxValues) - if isSliceAndExpectedItem(result.Type(), in, k) { - // if kind := result.Kind(); (kind == reflect.Slice || kind == reflect.Array) && n-1 > k { - prev := 0 - for j, nn := 1, result.Len(); j < nn; j++ { - item := result.Slice(prev, j) - prev++ - // remember; we already set the inputs type, so we know - // what the function expected to have. - if !equalTypes(item.Type(), in[k+1].Type()) { - break - } - - in[k+1] = item - } - } else { - in[k] = result - } - */ - - if ctx.IsStopped() { - return - } - } + // if contextIndex >= 0 { + // in[contextIndex] = ctxValue[0] + // } + // ctxValues := []reflect.Value{reflect.ValueOf(ctx)} + // for k, v := range m { + // in[k] = v.BindFunc(ctxValues) + // if ctx.IsStopped() { + // return + // } + // } + // methodfunc.DispatchFuncResult(ctx, fn.Call(in)) + + s.FillFuncInput(ctxValue, &in) + methodfunc.DispatchFuncResult(ctx, fn.Call(in)) - }, nil + } } diff --git a/mvc2/handler_test.go b/mvc2/handler_test.go index bc8def58fd..358a38c1d5 100644 --- a/mvc2/handler_test.go +++ b/mvc2/handler_test.go @@ -69,19 +69,19 @@ var ( ) func TestMakeHandler(t *testing.T) { - binders := []*InputBinder{ - // #1 - MustMakeFuncInputBinder(testBinderFuncUserStruct), - // #2 - MustMakeServiceInputBinder(testBinderService), - // #3 - MustMakeFuncInputBinder(testBinderFuncParam), - } + // binders := []*InputBinder{ + // // #1 + // MustMakeFuncInputBinder(testBinderFuncUserStruct), + // // #2 + // MustMakeServiceInputBinder(testBinderService), + // // #3 + // MustMakeFuncInputBinder(testBinderFuncParam), + // } var ( - h1 = MustMakeHandler(testConsumeUserHandler, binders) - h2 = MustMakeHandler(testConsumeServiceHandler, binders) - h3 = MustMakeHandler(testConsumeParamHandler, binders) + h1 = MustMakeHandler(testConsumeUserHandler, testBinderFuncUserStruct) + h2 = MustMakeHandler(testConsumeServiceHandler, testBinderService) + h3 = MustMakeHandler(testConsumeParamHandler, testBinderFuncParam) ) testAppWithMvcHandlers(t, h1, h2, h3) diff --git a/mvc2/mvc.go b/mvc2/mvc.go deleted file mode 100644 index e02bc43db7..0000000000 --- a/mvc2/mvc.go +++ /dev/null @@ -1,68 +0,0 @@ -package mvc2 - -import ( - "errors" - - "github.com/kataras/iris/context" -) - -var ( - errNil = errors.New("nil") - errBad = errors.New("bad") - errAlreadyExists = errors.New("already exists") -) - -type Mvc struct { - binders []*InputBinder -} - -func New() *Mvc { - return new(Mvc) -} - -func (m *Mvc) Child() *Mvc { - child := New() - - // copy the current parent's ctx func binders and services to this new child. - if len(m.binders) > 0 { - binders := make([]*InputBinder, len(m.binders), len(m.binders)) - for i, v := range m.binders { - binders[i] = v - } - child.binders = binders - } - - return child -} - -func (m *Mvc) In(binders ...interface{}) *Mvc { - for _, binder := range binders { - typ := resolveBinderType(binder) - - var ( - b *InputBinder - err error - ) - - if typ == functionType { - b, err = MakeFuncInputBinder(binder) - } else if typ == serviceType { - b, err = MakeServiceInputBinder(binder) - } else { - err = errBad - } - - if err != nil { - continue - } - - m.binders = append(m.binders, b) - } - - return m -} - -func (m *Mvc) Handler(handler interface{}) context.Handler { - h, _ := MakeHandler(handler, m.binders) // it logs errors already, so on any error the "h" will be nil. - return h -} diff --git a/mvc2/mvc_test.go b/mvc2/mvc_test.go index 4bcbfe062b..e33f11b0f2 100644 --- a/mvc2/mvc_test.go +++ b/mvc2/mvc_test.go @@ -9,7 +9,7 @@ import ( ) func TestMvcInAndHandler(t *testing.T) { - m := New().In(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) + m := New().Bind(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) var ( h1 = m.Handler(testConsumeUserHandler) diff --git a/mvc2/path_param.go b/mvc2/path_param.go deleted file mode 100644 index ea0da3f735..0000000000 --- a/mvc2/path_param.go +++ /dev/null @@ -1,44 +0,0 @@ -package mvc2 - -import ( - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/memstore" -) - -// PathParams is the context's named path parameters, see `PathParamsBinder` too. -type PathParams = context.RequestParams - -// PathParamsBinder is the binder which will bind the `PathParams` type value to the specific -// handler's input argument, see `PathParams` as well. -func PathParamsBinder(ctx context.Context) PathParams { - return *ctx.Params() -} - -// PathParam describes a named path parameter, it's the result of the PathParamBinder and the expected -// handler func's input argument's type, see `PathParamBinder` too. -type PathParam struct { - memstore.Entry - Empty bool -} - -// PathParamBinder is the binder which binds a handler func's input argument to a named path parameter -// based on its name, see `PathParam` as well. -func PathParamBinder(name string) func(ctx context.Context) PathParam { - return func(ctx context.Context) PathParam { - e, found := ctx.Params().GetEntry(name) - if !found { - - // useless check here but it doesn't hurt, - // useful only when white-box tests run. - if ctx.Application() != nil { - ctx.Application().Logger().Warnf(ctx.HandlerName()+": expected parameter name '%s' to be described in the route's path in order to be received by the `ParamBinder`, please fix it.\n The main handler will not be executed for your own protection.", name) - } - - ctx.StopExecution() - return PathParam{ - Empty: true, - } - } - return PathParam{e, false} - } -} diff --git a/mvc2/path_param_test.go b/mvc2/path_param_test.go deleted file mode 100644 index c10e47c5d2..0000000000 --- a/mvc2/path_param_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package mvc2 - -import ( - "testing" - - "github.com/kataras/iris/context" -) - -func TestPathParamsBinder(t *testing.T) { - m := New() - m.In(PathParamsBinder) - - got := "" - - h := m.Handler(func(params PathParams) { - got = params.Get("firstname") + params.Get("lastname") - }) - - ctx := context.NewContext(nil) - ctx.Params().Set("firstname", "Gerasimos") - ctx.Params().Set("lastname", "Maropoulos") - h(ctx) - expected := "GerasimosMaropoulos" - if got != expected { - t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got) - } -} -func TestPathParamBinder(t *testing.T) { - m := New() - m.In(PathParamBinder("username")) - - got := "" - executed := false - h := m.Handler(func(username PathParam) { - // this should not be fired at all if "username" param wasn't found at all. - // although router is responsible for that but the `ParamBinder` makes that check as well because - // the end-developer may put a param as input argument on her/his function but - // on its route's path didn't describe the path parameter, - // the handler fires a warning and stops the execution for the invalid handler to protect the user. - executed = true - got = username.String() - }) - - expectedUsername := "kataras" - ctx := context.NewContext(nil) - ctx.Params().Set("username", expectedUsername) - h(ctx) - - if got != expectedUsername { - t.Fatalf("expected the param 'username' to be '%s' but got '%s'", expectedUsername, got) - } - - // test the non executed if param not found. - executed = false - got = "" - - ctx2 := context.NewContext(nil) - h(ctx2) - - if got != "" { - t.Fatalf("expected the param 'username' to be entirely empty but got '%s'", got) - } - if executed { - t.Fatalf("expected the handler to not be executed") - } -} diff --git a/mvc2/reflect.go b/mvc2/reflect.go index 931aa4b8e5..69f533fbf4 100644 --- a/mvc2/reflect.go +++ b/mvc2/reflect.go @@ -58,3 +58,50 @@ func equalTypes(got reflect.Type, expected reflect.Type) bool { } return false } + +// for controller only. + +func structFieldIgnored(f reflect.StructField) bool { + if !f.Anonymous { + return true // if not anonymous(embedded), ignore it. + } + + s := f.Tag.Get("ignore") + return s == "true" // if has an ignore tag then ignore it. +} + +type field struct { + Type reflect.Type + Index []int // the index of the field, slice if it's part of a embedded struct + Name string // the actual name + + // this could be empty, but in our cases it's not, + // it's filled with the service and it's filled from the lookupFields' caller. + AnyValue reflect.Value +} + +func lookupFields(typ reflect.Type, parentIndex int) (fields []field) { + for i, n := 0, typ.NumField(); i < n; i++ { + f := typ.Field(i) + + if f.Type.Kind() == reflect.Struct && !structFieldIgnored(f) { + fields = append(fields, lookupFields(f.Type, i)...) + continue + } + + index := []int{i} + if parentIndex >= 0 { + index = append([]int{parentIndex}, index...) + } + + field := field{ + Type: f.Type, + Name: f.Name, + Index: index, + } + + fields = append(fields, field) + } + + return +} diff --git a/mvc2/service.go b/mvc2/service.go index f3155b8fed..03ab07e54f 100644 --- a/mvc2/service.go +++ b/mvc2/service.go @@ -1,92 +1,206 @@ package mvc2 import ( + "fmt" "reflect" ) -// // Service is a `reflect.Value` value. -// // We keep that type here, -// // if we ever need to change this type we will not have -// // to refactor the whole mvc's codebase. -// type Service struct { -// reflect.Value -// typ reflect.Type -// } - -// // Valid checks if the service's Value's Value is valid for set or get. -// func (s Service) Valid() bool { -// return goodVal(s.Value) -// } - -// // Equal returns if the -// func (s Service) Equal(other Service) bool { -// return equalTypes(s.typ, other.typ) -// } - -// func (s Service) String() string { -// return s.Type().String() -// } - -// func wrapService(service interface{}) Service { -// if s, ok := service.(Service); ok { -// return s // if it's a Service already. -// } -// return Service{ -// Value: reflect.ValueOf(service), -// typ: reflect.TypeOf(service), -// } -// } - -// // WrapServices wrap a generic services into structured Service slice. -// func WrapServices(services ...interface{}) []Service { -// if l := len(services); l > 0 { -// out := make([]Service, l, l) -// for i, s := range services { -// out[i] = wrapService(s) -// } -// return out -// } -// return nil -// } - -// MustMakeServiceInputBinder calls the `MakeServiceInputBinder` and returns its first result, see its docs. -// It panics on error. -func MustMakeServiceInputBinder(service interface{}) *InputBinder { - s, err := MakeServiceInputBinder(service) - if err != nil { - panic(err) +type service struct { + Type reflect.Type + Value reflect.Value + StructFieldIndex []int + + // for func input. + ReturnValue func(ctx []reflect.Value) reflect.Value + FuncInputIndex int + FuncInputContextIndex int +} + +type services []*service + +func (serv *services) AddSource(dest reflect.Value, source ...reflect.Value) { + fmt.Println("--------------AddSource------------") + if len(source) == 0 { + return + } + + typ := indirectTyp(dest.Type()) //indirectTyp(reflect.TypeOf(dest)) + _serv := *serv + + if typ.Kind() == reflect.Func { + n := typ.NumIn() + for i := 0; i < n; i++ { + + inTyp := typ.In(i) + if isContext(inTyp) { + _serv = append(_serv, &service{FuncInputContextIndex: i}) + continue + } + + for _, s := range source { + gotTyp := s.Type() + + service := service{ + Type: gotTyp, + Value: s, + FuncInputIndex: i, + FuncInputContextIndex: -1, + } + + if s.Type().Kind() == reflect.Func { + fmt.Printf("Source is Func\n") + returnValue, outType, err := makeReturnValue(s) + if err != nil { + fmt.Printf("Err on makeReturnValue: %v\n", err) + continue + } + gotTyp = outType + service.ReturnValue = returnValue + } + + fmt.Printf("Types: In=%s vs Got=%s\n", inTyp.String(), gotTyp.String()) + if equalTypes(gotTyp, inTyp) { + service.Type = gotTyp + fmt.Printf("Bind In=%s->%s for func\n", inTyp.String(), gotTyp.String()) + _serv = append(_serv, &service) + + break + } + } + } + fmt.Printf("[1] Bind %d for %s\n", len(_serv), typ.String()) + *serv = _serv + + return + } + + if typ.Kind() == reflect.Struct { + fields := lookupFields(typ, -1) + for _, f := range fields { + for _, s := range source { + gotTyp := s.Type() + + service := service{ + Type: gotTyp, + Value: s, + StructFieldIndex: f.Index, + FuncInputContextIndex: -1, + } + + if s.Type().Kind() == reflect.Func { + returnValue, outType, err := makeReturnValue(s) + if err != nil { + continue + } + gotTyp = outType + service.ReturnValue = returnValue + } + + if equalTypes(gotTyp, f.Type) { + service.Type = gotTyp + _serv = append(_serv, &service) + fmt.Printf("[2] Bind In=%s->%s for struct field[%d]\n", f.Type, gotTyp.String(), f.Index) + break + } + } + } + fmt.Printf("[2] Bind %d for %s\n", len(_serv), typ.String()) + *serv = _serv + + return } - return s } -// MakeServiceInputBinder uses a difference/or strange approach, -// we make the services as bind functions -// in order to keep the rest of the code simpler, however we have -// a performance penalty when calling the function instead -// of just put the responsible service to the certain handler's input argument. -func MakeServiceInputBinder(service interface{}) (*InputBinder, error) { - if service == nil { - return nil, errNil +func (serv services) FillStructStaticValues(elem reflect.Value) { + if len(serv) == 0 { + return } - var ( - val = reflect.ValueOf(service) - typ = val.Type() - ) + for _, s := range serv { + if len(s.StructFieldIndex) > 0 { + // fmt.Printf("FillStructStaticValues for index: %d\n", s.StructFieldIndex) + elem.FieldByIndex(s.StructFieldIndex).Set(s.Value) + } + } +} - if !goodVal(val) { - return nil, errBad +func (serv services) FillStructDynamicValues(elem reflect.Value, ctx []reflect.Value) { + if len(serv) == 0 { + return } - if indirectTyp(typ).Kind() != reflect.Struct { - // if the pointer's struct is not a struct then return err bad. - return nil, errBad + for _, s := range serv { + if len(s.StructFieldIndex) > 0 { + elem.FieldByIndex(s.StructFieldIndex).Set(s.ReturnValue(ctx)) + } } +} - return &InputBinder{ - BindType: typ, - BindFunc: func(_ []reflect.Value) reflect.Value { - return val - }, - }, nil +func (serv services) FillFuncInput(ctx []reflect.Value, destIn *[]reflect.Value) { + if len(serv) == 0 { + return + } + + in := *destIn + for _, s := range serv { + if s.ReturnValue != nil { + in[s.FuncInputIndex] = s.ReturnValue(ctx) + continue + } + + in[s.FuncInputIndex] = s.Value + if s.FuncInputContextIndex >= 0 { + in[s.FuncInputContextIndex] = ctx[0] + } + } + + *destIn = in +} + +func makeReturnValue(fn reflect.Value) (func([]reflect.Value) reflect.Value, reflect.Type, error) { + typ := indirectTyp(fn.Type()) + + // invalid if not a func. + if typ.Kind() != reflect.Func { + return nil, typ, errBad + } + + // invalid if not returns one single value. + if typ.NumOut() != 1 { + return nil, typ, errBad + } + + // invalid if input args length is not one. + if typ.NumIn() != 1 { + return nil, typ, errBad + } + + // invalid if that single input arg is not a typeof context.Context. + if !isContext(typ.In(0)) { + return nil, typ, errBad + } + + outTyp := typ.Out(0) + zeroOutVal := reflect.New(outTyp).Elem() + + bf := func(ctxValue []reflect.Value) reflect.Value { + // []reflect.Value{reflect.ValueOf(ctx)} + results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler. + if len(results) == 0 { + return zeroOutVal + } + + v := results[0] + if !v.IsValid() { + return zeroOutVal + } + return v + } + + return bf, outTyp, nil +} + +func getServicesFor(dest reflect.Value, source []reflect.Value) (s services) { + s.AddSource(dest, source...) + return s } diff --git a/mvc2/service_test.go b/mvc2/service_test.go deleted file mode 100644 index 88779dce8d..0000000000 --- a/mvc2/service_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package mvc2 - -import ( - "reflect" - "testing" -) - -type ( - testService interface { - say(string) - } - testServiceImpl struct { - prefix string - } -) - -func (s *testServiceImpl) say(message string) string { - return s.prefix + ": " + message -} - -func TestMakeServiceInputBinder(t *testing.T) { - expectedService := &testServiceImpl{"say"} - b := MustMakeServiceInputBinder(expectedService) - // in - var ( - intType = reflect.TypeOf(1) - availableBinders = []*InputBinder{b} - ) - - // 1 - testCheck(t, "test1", true, testGetBindersForInput(t, availableBinders, - []interface{}{expectedService}, reflect.TypeOf(expectedService))) - // 2 - testCheck(t, "test2-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{42})) - // 3 - testCheck(t, "test3-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{42}, intType)) - // 4 - testCheck(t, "test4-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{42})) - // 5 - check if nothing passed, so no valid binders at all. - testCheck(t, "test5", true, testGetBindersForInput(t, availableBinders, - []interface{}{})) - -} From f19bc49865307b98bc8575254b098b18c94b90c5 Mon Sep 17 00:00:00 2001 From: kataras Date: Mon, 4 Dec 2017 05:08:05 +0200 Subject: [PATCH 10/79] cm --- mvc2/binder/binder/input.go | 4 + mvc2/binder/binding.go | 19 ++ mvc2/binder/func_input.go | 1 + mvc2/binder/func_result.go | 53 ++++ mvc2/binder/reflect.go | 107 ++++++++ mvc2/binder/to_struct.go | 50 ++++ mvc2/binder_in.go | 173 ++++++++++++ mvc2/binder_in_path_param.go | 135 ++++++++++ mvc2/binder_in_path_param_test.go | 64 +++++ mvc2/binder_in_service.go | 81 ++++++ mvc2/binder_in_service_test.go | 46 ++++ mvc2/binder_in_test.go | 143 ++++++++++ mvc2/engine.go | 103 ++++++++ mvc2/handler_out.go | 422 ++++++++++++++++++++++++++++++ mvc2/handler_out_test.go | 271 +++++++++++++++++++ mvc2/session_controller.go | 47 ++++ 16 files changed, 1719 insertions(+) create mode 100644 mvc2/binder/binder/input.go create mode 100644 mvc2/binder/binding.go create mode 100644 mvc2/binder/func_input.go create mode 100644 mvc2/binder/func_result.go create mode 100644 mvc2/binder/reflect.go create mode 100644 mvc2/binder/to_struct.go create mode 100644 mvc2/binder_in.go create mode 100644 mvc2/binder_in_path_param.go create mode 100644 mvc2/binder_in_path_param_test.go create mode 100644 mvc2/binder_in_service.go create mode 100644 mvc2/binder_in_service_test.go create mode 100644 mvc2/binder_in_test.go create mode 100644 mvc2/engine.go create mode 100644 mvc2/handler_out.go create mode 100644 mvc2/handler_out_test.go create mode 100644 mvc2/session_controller.go diff --git a/mvc2/binder/binder/input.go b/mvc2/binder/binder/input.go new file mode 100644 index 0000000000..1e583530ef --- /dev/null +++ b/mvc2/binder/binder/input.go @@ -0,0 +1,4 @@ +package binder + +type Input interface { +} diff --git a/mvc2/binder/binding.go b/mvc2/binder/binding.go new file mode 100644 index 0000000000..9fe0eed383 --- /dev/null +++ b/mvc2/binder/binding.go @@ -0,0 +1,19 @@ +package binder + +import ( + "reflect" +) + +type Binding interface { + AddSource(v reflect.Value, source ...reflect.Value) +} + +type StructValue struct { + Type reflect.Type + Value reflect.Value +} + +type FuncResultValue struct { + Type reflect.Type + ReturnValue func(ctx []reflect.Value) reflect.Value +} diff --git a/mvc2/binder/func_input.go b/mvc2/binder/func_input.go new file mode 100644 index 0000000000..0587a0cced --- /dev/null +++ b/mvc2/binder/func_input.go @@ -0,0 +1 @@ +package binder diff --git a/mvc2/binder/func_result.go b/mvc2/binder/func_result.go new file mode 100644 index 0000000000..cb8ba5fe17 --- /dev/null +++ b/mvc2/binder/func_result.go @@ -0,0 +1,53 @@ +package binder + +import ( + "errors" + "reflect" +) + +var ( + errBad = errors.New("bad") +) + +func makeReturnValue(fn reflect.Value) (func([]reflect.Value) reflect.Value, reflect.Type, error) { + typ := indirectTyp(fn.Type()) + + // invalid if not a func. + if typ.Kind() != reflect.Func { + return nil, typ, errBad + } + + // invalid if not returns one single value. + if typ.NumOut() != 1 { + return nil, typ, errBad + } + + // invalid if input args length is not one. + if typ.NumIn() != 1 { + return nil, typ, errBad + } + + // invalid if that single input arg is not a typeof context.Context. + if !isContext(typ.In(0)) { + return nil, typ, errBad + } + + outTyp := typ.Out(0) + zeroOutVal := reflect.New(outTyp).Elem() + + bf := func(ctxValue []reflect.Value) reflect.Value { + // []reflect.Value{reflect.ValueOf(ctx)} + results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler. + if len(results) == 0 { + return zeroOutVal + } + + v := results[0] + if !v.IsValid() { + return zeroOutVal + } + return v + } + + return bf, outTyp, nil +} diff --git a/mvc2/binder/reflect.go b/mvc2/binder/reflect.go new file mode 100644 index 0000000000..20c75b9fb3 --- /dev/null +++ b/mvc2/binder/reflect.go @@ -0,0 +1,107 @@ +package binder + +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 +} + +/* +// no f. this, it's too complicated and it will be harder to maintain later on: +func isSliceAndExpectedItem(got reflect.Type, in []reflect.Type, currentBindersIdx int) bool { + kind := got.Kind() + // if got result is slice or array. + return (kind == reflect.Slice || kind == reflect.Array) && + // if has expected next input. + len(in)-1 > currentBindersIdx && + // if the current input's type is not the same as got (if it's not a slice of that types or anything else). + equalTypes(got, in[currentBindersIdx]) +} +*/ + +func equalTypes(got reflect.Type, expected reflect.Type) bool { + if got == expected { + return true + } + // if accepts an interface, check if the given "got" type does + // implement this "expected" user handler's input argument. + if expected.Kind() == reflect.Interface { + // fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String()) + return got.Implements(expected) + } + return false +} + +// for controller only. + +func structFieldIgnored(f reflect.StructField) bool { + if !f.Anonymous { + return true // if not anonymous(embedded), ignore it. + } + + s := f.Tag.Get("ignore") + return s == "true" // if has an ignore tag then ignore it. +} + +type field struct { + Type reflect.Type + Index []int // the index of the field, slice if it's part of a embedded struct + Name string // the actual name + + // this could be empty, but in our cases it's not, + // it's filled with the service and it's filled from the lookupFields' caller. + AnyValue reflect.Value +} + +func lookupFields(typ reflect.Type, parentIndex int) (fields []field) { + for i, n := 0, typ.NumField(); i < n; i++ { + f := typ.Field(i) + + if f.Type.Kind() == reflect.Struct && !structFieldIgnored(f) { + fields = append(fields, lookupFields(f.Type, i)...) + continue + } + + index := []int{i} + if parentIndex >= 0 { + index = append([]int{parentIndex}, index...) + } + + field := field{ + Type: f.Type, + Name: f.Name, + Index: index, + } + + fields = append(fields, field) + } + + return +} diff --git a/mvc2/binder/to_struct.go b/mvc2/binder/to_struct.go new file mode 100644 index 0000000000..cb85d0d6a3 --- /dev/null +++ b/mvc2/binder/to_struct.go @@ -0,0 +1,50 @@ +package binder + +import ( + "reflect" +) + +type StructBinding struct { + Field StructValue + Func FuncResultValue +} + +func (b *StructBinding) AddSource(dest reflect.Value, source ...reflect.Value) { + typ := indirectTyp(dest.Type()) //indirectTyp(reflect.TypeOf(dest)) + if typ.Kind() != reflect.Struct { + return + } + + fields := lookupFields(typ, -1) + for _, f := range fields { + for _, s := range source { + if s.Type().Kind() == reflect.Func { + returnValue, outType, err := makeReturnValue(s) + if err != nil { + continue + } + gotTyp = outType + service.ReturnValue = returnValue + } + + gotTyp := s.Type() + + v := StructValue{ + Type: gotTyp, + Value: s, + FieldIndex: f.Index, + } + + if equalTypes(gotTyp, f.Type) { + service.Type = gotTyp + _serv = append(_serv, &service) + fmt.Printf("[2] Bind In=%s->%s for struct field[%d]\n", f.Type, gotTyp.String(), f.Index) + break + } + } + } + fmt.Printf("[2] Bind %d for %s\n", len(_serv), typ.String()) + *serv = _serv + + return +} diff --git a/mvc2/binder_in.go b/mvc2/binder_in.go new file mode 100644 index 0000000000..acc8dee880 --- /dev/null +++ b/mvc2/binder_in.go @@ -0,0 +1,173 @@ +package mvc2 + +import ( + "reflect" +) + +// 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 { + BinderType binderType + BindType reflect.Type + BindFunc func(ctx []reflect.Value) reflect.Value +} + +// key = the func input argument index, value is the responsible input binder. +type bindersMap map[int]*InputBinder + +// joinBindersMap joins the "m2" to m1 and returns the result, it's the same "m1" map. +// if "m2" is not nil and "m2" is not nil then it loops the "m2"'s keys and sets the values +// to the "m1", if "m2" is not and not empty nil but m1 is nil then "m1" = "m2". +// The result may be nil if the "m1" and "m2" are nil or "m2" is empty and "m1" is nil. +func joinBindersMap(m1, m2 bindersMap) bindersMap { + if m2 != nil && len(m2) > 0 { + if m1 == nil { + m1 = m2 + } else { + for k, v := range m2 { + m1[k] = v + } + } + } + return m1 +} + +// getBindersForInput 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 getBindersForInput(binders []*InputBinder, expected ...reflect.Type) (m bindersMap) { + for idx, in := range expected { + if idx == 0 && isContext(in) { + // if the first is context then set it directly here. + m = make(bindersMap) + m[0] = &InputBinder{ + BindType: contextTyp, + BindFunc: func(ctxValues []reflect.Value) reflect.Value { + return ctxValues[0] + }, + } + continue + } + + for _, b := range binders { + if equalTypes(b.BindType, in) { + if m == nil { + m = make(bindersMap) + } + // 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 +} + +// MustMakeFuncInputBinder calls the `MakeFuncInputBinder` and returns its first result, see its docs. +// It panics on error. +func MustMakeFuncInputBinder(binder interface{}) *InputBinder { + b, err := MakeFuncInputBinder(binder) + if err != nil { + panic(err) + } + return b +} + +type binderType uint32 + +const ( + functionType binderType = iota + serviceType + invalidType +) + +func resolveBinderType(binder interface{}) binderType { + if binder == nil { + return invalidType + } + + return resolveBinderTypeFromKind(reflect.TypeOf(binder).Kind()) +} + +func resolveBinderTypeFromKind(k reflect.Kind) binderType { + switch k { + case reflect.Func: + return functionType + case reflect.Struct, reflect.Interface, reflect.Ptr, reflect.Slice, reflect.Array: + return serviceType + } + + return invalidType +} + +// MakeFuncInputBinder 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`. +// +// 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 MakeFuncInputBinder(binder interface{}) (*InputBinder, error) { + v := reflect.ValueOf(binder) + return makeFuncInputBinder(v) +} + +func makeFuncInputBinder(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(ctxValue []reflect.Value) reflect.Value { + // []reflect.Value{reflect.ValueOf(ctx)} + results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler. + if len(results) == 0 { + return zeroOutVal + } + + v := results[0] + if !v.IsValid() { + return zeroOutVal + } + return v + } + + return &InputBinder{ + BinderType: functionType, + BindType: outTyp, + BindFunc: bf, + }, nil +} diff --git a/mvc2/binder_in_path_param.go b/mvc2/binder_in_path_param.go new file mode 100644 index 0000000000..521965641d --- /dev/null +++ b/mvc2/binder_in_path_param.go @@ -0,0 +1,135 @@ +package mvc2 + +import ( + "fmt" + "reflect" + + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/memstore" + "github.com/kataras/iris/core/router/macro" + "github.com/kataras/iris/core/router/macro/interpreter/ast" +) + +func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { + n := funcTyp.NumIn() + funcIn := make([]reflect.Type, n, n) + for i := 0; i < n; i++ { + funcIn[i] = funcTyp.In(i) + } + return funcIn +} + +func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) (values []reflect.Value) { + if len(funcIn) == 0 || len(params) == 0 { + return + } + + funcInIdx := 0 + // it's a valid param type. + for _, p := range params { + in := funcIn[funcInIdx] + paramType := p.Type + paramName := p.Name + + // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) + if p.Type.Assignable(in.Kind()) { + + // b = append(b, &InputBinder{ + // BindType: in, // or p.Type.Kind, should be the same. + // BindFunc: func(ctx []reflect.Value) reflect.Value { + // // I don't like this ctx[0].Interface(0) + // // it will be slow, and silly because we have ctx already + // // before the bindings at serve-time, so we will create + // // a func for each one of the param types, they are just 4 so + // // it worths some dublications. + // return getParamValueFromType(ctx[0].Interface(), paramType, paramName) + // }, + // }) + + var fn interface{} + + if paramType == ast.ParamTypeInt { + fn = func(ctx context.Context) int { + v, _ := ctx.Params().GetInt(paramName) + return v + } + } else if paramType == ast.ParamTypeLong { + fn = func(ctx context.Context) int64 { + v, _ := ctx.Params().GetInt64(paramName) + return v + } + + } else if paramType == ast.ParamTypeBoolean { + fn = func(ctx context.Context) bool { + v, _ := ctx.Params().GetBool(paramName) + return v + } + + } else { + // string, path... + fn = func(ctx context.Context) string { + return ctx.Params().Get(paramName) + } + } + + fmt.Printf("binder_in_path_param.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) + values = append(values, reflect.ValueOf(fn)) + + // inputBinder, err := MakeFuncInputBinder(fn) + // if err != nil { + // fmt.Printf("err on make func binder: %v\n", err.Error()) + // continue + // } + + // if m == nil { + // m = make(bindersMap, 0) + // } + + // // fmt.Printf("set param input binder for func arg index: %d\n", funcInIdx) + // m[funcInIdx] = inputBinder + } + + funcInIdx++ + } + + return + // return m +} + +// PathParams is the context's named path parameters, see `PathParamsBinder` too. +type PathParams = context.RequestParams + +// PathParamsBinder is the binder which will bind the `PathParams` type value to the specific +// handler's input argument, see `PathParams` as well. +func PathParamsBinder(ctx context.Context) PathParams { + return *ctx.Params() +} + +// PathParam describes a named path parameter, it's the result of the PathParamBinder and the expected +// handler func's input argument's type, see `PathParamBinder` too. +type PathParam struct { + memstore.Entry + Empty bool +} + +// PathParamBinder is the binder which binds a handler func's input argument to a named path parameter +// based on its name, see `PathParam` as well. +func PathParamBinder(name string) func(ctx context.Context) PathParam { + return func(ctx context.Context) PathParam { + e, found := ctx.Params().GetEntry(name) + if !found { + + // useless check here but it doesn't hurt, + // useful only when white-box tests run. + if ctx.Application() != nil { + ctx.Application().Logger().Warnf(ctx.HandlerName()+": expected parameter name '%s' to be described in the route's path in order to be received by the `ParamBinder`, please fix it.\n The main handler will not be executed for your own protection.", name) + } + + ctx.StopExecution() + return PathParam{ + Empty: true, + } + } + return PathParam{e, false} + } +} diff --git a/mvc2/binder_in_path_param_test.go b/mvc2/binder_in_path_param_test.go new file mode 100644 index 0000000000..582a1ee22c --- /dev/null +++ b/mvc2/binder_in_path_param_test.go @@ -0,0 +1,64 @@ +package mvc2 + +import ( + "testing" + + "github.com/kataras/iris/context" +) + +func TestPathParamsBinder(t *testing.T) { + m := New().Bind(PathParamsBinder) + + got := "" + + h := m.Handler(func(params PathParams) { + got = params.Get("firstname") + params.Get("lastname") + }) + + ctx := context.NewContext(nil) + ctx.Params().Set("firstname", "Gerasimos") + ctx.Params().Set("lastname", "Maropoulos") + h(ctx) + expected := "GerasimosMaropoulos" + if got != expected { + t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got) + } +} +func TestPathParamBinder(t *testing.T) { + m := New().Bind(PathParamBinder("username")) + + got := "" + executed := false + h := m.Handler(func(username PathParam) { + // this should not be fired at all if "username" param wasn't found at all. + // although router is responsible for that but the `ParamBinder` makes that check as well because + // the end-developer may put a param as input argument on her/his function but + // on its route's path didn't describe the path parameter, + // the handler fires a warning and stops the execution for the invalid handler to protect the user. + executed = true + got = username.String() + }) + + expectedUsername := "kataras" + ctx := context.NewContext(nil) + ctx.Params().Set("username", expectedUsername) + h(ctx) + + if got != expectedUsername { + t.Fatalf("expected the param 'username' to be '%s' but got '%s'", expectedUsername, got) + } + + // test the non executed if param not found. + executed = false + got = "" + + ctx2 := context.NewContext(nil) + h(ctx2) + + if got != "" { + t.Fatalf("expected the param 'username' to be entirely empty but got '%s'", got) + } + if executed { + t.Fatalf("expected the handler to not be executed") + } +} diff --git a/mvc2/binder_in_service.go b/mvc2/binder_in_service.go new file mode 100644 index 0000000000..db4fde0874 --- /dev/null +++ b/mvc2/binder_in_service.go @@ -0,0 +1,81 @@ +package mvc2 + +import ( + "reflect" +) + +type serviceFieldBinder struct { + Index []int + Binder *InputBinder +} + +func getServicesBinderForStruct(binders []*InputBinder, typ reflect.Type) func(elem reflect.Value) { + fields := lookupFields(typ, -1) + var validBinders []*serviceFieldBinder + + for _, b := range binders { + for _, f := range fields { + if b.BinderType != serviceType { + continue + } + if equalTypes(b.BindType, f.Type) { + validBinders = append(validBinders, + &serviceFieldBinder{Index: f.Index, Binder: b}) + } + } + + } + + if len(validBinders) == 0 { + return func(_ reflect.Value) {} + } + + return func(elem reflect.Value) { + for _, b := range validBinders { + elem.FieldByIndex(b.Index).Set(b.Binder.BindFunc(nil)) + } + } +} + +// MustMakeServiceInputBinder calls the `MakeServiceInputBinder` and returns its first result, see its docs. +// It panics on error. +func MustMakeServiceInputBinder(service interface{}) *InputBinder { + s, err := MakeServiceInputBinder(service) + if err != nil { + panic(err) + } + return s +} + +// MakeServiceInputBinder uses a difference/or strange approach, +// we make the services as bind functions +// in order to keep the rest of the code simpler, however we have +// a performance penalty when calling the function instead +// of just put the responsible service to the certain handler's input argument. +func MakeServiceInputBinder(service interface{}) (*InputBinder, error) { + if service == nil { + return nil, errNil + } + + var ( + val = reflect.ValueOf(service) + typ = val.Type() + ) + + if !goodVal(val) { + return nil, errBad + } + + if indirectTyp(typ).Kind() != reflect.Struct { + // if the pointer's struct is not a struct then return err bad. + return nil, errBad + } + + return &InputBinder{ + BinderType: serviceType, + BindType: typ, + BindFunc: func(_ []reflect.Value) reflect.Value { + return val + }, + }, nil +} diff --git a/mvc2/binder_in_service_test.go b/mvc2/binder_in_service_test.go new file mode 100644 index 0000000000..88779dce8d --- /dev/null +++ b/mvc2/binder_in_service_test.go @@ -0,0 +1,46 @@ +package mvc2 + +import ( + "reflect" + "testing" +) + +type ( + testService interface { + say(string) + } + testServiceImpl struct { + prefix string + } +) + +func (s *testServiceImpl) say(message string) string { + return s.prefix + ": " + message +} + +func TestMakeServiceInputBinder(t *testing.T) { + expectedService := &testServiceImpl{"say"} + b := MustMakeServiceInputBinder(expectedService) + // in + var ( + intType = reflect.TypeOf(1) + availableBinders = []*InputBinder{b} + ) + + // 1 + testCheck(t, "test1", true, testGetBindersForInput(t, availableBinders, + []interface{}{expectedService}, reflect.TypeOf(expectedService))) + // 2 + testCheck(t, "test2-fail", false, testGetBindersForInput(t, availableBinders, + []interface{}{42})) + // 3 + testCheck(t, "test3-fail", false, testGetBindersForInput(t, availableBinders, + []interface{}{42}, intType)) + // 4 + testCheck(t, "test4-fail", false, testGetBindersForInput(t, availableBinders, + []interface{}{42})) + // 5 - check if nothing passed, so no valid binders at all. + testCheck(t, "test5", true, testGetBindersForInput(t, availableBinders, + []interface{}{})) + +} diff --git a/mvc2/binder_in_test.go b/mvc2/binder_in_test.go new file mode 100644 index 0000000000..099a3578f7 --- /dev/null +++ b/mvc2/binder_in_test.go @@ -0,0 +1,143 @@ +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, + } +} + +func TestMakeFuncInputBinder(t *testing.T) { + testMakeFuncInputBinder(t, testBinderFunc) +} + +func testMakeFuncInputBinder(t *testing.T, binder interface{}) { + b, err := MakeFuncInputBinder(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) + ctxValue := []reflect.Value{reflect.ValueOf(ctx)} + v := b.BindFunc(ctxValue) + 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) + } +} + +func testCheck(t *testing.T, 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) + } +} + +// TestGetBindersForInput 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 TestGetBindersForInput(t *testing.T) { + // binders + var ( + stringBinder = MustMakeFuncInputBinder(func(ctx context.Context) string { + return "a string" + }) + intBinder = MustMakeFuncInputBinder(func(ctx context.Context) int { + return 42 + }) + ) + // in + var ( + stringType = reflect.TypeOf("string") + intType = reflect.TypeOf(1) + ) + + // 1 + testCheck(t, "test1", true, testGetBindersForInput(t, []*InputBinder{intBinder, stringBinder}, + []interface{}{"a string", 42}, stringType, intType)) + availableBinders := []*InputBinder{stringBinder, intBinder} // different order than the fist test. + // 2 + testCheck(t, "test2", true, testGetBindersForInput(t, availableBinders, + []interface{}{"a string", 42}, stringType, intType)) + // 3 + testCheck(t, "test-3-fail", false, testGetBindersForInput(t, availableBinders, + []interface{}{42}, stringType, intType)) + // 4 + testCheck(t, "test-4-fail", false, testGetBindersForInput(t, availableBinders, + []interface{}{"a string"}, stringType, intType)) + // 5 + testCheck(t, "test-5-fail", false, testGetBindersForInput(t, availableBinders, + []interface{}{42, 42}, stringType, intType)) + // 6 + testCheck(t, "test-6-fail", false, testGetBindersForInput(t, availableBinders, + []interface{}{testUserStruct{}}, stringType, intType)) + +} + +func testGetBindersForInput(t *testing.T, binders []*InputBinder, expectingResults []interface{}, in ...reflect.Type) (errString string) { + m := getBindersForInput(binders, in...) + + if expected, got := len(expectingResults), len(m); expected != got { + return fmt.Sprintf("expected results length(%d) and valid binders length(%d) to be equal, so each input has one binder", expected, got) + } + + ctxValue := []reflect.Value{reflect.ValueOf(context.NewContext(nil))} + for idx, expected := range expectingResults { + if m[idx] != nil { + v := m[idx].BindFunc(ctxValue) + 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/engine.go b/mvc2/engine.go new file mode 100644 index 0000000000..f40816e010 --- /dev/null +++ b/mvc2/engine.go @@ -0,0 +1,103 @@ +package mvc2 + +import ( + "errors" + "reflect" + + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router" +) + +var ( + errNil = errors.New("nil") + errBad = errors.New("bad") + errAlreadyExists = errors.New("already exists") +) + +type Engine struct { + binders []*InputBinder + + Input []reflect.Value +} + +func New() *Engine { + return new(Engine) +} + +func (e *Engine) Child() *Engine { + child := New() + + // copy the current parent's ctx func binders and services to this new child. + // if l := len(e.binders); l > 0 { + // binders := make([]*InputBinder, l, l) + // copy(binders, e.binders) + // child.binders = binders + // } + if l := len(e.Input); l > 0 { + input := make([]reflect.Value, l, l) + copy(input, e.Input) + child.Input = input + } + return child +} + +func (e *Engine) Bind(binders ...interface{}) *Engine { + for _, binder := range binders { + // typ := resolveBinderType(binder) + + // var ( + // b *InputBinder + // err error + // ) + + // if typ == functionType { + // b, err = MakeFuncInputBinder(binder) + // } else if typ == serviceType { + // b, err = MakeServiceInputBinder(binder) + // } else { + // err = errBad + // } + + // if err != nil { + // continue + // } + + // e.binders = append(e.binders, b) + + e.Input = append(e.Input, reflect.ValueOf(binder)) + } + + return e +} + +// BindTypeExists returns true if a binder responsible to +// bind and return a type of "typ" is already registered. +func (e *Engine) BindTypeExists(typ reflect.Type) bool { + // for _, b := range e.binders { + // if equalTypes(b.BindType, typ) { + // return true + // } + // } + for _, in := range e.Input { + if equalTypes(in.Type(), typ) { + return true + } + } + return false +} + +func (e *Engine) Handler(handler interface{}) context.Handler { + h, _ := MakeHandler(handler, e.binders) // it logs errors already, so on any error the "h" will be nil. + return h +} + +type ActivateListener interface { + OnActivate(*ControllerActivator) +} + +func (e *Engine) Controller(router router.Party, controller BaseController) { + ca := newControllerActivator(e, router, controller) + if al, ok := controller.(ActivateListener); ok { + al.OnActivate(ca) + } +} diff --git a/mvc2/handler_out.go b/mvc2/handler_out.go new file mode 100644 index 0000000000..eb7999ad4d --- /dev/null +++ b/mvc2/handler_out.go @@ -0,0 +1,422 @@ +package mvc2 + +import ( + "reflect" + "strings" + + "github.com/fatih/structs" + "github.com/kataras/iris/context" +) + +// Result is a response dispatcher. +// All types that complete this interface +// can be returned as values from the method functions. +// +// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview. +type Result interface { + // Dispatch should sends the response to the context's response writer. + Dispatch(ctx context.Context) +} + +var defaultFailureResponse = Response{Code: DefaultErrStatusCode} + +// Try will check if "fn" ran without any panics, +// using recovery, +// and return its result as the final response +// otherwise it returns the "failure" response if any, +// if not then a 400 bad request is being sent. +// +// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go. +func Try(fn func() Result, failure ...Result) Result { + var failed bool + var actionResponse Result + + func() { + defer func() { + if rec := recover(); rec != nil { + failed = true + } + }() + actionResponse = fn() + }() + + if failed { + if len(failure) > 0 { + return failure[0] + } + return defaultFailureResponse + } + + return actionResponse +} + +const slashB byte = '/' + +type compatibleErr interface { + Error() string +} + +// DefaultErrStatusCode is the default error status code (400) +// when the response contains an error which is not nil. +var DefaultErrStatusCode = 400 + +// DispatchErr writes the error to the response. +func DispatchErr(ctx context.Context, status int, err error) { + if status < 400 { + status = DefaultErrStatusCode + } + ctx.StatusCode(status) + if text := err.Error(); text != "" { + ctx.WriteString(text) + ctx.StopExecution() + } +} + +// DispatchCommon is being used internally to send +// commonly used data to the response writer with a smart way. +func DispatchCommon(ctx context.Context, + statusCode int, contentType string, content []byte, v interface{}, err error, found bool) { + + // if we have a false boolean as a return value + // then skip everything and fire a not found, + // we even don't care about the given status code or the object or the content. + if !found { + ctx.NotFound() + return + } + + status := statusCode + if status == 0 { + status = 200 + } + + if err != nil { + DispatchErr(ctx, status, err) + return + } + + // write the status code, the rest will need that before any write ofc. + ctx.StatusCode(status) + if contentType == "" { + // to respect any ctx.ContentType(...) call + // especially if v is not nil. + contentType = ctx.GetContentType() + } + + if v != nil { + if d, ok := v.(Result); ok { + // write the content type now (internal check for empty value) + ctx.ContentType(contentType) + d.Dispatch(ctx) + return + } + + if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) { + _, err = ctx.JSONP(v) + } else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) { + _, err = ctx.XML(v, context.XML{Indent: " "}) + } else { + // defaults to json if content type is missing or its application/json. + _, err = ctx.JSON(v, context.JSON{Indent: " "}) + } + + if err != nil { + DispatchErr(ctx, status, err) + } + + return + } + + ctx.ContentType(contentType) + // .Write even len(content) == 0 , this should be called in order to call the internal tryWriteHeader, + // it will not cost anything. + ctx.Write(content) +} + +// DispatchFuncResult is being used internally to resolve +// and send the method function's output values to the +// context's response writer using a smart way which +// respects status code, content type, content, custom struct +// and an error type. +// Supports for: +// func(c *ExampleController) Get() string | +// (string, string) | +// (string, int) | +// ... +// int | +// (int, string | +// (string, error) | +// ... +// error | +// (int, error) | +// (customStruct, error) | +// ... +// bool | +// (int, bool) | +// (string, bool) | +// (customStruct, bool) | +// ... +// customStruct | +// (customStruct, int) | +// (customStruct, string) | +// Result or (Result, error) and so on... +// +// where Get is an HTTP METHOD. +func DispatchFuncResult(ctx context.Context, values []reflect.Value) { + numOut := len(values) + if numOut == 0 { + return + } + + var ( + // if statusCode > 0 then send this status code. + // Except when err != nil then check if status code is < 400 and + // if it's set it as DefaultErrStatusCode. + // Except when found == false, then the status code is 404. + statusCode int + // if not empty then use that as content type, + // if empty and custom != nil then set it to application/json. + contentType string + // if len > 0 then write that to the response writer as raw bytes, + // except when found == false or err != nil or custom != nil. + content []byte + // if not nil then check + // for content type (or json default) and send the custom data object + // except when found == false or err != nil. + custom interface{} + // if not nil then check for its status code, + // if not status code or < 400 then set it as DefaultErrStatusCode + // and fire the error's text. + err error + // if false then skip everything and fire 404. + found = true // defaults to true of course, otherwise will break :) + ) + + for _, v := range values { + // order of these checks matters + // for example, first we need to check for status code, + // secondly the string (for content type and content)... + if !v.IsValid() { + continue + } + + f := v.Interface() + + if b, ok := f.(bool); ok { + found = b + if !found { + // skip everything, we don't care about other return values, + // this boolean is the higher in order. + break + } + continue + } + + if i, ok := f.(int); ok { + statusCode = i + continue + } + + if s, ok := f.(string); ok { + // a string is content type when it contains a slash and + // content or custom struct is being calculated already; + // (string -> content, string-> content type) + // (customStruct, string -> content type) + if (len(content) > 0 || custom != nil) && strings.IndexByte(s, slashB) > 0 { + contentType = s + } else { + // otherwise is content + content = []byte(s) + } + + continue + } + + if b, ok := f.([]byte); ok { + // it's raw content, get the latest + content = b + continue + } + + if e, ok := f.(compatibleErr); ok { + if e != nil { // it's always not nil but keep it here. + err = e + if statusCode < 400 { + statusCode = DefaultErrStatusCode + } + break // break on first error, error should be in the end but we + // need to know break the dispatcher if any error. + // at the end; we don't want to write anything to the response if error is not nil. + } + continue + } + + // else it's a custom struct or a dispatcher, we'll decide later + // because content type and status code matters + // do that check in order to be able to correctly dispatch: + // (customStruct, error) -> customStruct filled and error is nil + if custom == nil && f != nil { + custom = f + } + + } + + DispatchCommon(ctx, statusCode, contentType, content, custom, err, found) +} + +// Response completes the `methodfunc.Result` interface. +// It's being used as an alternative return value which +// wraps the status code, the content type, a content as bytes or as string +// and an error, it's smart enough to complete the request and send the correct response to the client. +type Response struct { + Code int + ContentType string + Content []byte + + // if not empty then content type is the text/plain + // and content is the text as []byte. + Text string + // If not nil then it will fire that as "application/json" or the + // "ContentType" if not empty. + Object interface{} + + // If Path is not empty then it will redirect + // the client to this Path, if Code is >= 300 and < 400 + // then it will use that Code to do the redirection, otherwise + // StatusFound(302) or StatusSeeOther(303) for post methods will be used. + // Except when err != nil. + Path string + + // if not empty then fire a 400 bad request error + // unless the Status is > 200, then fire that error code + // with the Err.Error() string as its content. + // + // if Err.Error() is empty then it fires the custom error handler + // if any otherwise the framework sends the default http error text based on the status. + Err error + Try func() int + + // if true then it skips everything else and it throws a 404 not found error. + // Can be named as Failure but NotFound is more precise name in order + // to be visible that it's different than the `Err` + // because it throws a 404 not found instead of a 400 bad request. + // NotFound bool + // let's don't add this yet, it has its dangerous of missuse. +} + +var _ Result = Response{} + +// Dispatch writes the response result to the context's response writer. +func (r Response) Dispatch(ctx context.Context) { + if r.Path != "" && r.Err == nil { + // it's not a redirect valid status + if r.Code < 300 || r.Code >= 400 { + if ctx.Method() == "POST" { + r.Code = 303 // StatusSeeOther + } + r.Code = 302 // StatusFound + } + ctx.Redirect(r.Path, r.Code) + return + } + + if s := r.Text; s != "" { + r.Content = []byte(s) + } + + DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err, true) +} + +// View completes the `methodfunc.Result` interface. +// It's being used as an alternative return value which +// wraps the template file name, layout, (any) view data, status code and error. +// It's smart enough to complete the request and send the correct response to the client. +// +// Example at: https://github.com/kataras/iris/blob/master/_examples/mvc/overview/web/controllers/hello_controller.go. +type View struct { + Name string + Layout string + Data interface{} // map or a custom struct. + Code int + Err error +} + +var _ Result = View{} + +const dotB = byte('.') + +// DefaultViewExt is the default extension if `view.Name `is missing, +// but note that it doesn't care about +// the app.RegisterView(iris.$VIEW_ENGINE("./$dir", "$ext"))'s $ext. +// so if you don't use the ".html" as extension for your files +// you have to append the extension manually into the `view.Name` +// or change this global variable. +var DefaultViewExt = ".html" + +func ensureExt(s string) string { + if len(s) == 0 { + return "index" + DefaultViewExt + } + + if strings.IndexByte(s, dotB) < 1 { + s += DefaultViewExt + } + + return s +} + +// Dispatch writes the template filename, template layout and (any) data to the client. +// Completes the `Result` interface. +func (r View) Dispatch(ctx context.Context) { // r as Response view. + if r.Err != nil { + if r.Code < 400 { + r.Code = DefaultErrStatusCode + } + ctx.StatusCode(r.Code) + ctx.WriteString(r.Err.Error()) + ctx.StopExecution() + return + } + + if r.Code > 0 { + ctx.StatusCode(r.Code) + } + + if r.Name != "" { + r.Name = ensureExt(r.Name) + + if r.Layout != "" { + r.Layout = ensureExt(r.Layout) + ctx.ViewLayout(r.Layout) + } + + if r.Data != nil { + // In order to respect any c.Ctx.ViewData that may called manually before; + dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey() + if ctx.Values().Get(dataKey) == nil { + // if no c.Ctx.ViewData then it's empty do a + // pure set, it's faster. + ctx.Values().Set(dataKey, r.Data) + } else { + // else check if r.Data is map or struct, if struct convert it to map, + // do a range loop and set the data one by one. + // context.Map is actually a map[string]interface{} but we have to make that check; + if m, ok := r.Data.(map[string]interface{}); ok { + setViewData(ctx, m) + } else if m, ok := r.Data.(context.Map); ok { + setViewData(ctx, m) + } else if structs.IsStruct(r.Data) { + setViewData(ctx, structs.Map(r)) + } + } + } + + ctx.View(r.Name) + } +} + +func setViewData(ctx context.Context, data map[string]interface{}) { + for k, v := range data { + ctx.ViewData(k, v) + } +} diff --git a/mvc2/handler_out_test.go b/mvc2/handler_out_test.go new file mode 100644 index 0000000000..bf5abda300 --- /dev/null +++ b/mvc2/handler_out_test.go @@ -0,0 +1,271 @@ +package mvc2_test + +// import ( +// "errors" +// "testing" + +// "github.com/kataras/iris" +// "github.com/kataras/iris/context" +// "github.com/kataras/iris/httptest" +// "github.com/kataras/iris/mvc2" +// ) + +// // activator/methodfunc/func_caller.go. +// // and activator/methodfunc/func_result_dispatcher.go + +// type testControllerMethodResult struct { +// mvc2.C +// } + +// func (c *testControllerMethodResult) Get() mvc2.Result { +// return mvc2.Response{ +// Text: "Hello World!", +// } +// } + +// func (c *testControllerMethodResult) GetWithStatus() mvc2.Response { // or mvc.Result again, no problem. +// return mvc2.Response{ +// Text: "This page doesn't exist", +// Code: iris.StatusNotFound, +// } +// } + +// type testCustomStruct struct { +// Name string `json:"name" xml:"name"` +// Age int `json:"age" xml:"age"` +// } + +// func (c *testControllerMethodResult) GetJson() mvc2.Result { +// var err error +// if c.Ctx.URLParamExists("err") { +// err = errors.New("error here") +// } +// return mvc2.Response{ +// Err: err, // if err != nil then it will fire the error's text with a BadRequest. +// Object: testCustomStruct{Name: "Iris", Age: 2}, +// } +// } + +// var things = []string{"thing 0", "thing 1", "thing 2"} + +// func (c *testControllerMethodResult) GetThingWithTryBy(index int) mvc2.Result { +// failure := mvc2.Response{ +// Text: "thing does not exist", +// Code: iris.StatusNotFound, +// } + +// return mvc2.Try(func() mvc2.Result { +// // if panic because of index exceed the slice +// // then the "failure" response will be returned instead. +// return mvc2.Response{Text: things[index]} +// }, failure) +// } + +// func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) mvc2.Result { +// return mvc2.Try(func() mvc2.Result { +// // if panic because of index exceed the slice +// // then the default failure response will be returned instead (400 bad request). +// return mvc2.Response{Text: things[index]} +// }) +// } + +// func TestControllerMethodResult(t *testing.T) { +// app := iris.New() +// app.Controller("/", new(testControllerMethodResult)) + +// e := httptest.New(t, app) + +// e.GET("/").Expect().Status(iris.StatusOK). +// Body().Equal("Hello World!") + +// e.GET("/with/status").Expect().Status(iris.StatusNotFound). +// Body().Equal("This page doesn't exist") + +// e.GET("/json").Expect().Status(iris.StatusOK). +// JSON().Equal(iris.Map{ +// "name": "Iris", +// "age": 2, +// }) + +// e.GET("/json").WithQuery("err", true).Expect(). +// Status(iris.StatusBadRequest). +// Body().Equal("error here") + +// e.GET("/thing/with/try/1").Expect(). +// Status(iris.StatusOK). +// Body().Equal("thing 1") +// // failure because of index exceed the slice +// e.GET("/thing/with/try/3").Expect(). +// Status(iris.StatusNotFound). +// Body().Equal("thing does not exist") + +// e.GET("/thing/with/try/default/3").Expect(). +// Status(iris.StatusBadRequest). +// Body().Equal("Bad Request") +// } + +// type testControllerMethodResultTypes struct { +// mvc2.C +// } + +// func (c *testControllerMethodResultTypes) GetText() string { +// return "text" +// } + +// func (c *testControllerMethodResultTypes) GetStatus() int { +// return iris.StatusBadGateway +// } + +// func (c *testControllerMethodResultTypes) GetTextWithStatusOk() (string, int) { +// return "OK", iris.StatusOK +// } + +// // tests should have output arguments mixed +// func (c *testControllerMethodResultTypes) GetStatusWithTextNotOkBy(first string, second string) (int, string) { +// return iris.StatusForbidden, "NOT_OK_" + first + second +// } + +// func (c *testControllerMethodResultTypes) GetTextAndContentType() (string, string) { +// return "text", "text/html" +// } + +// type testControllerMethodCustomResult struct { +// HTML string +// } + +// // The only one required function to make that a custom Response dispatcher. +// func (r testControllerMethodCustomResult) Dispatch(ctx context.Context) { +// ctx.HTML(r.HTML) +// } + +// func (c *testControllerMethodResultTypes) GetCustomResponse() testControllerMethodCustomResult { +// return testControllerMethodCustomResult{"text"} +// } + +// func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) { +// return testControllerMethodCustomResult{"OK"}, iris.StatusOK +// } + +// func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) { +// return testControllerMethodCustomResult{"internal server error"}, iris.StatusInternalServerError +// } + +// func (c *testControllerMethodResultTypes) GetCustomStruct() testCustomStruct { +// return testCustomStruct{"Iris", 2} +// } + +// func (c *testControllerMethodResultTypes) GetCustomStructWithStatusNotOk() (testCustomStruct, int) { +// return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError +// } + +// func (c *testControllerMethodResultTypes) GetCustomStructWithContentType() (testCustomStruct, string) { +// return testCustomStruct{"Iris", 2}, "text/xml" +// } + +// func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCustomStruct, err error) { +// s = testCustomStruct{"Iris", 2} +// if c.Ctx.URLParamExists("err") { +// err = errors.New("omit return of testCustomStruct and fire error") +// } + +// // it should send the testCustomStruct as JSON if error is nil +// // otherwise it should fire the default error(BadRequest) with the error's text. +// return +// } + +// func TestControllerMethodResultTypes(t *testing.T) { +// app := iris.New() +// app.Controller("/", new(testControllerMethodResultTypes)) + +// e := httptest.New(t, app) + +// e.GET("/text").Expect().Status(iris.StatusOK). +// Body().Equal("text") + +// e.GET("/status").Expect().Status(iris.StatusBadGateway) + +// e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK). +// Body().Equal("OK") + +// e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden). +// Body().Equal("NOT_OK_firstsecond") + +// e.GET("/text/and/content/type").Expect().Status(iris.StatusOK). +// ContentType("text/html", "utf-8"). +// Body().Equal("text") + +// e.GET("/custom/response").Expect().Status(iris.StatusOK). +// ContentType("text/html", "utf-8"). +// Body().Equal("text") +// e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK). +// ContentType("text/html", "utf-8"). +// Body().Equal("OK") +// e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). +// ContentType("text/html", "utf-8"). +// Body().Equal("internal server error") + +// expectedResultFromCustomStruct := map[string]interface{}{ +// "name": "Iris", +// "age": 2, +// } +// e.GET("/custom/struct").Expect().Status(iris.StatusOK). +// JSON().Equal(expectedResultFromCustomStruct) +// e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). +// JSON().Equal(expectedResultFromCustomStruct) +// e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK). +// ContentType("text/xml", "utf-8") +// e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK). +// JSON().Equal(expectedResultFromCustomStruct) +// e.GET("/custom/struct/with/error").WithQuery("err", true).Expect(). +// Status(iris.StatusBadRequest). // the default status code if error is not nil +// // the content should be not JSON it should be the status code's text +// // it will fire the error's text +// Body().Equal("omit return of testCustomStruct and fire error") +// } + +// type testControllerViewResultRespectCtxViewData struct { +// T *testing.T +// mvc2.C +// } + +// func (t *testControllerViewResultRespectCtxViewData) BeginRequest(ctx context.Context) { +// t.C.BeginRequest(ctx) +// ctx.ViewData("name_begin", "iris_begin") +// } + +// func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx context.Context) { +// t.C.EndRequest(ctx) +// // check if data is not overridden by return mvc.View {Data: context.Map...} + +// dataWritten := ctx.GetViewData() +// if dataWritten == nil { +// t.T.Fatalf("view data is nil, both BeginRequest and Get failed to write the data") +// return +// } + +// if dataWritten["name_begin"] == nil { +// t.T.Fatalf(`view data[name_begin] is nil, +// BeginRequest's ctx.ViewData call have been overridden by Get's return mvc.View {Data: }. +// Total view data: %v`, dataWritten) +// } + +// if dataWritten["name"] == nil { +// t.T.Fatalf("view data[name] is nil, Get's return mvc.View {Data: } didn't work. Total view data: %v", dataWritten) +// } +// } + +// func (t *testControllerViewResultRespectCtxViewData) Get() mvc2.Result { +// return mvc2.View{ +// Name: "doesnt_exists.html", +// Data: context.Map{"name": "iris"}, // we care about this only. +// Code: iris.StatusInternalServerError, +// } +// } + +// func TestControllerViewResultRespectCtxViewData(t *testing.T) { +// app := iris.New() +// app.Controller("/", new(testControllerViewResultRespectCtxViewData), t) +// e := httptest.New(t, app) + +// e.GET("/").Expect().Status(iris.StatusInternalServerError) +// } diff --git a/mvc2/session_controller.go b/mvc2/session_controller.go new file mode 100644 index 0000000000..5ea5f5c442 --- /dev/null +++ b/mvc2/session_controller.go @@ -0,0 +1,47 @@ +package mvc2 + +import ( + "github.com/kataras/iris/context" + "github.com/kataras/iris/sessions" + "reflect" + + "github.com/kataras/golog" +) + +var defaultManager = sessions.New(sessions.Config{}) + +// SessionController is a simple `Controller` implementation +// which requires a binded session manager in order to give +// direct access to the current client's session via its `Session` field. +type SessionController struct { + C + + Manager *sessions.Sessions + Session *sessions.Session +} + +// OnActivate called, once per application lifecycle NOT request, +// every single time the dev registers a specific SessionController-based controller. +// It makes sure that its "Manager" field is filled +// even if the caller didn't provide any sessions manager via the `app.Controller` function. +func (s *SessionController) OnActivate(ca *ControllerActivator) { + if !ca.Engine.BindTypeExists(reflect.TypeOf(defaultManager)) { + ca.Engine.Bind(defaultManager) + golog.Warnf(`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, +therefore this controller is using the default sessions manager instead. +Please refer to the documentation to learn how you can provide the session manager`) + } +} + +// BeginRequest calls the Controller's BeginRequest +// and tries to initialize the current user's Session. +func (s *SessionController) BeginRequest(ctx context.Context) { + s.C.BeginRequest(ctx) + if s.Manager == nil { + ctx.Application().Logger().Errorf(`MVC SessionController: sessions manager is nil, report this as a bug +because the SessionController should predict this on its activation state and use a default one automatically`) + return + } + + s.Session = s.Manager.Start(ctx) +} From f49a12279addd4d9344ddb53556c28310dcd0197 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 10 Dec 2017 07:00:51 +0200 Subject: [PATCH 11/79] ok the new mvc system works great, all tests done and almost x3 smaller LOC used plus new three awesome features:) - next commit will be commenting out and replace the mvc package with the new mvc2 --- .gitattributes | 20 +- _examples/README.md | 0 context/context.go | 47 +- middleware/README.md | 0 .../github.com/iris-contrib/i18n/i18n.go | 0 .../i18n/vendor/gopkg.in/ini.v1/LICENSE | 0 .../i18n/vendor/gopkg.in/ini.v1/error.go | 0 .../i18n/vendor/gopkg.in/ini.v1/ini.go | 0 .../i18n/vendor/gopkg.in/ini.v1/key.go | 0 .../i18n/vendor/gopkg.in/ini.v1/parser.go | 0 .../i18n/vendor/gopkg.in/ini.v1/section.go | 0 .../i18n/vendor/gopkg.in/ini.v1/struct.go | 0 mvc/session_controller.go | 4 +- mvc2/bind.go | 244 ++++++++++ mvc2/binder/binder/input.go | 4 - mvc2/binder/binding.go | 19 - mvc2/binder/func_input.go | 1 - mvc2/binder/func_result.go | 53 --- mvc2/binder/reflect.go | 107 ----- mvc2/binder/to_struct.go | 50 -- mvc2/binder_in.go | 173 ------- mvc2/binder_in_service.go | 81 ---- mvc2/binder_in_service_test.go | 46 -- mvc2/binder_in_test.go | 143 ------ mvc2/controller.go | 267 ++++++----- ...dler_test.go => controller_handle_test.go} | 33 +- mvc2/controller_test.go | 446 ++++++++++++++++++ mvc2/engine.go | 92 ++-- mvc2/{mvc_test.go => engine_handler_test.go} | 2 +- mvc2/{handler_out.go => func_result.go} | 10 +- mvc2/func_result_test.go | 275 +++++++++++ mvc2/handler.go | 100 ++-- mvc2/handler_out_test.go | 271 ----------- mvc2/handler_test.go | 16 +- ..._in_path_param.go => path_param_binder.go} | 101 ++-- ...aram_test.go => path_param_binder_test.go} | 0 mvc2/reflect.go | 73 ++- mvc2/service.go | 206 -------- mvc2/session_controller.go | 8 +- 39 files changed, 1332 insertions(+), 1560 deletions(-) mode change 100755 => 100644 _examples/README.md mode change 100755 => 100644 middleware/README.md mode change 100755 => 100644 middleware/i18n/vendor/github.com/iris-contrib/i18n/i18n.go mode change 100755 => 100644 middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/LICENSE mode change 100755 => 100644 middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/error.go mode change 100755 => 100644 middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/ini.go mode change 100755 => 100644 middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/key.go mode change 100755 => 100644 middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/parser.go mode change 100755 => 100644 middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/section.go mode change 100755 => 100644 middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/struct.go create mode 100644 mvc2/bind.go delete mode 100644 mvc2/binder/binder/input.go delete mode 100644 mvc2/binder/binding.go delete mode 100644 mvc2/binder/func_input.go delete mode 100644 mvc2/binder/func_result.go delete mode 100644 mvc2/binder/reflect.go delete mode 100644 mvc2/binder/to_struct.go delete mode 100644 mvc2/binder_in.go delete mode 100644 mvc2/binder_in_service.go delete mode 100644 mvc2/binder_in_service_test.go delete mode 100644 mvc2/binder_in_test.go rename mvc2/{controller_handler_test.go => controller_handle_test.go} (70%) create mode 100644 mvc2/controller_test.go rename mvc2/{mvc_test.go => engine_handler_test.go} (89%) rename mvc2/{handler_out.go => func_result.go} (97%) create mode 100644 mvc2/func_result_test.go delete mode 100644 mvc2/handler_out_test.go rename mvc2/{binder_in_path_param.go => path_param_binder.go} (51%) rename mvc2/{binder_in_path_param_test.go => path_param_binder_test.go} (100%) delete mode 100644 mvc2/service.go diff --git a/.gitattributes b/.gitattributes index 158482ee13..574feb3509 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,11 +1,11 @@ -*.go linguist-language=Go -vendor/* linguist-vendored -_examples/* linguist-documentation -_benchmarks/* linguist-documentation -# Set the default behavior, in case people don't have core.autocrlf set. -# if from windows: -# git config --global core.autocrlf true -# if from unix: -# git config --global core.autocrlf input -# https://help.github.com/articles/dealing-with-line-endings/#per-repository-settings +*.go linguist-language=Go +vendor/* linguist-vendored +_examples/* linguist-documentation +_benchmarks/* linguist-documentation +# Set the default behavior, in case people don't have core.autocrlf set. +# if from windows: +# git config --global core.autocrlf true +# if from unix: +# git config --global core.autocrlf input +# https://help.github.com/articles/dealing-with-line-endings/#per-repository-settings * text=auto \ No newline at end of file diff --git a/_examples/README.md b/_examples/README.md old mode 100755 new mode 100644 diff --git a/context/context.go b/context/context.go index a669f619af..19ea0f9ac1 100644 --- a/context/context.go +++ b/context/context.go @@ -697,18 +697,23 @@ type Context interface { // ServeContent serves content, headers are autoset // receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string) // - // You can define your own "Content-Type" header also, after this function call - // Doesn't implements resuming (by range), use ctx.SendFile instead + // + // You can define your own "Content-Type" with `context#ContentType`, before this function call. + // + // This function doesn't support resuming (by range), + // use ctx.SendFile or router's `StaticWeb` instead. ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error - // ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename) + // ServeFile serves a file (to send a file, a zip for example to the client you should use the `SendFile` instead) // receives two parameters // filename/path (string) // gzipCompression (bool) // - // You can define your own "Content-Type" header also, after this function call - // This function doesn't implement resuming (by range), use ctx.SendFile instead + // You can define your own "Content-Type" with `context#ContentType`, before this function call. + // + // This function doesn't support resuming (by range), + // use ctx.SendFile or router's `StaticWeb` instead. // - // Use it when you want to serve css/js/... files to the client, for bigger files and 'force-download' use the SendFile. + // Use it when you want to serve dynamic files to the client. ServeFile(filename string, gzipCompression bool) error // SendFile sends file for force-download to the client // @@ -806,6 +811,11 @@ type Context interface { // to be executed at serve-time. The full app's fields // and methods are not available here for the developer's safety. Application() Application + + // String returns the string representation of this request. + // Each context has a unique string representation, so this can be used + // as an "ID" as well, if needed. + String() string } // Next calls all the next handler from the handlers chain, @@ -857,7 +867,11 @@ type Map map[string]interface{} // +------------------------------------------------------------+ type context struct { - // the http.ResponseWriter wrapped by custom writer + // the unique id, it's empty until `String` function is called, + // it's here to cache the random, unique context's id, although `String` + // returns more than this. + id string + // the http.ResponseWriter wrapped by custom writer. writer ResponseWriter // the original http.Request request *http.Request @@ -865,10 +879,10 @@ type context struct { currentRouteName string // the local key-value storage - params RequestParams // url named parameters - values memstore.Store // generic storage, middleware communication + params RequestParams // url named parameters. + values memstore.Store // generic storage, middleware communication. - // the underline application app + // the underline application app. app Application // the route's handlers handlers Handlers @@ -2721,6 +2735,19 @@ func (ctx *context) Exec(method string, path string) { } } +// String returns the string representation of this request. +// Each context has a unique string representation, so this can be used +// as an "ID" as well, if needed. +func (ctx *context) String() (s string) { + if ctx.id == "" { + // set the id here. + + s = "..." + } + + return +} + // Application returns the iris app instance which belongs to this context. // Worth to notice that this function returns an interface // of the Application, which contains methods that are safe diff --git a/middleware/README.md b/middleware/README.md old mode 100755 new mode 100644 diff --git a/middleware/i18n/vendor/github.com/iris-contrib/i18n/i18n.go b/middleware/i18n/vendor/github.com/iris-contrib/i18n/i18n.go old mode 100755 new mode 100644 diff --git a/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/LICENSE b/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/LICENSE old mode 100755 new mode 100644 diff --git a/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/error.go b/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/error.go old mode 100755 new mode 100644 diff --git a/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/ini.go b/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/ini.go old mode 100755 new mode 100644 diff --git a/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/key.go b/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/key.go old mode 100755 new mode 100644 diff --git a/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/parser.go b/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/parser.go old mode 100755 new mode 100644 diff --git a/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/section.go b/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/section.go old mode 100755 new mode 100644 diff --git a/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/struct.go b/middleware/i18n/vendor/github.com/iris-contrib/i18n/vendor/gopkg.in/ini.v1/struct.go old mode 100755 new mode 100644 diff --git a/mvc/session_controller.go b/mvc/session_controller.go index 240a88be81..bc403311a3 100644 --- a/mvc/session_controller.go +++ b/mvc/session_controller.go @@ -14,7 +14,7 @@ var defaultManager = sessions.New(sessions.Config{}) // which requires a binded session manager in order to give // direct access to the current client's session via its `Session` field. type SessionController struct { - Controller + C Manager *sessions.Sessions Session *sessions.Session @@ -36,7 +36,7 @@ Please refer to the documentation to learn how you can provide the session manag // BeginRequest calls the Controller's BeginRequest // and tries to initialize the current user's Session. func (s *SessionController) BeginRequest(ctx context.Context) { - s.Controller.BeginRequest(ctx) + s.C.BeginRequest(ctx) if s.Manager == nil { ctx.Application().Logger().Errorf(`MVC SessionController: sessions manager is nil, report this as a bug because the SessionController should predict this on its activation state and use a default one automatically`) diff --git a/mvc2/bind.go b/mvc2/bind.go new file mode 100644 index 0000000000..d305dc5009 --- /dev/null +++ b/mvc2/bind.go @@ -0,0 +1,244 @@ +package mvc2 + +import "reflect" + +type bindType uint32 + +const ( + objectType bindType = iota // simple assignable value. + functionResultType // dynamic value, depends on the context. +) + +type bindObject struct { + Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' . + Value reflect.Value + + BindType bindType + ReturnValue func(ctx []reflect.Value) reflect.Value +} + +// makeReturnValue takes any function +// that accept a context and returns something +// and returns a binder function, which accepts the context as slice of reflect.Value +// and returns a reflect.Value for that. +// Iris uses to +// resolve and set the input parameters when a handler is executed. +// +// The "fn" can have the following form: +// `func(iris.Context) UserViewModel`. +// +// The return type of the "fn" 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 makeReturnValue(fn reflect.Value) (func([]reflect.Value) reflect.Value, reflect.Type, error) { + typ := indirectTyp(fn.Type()) + + // invalid if not a func. + if typ.Kind() != reflect.Func { + return nil, typ, errBad + } + + // invalid if not returns one single value. + if typ.NumOut() != 1 { + return nil, typ, errBad + } + + // invalid if input args length is not one. + if typ.NumIn() != 1 { + return nil, typ, errBad + } + + // invalid if that single input arg is not a typeof context.Context. + if !isContext(typ.In(0)) { + return nil, typ, errBad + } + + outTyp := typ.Out(0) + zeroOutVal := reflect.New(outTyp).Elem() + + bf := func(ctxValue []reflect.Value) reflect.Value { + results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler. + if len(results) == 0 { + return zeroOutVal + } + + v := results[0] + if !v.IsValid() { + return zeroOutVal + } + return v + } + + return bf, outTyp, nil +} + +func makeBindObject(v reflect.Value) (b bindObject, err error) { + if isFunc(v) { + b.BindType = functionResultType + b.ReturnValue, b.Type, err = makeReturnValue(v) + } else { + b.BindType = objectType + b.Type = v.Type() + b.Value = v + } + + return +} + +func (b *bindObject) IsAssignable(to reflect.Type) bool { + return equalTypes(b.Type, to) +} + +func (b *bindObject) Assign(ctx []reflect.Value, toSetter func(reflect.Value)) { + if b.BindType == functionResultType { + toSetter(b.ReturnValue(ctx)) + return + } + toSetter(b.Value) +} + +type ( + targetField struct { + Object *bindObject + FieldIndex []int + } + targetFuncInput struct { + Object *bindObject + InputIndex int + } +) + +type targetStruct struct { + Fields []*targetField + Valid bool // is True when contains fields and it's a valid target struct. +} + +func newTargetStruct(v reflect.Value, bindValues ...reflect.Value) *targetStruct { + typ := indirectTyp(v.Type()) + s := &targetStruct{} + + fields := lookupFields(typ, nil) + for _, f := range fields { + for _, val := range bindValues { + // the binded values to the struct's fields. + b, err := makeBindObject(val) + + if err != nil { + return s // if error stop here. + } + + if b.IsAssignable(f.Type) { + // fmt.Printf("bind the object to the field: %s at index: %#v and type: %s\n", f.Name, f.Index, f.Type.String()) + s.Fields = append(s.Fields, &targetField{ + FieldIndex: f.Index, + Object: &b, + }) + break + } + + } + } + + s.Valid = len(s.Fields) > 0 + return s +} + +func (s *targetStruct) Fill(destElem reflect.Value, ctx ...reflect.Value) { + for _, f := range s.Fields { + f.Object.Assign(ctx, func(v reflect.Value) { + // defer func() { + // if err := recover(); err != nil { + // fmt.Printf("for index: %#v on: %s where num fields are: %d\n", + // f.FieldIndex, f.Object.Type.String(), destElem.NumField()) + // } + // }() + destElem.FieldByIndex(f.FieldIndex).Set(v) + }) + } +} + +type targetFunc struct { + Inputs []*targetFuncInput + Valid bool // is True when contains func inputs and it's a valid target func. +} + +func newTargetFunc(fn reflect.Value, bindValues ...reflect.Value) *targetFunc { + typ := indirectTyp(fn.Type()) + s := &targetFunc{ + Valid: false, + } + + if !isFunc(typ) { + return s + } + + n := typ.NumIn() + + // function input can have many values of the same types, + // so keep track of them in order to not set a func input to a next bind value, + // i.e (string, string) with two different binder funcs because of the different param's name. + consumedValues := make(map[int]bool, n) + + for i := 0; i < n; i++ { + inTyp := typ.In(i) + + // if it's context then bind it directly here and continue to the next func's input arg. + if isContext(inTyp) { + s.Inputs = append(s.Inputs, &targetFuncInput{ + InputIndex: i, + Object: &bindObject{ + Type: contextTyp, + BindType: functionResultType, + ReturnValue: func(ctxValue []reflect.Value) reflect.Value { + return ctxValue[0] + }, + }, + }) + continue + } + + for valIdx, val := range bindValues { + if _, shouldSkip := consumedValues[valIdx]; shouldSkip { + continue + } + inTyp := typ.In(i) + + // the binded values to the func's inputs. + b, err := makeBindObject(val) + + if err != nil { + return s // if error stop here. + } + + if b.IsAssignable(inTyp) { + // fmt.Printf("binded input index: %d for type: %s and value: %v with pointer: %v\n", + // i, b.Type.String(), val.String(), val.Pointer()) + s.Inputs = append(s.Inputs, &targetFuncInput{ + InputIndex: i, + Object: &b, + }) + + consumedValues[valIdx] = true + break + } + } + } + + s.Valid = len(s.Inputs) > 0 + return s +} + +func (s *targetFunc) Fill(in *[]reflect.Value, ctx ...reflect.Value) { + args := *in + for _, input := range s.Inputs { + input.Object.Assign(ctx, func(v reflect.Value) { + // fmt.Printf("assign input index: %d for value: %v\n", + // input.InputIndex, v.String()) + args[input.InputIndex] = v + }) + + } + + *in = args +} diff --git a/mvc2/binder/binder/input.go b/mvc2/binder/binder/input.go deleted file mode 100644 index 1e583530ef..0000000000 --- a/mvc2/binder/binder/input.go +++ /dev/null @@ -1,4 +0,0 @@ -package binder - -type Input interface { -} diff --git a/mvc2/binder/binding.go b/mvc2/binder/binding.go deleted file mode 100644 index 9fe0eed383..0000000000 --- a/mvc2/binder/binding.go +++ /dev/null @@ -1,19 +0,0 @@ -package binder - -import ( - "reflect" -) - -type Binding interface { - AddSource(v reflect.Value, source ...reflect.Value) -} - -type StructValue struct { - Type reflect.Type - Value reflect.Value -} - -type FuncResultValue struct { - Type reflect.Type - ReturnValue func(ctx []reflect.Value) reflect.Value -} diff --git a/mvc2/binder/func_input.go b/mvc2/binder/func_input.go deleted file mode 100644 index 0587a0cced..0000000000 --- a/mvc2/binder/func_input.go +++ /dev/null @@ -1 +0,0 @@ -package binder diff --git a/mvc2/binder/func_result.go b/mvc2/binder/func_result.go deleted file mode 100644 index cb8ba5fe17..0000000000 --- a/mvc2/binder/func_result.go +++ /dev/null @@ -1,53 +0,0 @@ -package binder - -import ( - "errors" - "reflect" -) - -var ( - errBad = errors.New("bad") -) - -func makeReturnValue(fn reflect.Value) (func([]reflect.Value) reflect.Value, reflect.Type, error) { - typ := indirectTyp(fn.Type()) - - // invalid if not a func. - if typ.Kind() != reflect.Func { - return nil, typ, errBad - } - - // invalid if not returns one single value. - if typ.NumOut() != 1 { - return nil, typ, errBad - } - - // invalid if input args length is not one. - if typ.NumIn() != 1 { - return nil, typ, errBad - } - - // invalid if that single input arg is not a typeof context.Context. - if !isContext(typ.In(0)) { - return nil, typ, errBad - } - - outTyp := typ.Out(0) - zeroOutVal := reflect.New(outTyp).Elem() - - bf := func(ctxValue []reflect.Value) reflect.Value { - // []reflect.Value{reflect.ValueOf(ctx)} - results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler. - if len(results) == 0 { - return zeroOutVal - } - - v := results[0] - if !v.IsValid() { - return zeroOutVal - } - return v - } - - return bf, outTyp, nil -} diff --git a/mvc2/binder/reflect.go b/mvc2/binder/reflect.go deleted file mode 100644 index 20c75b9fb3..0000000000 --- a/mvc2/binder/reflect.go +++ /dev/null @@ -1,107 +0,0 @@ -package binder - -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 -} - -/* -// no f. this, it's too complicated and it will be harder to maintain later on: -func isSliceAndExpectedItem(got reflect.Type, in []reflect.Type, currentBindersIdx int) bool { - kind := got.Kind() - // if got result is slice or array. - return (kind == reflect.Slice || kind == reflect.Array) && - // if has expected next input. - len(in)-1 > currentBindersIdx && - // if the current input's type is not the same as got (if it's not a slice of that types or anything else). - equalTypes(got, in[currentBindersIdx]) -} -*/ - -func equalTypes(got reflect.Type, expected reflect.Type) bool { - if got == expected { - return true - } - // if accepts an interface, check if the given "got" type does - // implement this "expected" user handler's input argument. - if expected.Kind() == reflect.Interface { - // fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String()) - return got.Implements(expected) - } - return false -} - -// for controller only. - -func structFieldIgnored(f reflect.StructField) bool { - if !f.Anonymous { - return true // if not anonymous(embedded), ignore it. - } - - s := f.Tag.Get("ignore") - return s == "true" // if has an ignore tag then ignore it. -} - -type field struct { - Type reflect.Type - Index []int // the index of the field, slice if it's part of a embedded struct - Name string // the actual name - - // this could be empty, but in our cases it's not, - // it's filled with the service and it's filled from the lookupFields' caller. - AnyValue reflect.Value -} - -func lookupFields(typ reflect.Type, parentIndex int) (fields []field) { - for i, n := 0, typ.NumField(); i < n; i++ { - f := typ.Field(i) - - if f.Type.Kind() == reflect.Struct && !structFieldIgnored(f) { - fields = append(fields, lookupFields(f.Type, i)...) - continue - } - - index := []int{i} - if parentIndex >= 0 { - index = append([]int{parentIndex}, index...) - } - - field := field{ - Type: f.Type, - Name: f.Name, - Index: index, - } - - fields = append(fields, field) - } - - return -} diff --git a/mvc2/binder/to_struct.go b/mvc2/binder/to_struct.go deleted file mode 100644 index cb85d0d6a3..0000000000 --- a/mvc2/binder/to_struct.go +++ /dev/null @@ -1,50 +0,0 @@ -package binder - -import ( - "reflect" -) - -type StructBinding struct { - Field StructValue - Func FuncResultValue -} - -func (b *StructBinding) AddSource(dest reflect.Value, source ...reflect.Value) { - typ := indirectTyp(dest.Type()) //indirectTyp(reflect.TypeOf(dest)) - if typ.Kind() != reflect.Struct { - return - } - - fields := lookupFields(typ, -1) - for _, f := range fields { - for _, s := range source { - if s.Type().Kind() == reflect.Func { - returnValue, outType, err := makeReturnValue(s) - if err != nil { - continue - } - gotTyp = outType - service.ReturnValue = returnValue - } - - gotTyp := s.Type() - - v := StructValue{ - Type: gotTyp, - Value: s, - FieldIndex: f.Index, - } - - if equalTypes(gotTyp, f.Type) { - service.Type = gotTyp - _serv = append(_serv, &service) - fmt.Printf("[2] Bind In=%s->%s for struct field[%d]\n", f.Type, gotTyp.String(), f.Index) - break - } - } - } - fmt.Printf("[2] Bind %d for %s\n", len(_serv), typ.String()) - *serv = _serv - - return -} diff --git a/mvc2/binder_in.go b/mvc2/binder_in.go deleted file mode 100644 index acc8dee880..0000000000 --- a/mvc2/binder_in.go +++ /dev/null @@ -1,173 +0,0 @@ -package mvc2 - -import ( - "reflect" -) - -// 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 { - BinderType binderType - BindType reflect.Type - BindFunc func(ctx []reflect.Value) reflect.Value -} - -// key = the func input argument index, value is the responsible input binder. -type bindersMap map[int]*InputBinder - -// joinBindersMap joins the "m2" to m1 and returns the result, it's the same "m1" map. -// if "m2" is not nil and "m2" is not nil then it loops the "m2"'s keys and sets the values -// to the "m1", if "m2" is not and not empty nil but m1 is nil then "m1" = "m2". -// The result may be nil if the "m1" and "m2" are nil or "m2" is empty and "m1" is nil. -func joinBindersMap(m1, m2 bindersMap) bindersMap { - if m2 != nil && len(m2) > 0 { - if m1 == nil { - m1 = m2 - } else { - for k, v := range m2 { - m1[k] = v - } - } - } - return m1 -} - -// getBindersForInput 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 getBindersForInput(binders []*InputBinder, expected ...reflect.Type) (m bindersMap) { - for idx, in := range expected { - if idx == 0 && isContext(in) { - // if the first is context then set it directly here. - m = make(bindersMap) - m[0] = &InputBinder{ - BindType: contextTyp, - BindFunc: func(ctxValues []reflect.Value) reflect.Value { - return ctxValues[0] - }, - } - continue - } - - for _, b := range binders { - if equalTypes(b.BindType, in) { - if m == nil { - m = make(bindersMap) - } - // 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 -} - -// MustMakeFuncInputBinder calls the `MakeFuncInputBinder` and returns its first result, see its docs. -// It panics on error. -func MustMakeFuncInputBinder(binder interface{}) *InputBinder { - b, err := MakeFuncInputBinder(binder) - if err != nil { - panic(err) - } - return b -} - -type binderType uint32 - -const ( - functionType binderType = iota - serviceType - invalidType -) - -func resolveBinderType(binder interface{}) binderType { - if binder == nil { - return invalidType - } - - return resolveBinderTypeFromKind(reflect.TypeOf(binder).Kind()) -} - -func resolveBinderTypeFromKind(k reflect.Kind) binderType { - switch k { - case reflect.Func: - return functionType - case reflect.Struct, reflect.Interface, reflect.Ptr, reflect.Slice, reflect.Array: - return serviceType - } - - return invalidType -} - -// MakeFuncInputBinder 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`. -// -// 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 MakeFuncInputBinder(binder interface{}) (*InputBinder, error) { - v := reflect.ValueOf(binder) - return makeFuncInputBinder(v) -} - -func makeFuncInputBinder(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(ctxValue []reflect.Value) reflect.Value { - // []reflect.Value{reflect.ValueOf(ctx)} - results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler. - if len(results) == 0 { - return zeroOutVal - } - - v := results[0] - if !v.IsValid() { - return zeroOutVal - } - return v - } - - return &InputBinder{ - BinderType: functionType, - BindType: outTyp, - BindFunc: bf, - }, nil -} diff --git a/mvc2/binder_in_service.go b/mvc2/binder_in_service.go deleted file mode 100644 index db4fde0874..0000000000 --- a/mvc2/binder_in_service.go +++ /dev/null @@ -1,81 +0,0 @@ -package mvc2 - -import ( - "reflect" -) - -type serviceFieldBinder struct { - Index []int - Binder *InputBinder -} - -func getServicesBinderForStruct(binders []*InputBinder, typ reflect.Type) func(elem reflect.Value) { - fields := lookupFields(typ, -1) - var validBinders []*serviceFieldBinder - - for _, b := range binders { - for _, f := range fields { - if b.BinderType != serviceType { - continue - } - if equalTypes(b.BindType, f.Type) { - validBinders = append(validBinders, - &serviceFieldBinder{Index: f.Index, Binder: b}) - } - } - - } - - if len(validBinders) == 0 { - return func(_ reflect.Value) {} - } - - return func(elem reflect.Value) { - for _, b := range validBinders { - elem.FieldByIndex(b.Index).Set(b.Binder.BindFunc(nil)) - } - } -} - -// MustMakeServiceInputBinder calls the `MakeServiceInputBinder` and returns its first result, see its docs. -// It panics on error. -func MustMakeServiceInputBinder(service interface{}) *InputBinder { - s, err := MakeServiceInputBinder(service) - if err != nil { - panic(err) - } - return s -} - -// MakeServiceInputBinder uses a difference/or strange approach, -// we make the services as bind functions -// in order to keep the rest of the code simpler, however we have -// a performance penalty when calling the function instead -// of just put the responsible service to the certain handler's input argument. -func MakeServiceInputBinder(service interface{}) (*InputBinder, error) { - if service == nil { - return nil, errNil - } - - var ( - val = reflect.ValueOf(service) - typ = val.Type() - ) - - if !goodVal(val) { - return nil, errBad - } - - if indirectTyp(typ).Kind() != reflect.Struct { - // if the pointer's struct is not a struct then return err bad. - return nil, errBad - } - - return &InputBinder{ - BinderType: serviceType, - BindType: typ, - BindFunc: func(_ []reflect.Value) reflect.Value { - return val - }, - }, nil -} diff --git a/mvc2/binder_in_service_test.go b/mvc2/binder_in_service_test.go deleted file mode 100644 index 88779dce8d..0000000000 --- a/mvc2/binder_in_service_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package mvc2 - -import ( - "reflect" - "testing" -) - -type ( - testService interface { - say(string) - } - testServiceImpl struct { - prefix string - } -) - -func (s *testServiceImpl) say(message string) string { - return s.prefix + ": " + message -} - -func TestMakeServiceInputBinder(t *testing.T) { - expectedService := &testServiceImpl{"say"} - b := MustMakeServiceInputBinder(expectedService) - // in - var ( - intType = reflect.TypeOf(1) - availableBinders = []*InputBinder{b} - ) - - // 1 - testCheck(t, "test1", true, testGetBindersForInput(t, availableBinders, - []interface{}{expectedService}, reflect.TypeOf(expectedService))) - // 2 - testCheck(t, "test2-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{42})) - // 3 - testCheck(t, "test3-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{42}, intType)) - // 4 - testCheck(t, "test4-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{42})) - // 5 - check if nothing passed, so no valid binders at all. - testCheck(t, "test5", true, testGetBindersForInput(t, availableBinders, - []interface{}{})) - -} diff --git a/mvc2/binder_in_test.go b/mvc2/binder_in_test.go deleted file mode 100644 index 099a3578f7..0000000000 --- a/mvc2/binder_in_test.go +++ /dev/null @@ -1,143 +0,0 @@ -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, - } -} - -func TestMakeFuncInputBinder(t *testing.T) { - testMakeFuncInputBinder(t, testBinderFunc) -} - -func testMakeFuncInputBinder(t *testing.T, binder interface{}) { - b, err := MakeFuncInputBinder(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) - ctxValue := []reflect.Value{reflect.ValueOf(ctx)} - v := b.BindFunc(ctxValue) - 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) - } -} - -func testCheck(t *testing.T, 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) - } -} - -// TestGetBindersForInput 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 TestGetBindersForInput(t *testing.T) { - // binders - var ( - stringBinder = MustMakeFuncInputBinder(func(ctx context.Context) string { - return "a string" - }) - intBinder = MustMakeFuncInputBinder(func(ctx context.Context) int { - return 42 - }) - ) - // in - var ( - stringType = reflect.TypeOf("string") - intType = reflect.TypeOf(1) - ) - - // 1 - testCheck(t, "test1", true, testGetBindersForInput(t, []*InputBinder{intBinder, stringBinder}, - []interface{}{"a string", 42}, stringType, intType)) - availableBinders := []*InputBinder{stringBinder, intBinder} // different order than the fist test. - // 2 - testCheck(t, "test2", true, testGetBindersForInput(t, availableBinders, - []interface{}{"a string", 42}, stringType, intType)) - // 3 - testCheck(t, "test-3-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{42}, stringType, intType)) - // 4 - testCheck(t, "test-4-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{"a string"}, stringType, intType)) - // 5 - testCheck(t, "test-5-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{42, 42}, stringType, intType)) - // 6 - testCheck(t, "test-6-fail", false, testGetBindersForInput(t, availableBinders, - []interface{}{testUserStruct{}}, stringType, intType)) - -} - -func testGetBindersForInput(t *testing.T, binders []*InputBinder, expectingResults []interface{}, in ...reflect.Type) (errString string) { - m := getBindersForInput(binders, in...) - - if expected, got := len(expectingResults), len(m); expected != got { - return fmt.Sprintf("expected results length(%d) and valid binders length(%d) to be equal, so each input has one binder", expected, got) - } - - ctxValue := []reflect.Value{reflect.ValueOf(context.NewContext(nil))} - for idx, expected := range expectingResults { - if m[idx] != nil { - v := m[idx].BindFunc(ctxValue) - 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/controller.go b/mvc2/controller.go index 9d7aad3d84..452e68c890 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -11,7 +11,6 @@ import ( "github.com/kataras/iris/core/router" "github.com/kataras/iris/core/router/macro" "github.com/kataras/iris/core/router/macro/interpreter/ast" - "github.com/kataras/iris/mvc/activator/methodfunc" ) type BaseController interface { @@ -72,38 +71,52 @@ type ControllerActivator struct { Router router.Party initRef BaseController // the BaseController as it's passed from the end-dev. - + Type reflect.Type // raw type of the BaseController (initRef). // FullName it's the last package path segment + "." + the Name. // i.e: if login-example/user/controller.go, the FullName is "user.Controller". FullName string - // key = the method's name. - methods map[string]reflect.Method + // the methods names that is already binded to a handler, + // the BeginRequest, EndRequest and OnActivate are reserved by the internal implementation. + reservedMethods []string + + // input are always empty after the `activate` + // are used to build the bindings, and we need this field + // because we have 3 states (Engine.Input, OnActivate, Bind) + // that we can add or override binding values. + input []reflect.Value - // services []field - // bindServices func(elem reflect.Value) - s services + // the bindings that comes from input (and Engine) and can be binded to the controller's(initRef) fields. + bindings *targetStruct } -func newControllerActivator(engine *Engine, router router.Party, controller BaseController) *ControllerActivator { +var emptyMethod = reflect.Method{} + +func newControllerActivator(router router.Party, controller BaseController, bindValues ...reflect.Value) *ControllerActivator { c := &ControllerActivator{ - Engine: engine, Router: router, initRef: controller, + reservedMethods: []string{ + "BeginRequest", + "EndRequest", + "OnActivate", + }, + // the following will make sure that if + // the controller's has set-ed pointer struct fields by the end-dev + // we will include them to the bindings. + // set bindings to the non-zero pointer fields' values that may be set-ed by + // the end-developer when declaring the controller, + // activate listeners needs them in order to know if something set-ed already or not, + // look `BindTypeExists`. + input: append(lookupNonZeroFieldsValues(reflect.ValueOf(controller)), bindValues...), } c.analyze() return c } -var reservedMethodNames = []string{ - "BeginRequest", - "EndRequest", - "OnActivate", -} - -func isReservedMethod(name string) bool { - for _, s := range reservedMethodNames { +func (c *ControllerActivator) isReservedMethod(name string) bool { + for _, s := range c.reservedMethods { if s == name { return true } @@ -113,55 +126,86 @@ func isReservedMethod(name string) bool { } func (c *ControllerActivator) analyze() { - // set full name. - { - // first instance value, needed to validate - // the actual type of the controller field - // and to collect and save the instance's persistence fields' - // values later on. - val := reflect.Indirect(reflect.ValueOf(c.initRef)) - - ctrlName := val.Type().Name() - pkgPath := val.Type().PkgPath() - fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName - c.FullName = fullName - } - // set all available, exported methods. - { - typ := reflect.TypeOf(c.initRef) // typ, with pointer - n := typ.NumMethod() - c.methods = make(map[string]reflect.Method, n) - for i := 0; i < n; i++ { - m := typ.Method(i) - key := m.Name - - if !isReservedMethod(key) { - c.methods[key] = m - } + // first instance value, needed to validate + // the actual type of the controller field + // and to collect and save the instance's persistence fields' + // values later on. + typ := reflect.TypeOf(c.initRef) // type with pointer + elemTyp := indirectTyp(typ) + + ctrlName := elemTyp.Name() + pkgPath := elemTyp.PkgPath() + fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName + c.FullName = fullName + c.Type = typ + + // register all available, exported methods to handlers if possible. + n := typ.NumMethod() + for i := 0; i < n; i++ { + m := typ.Method(i) + funcName := m.Name + + if c.isReservedMethod(funcName) { + continue } + + httpMethod, httpPath, err := parse(m) + if err != nil && err != errSkip { + err = fmt.Errorf("MVC: fail to parse the path and method for '%s.%s': %v", c.FullName, m.Name, err) + c.Router.GetReporter().AddErr(err) + continue + } + + c.Handle(httpMethod, httpPath, funcName) } - // set field index with matching service binders, if any. - { - // typ := indirectTyp(reflect.TypeOf(c.initRef)) // element's typ. +} + +// SetBindings will override any bindings with the new "values". +func (c *ControllerActivator) SetBindings(values ...reflect.Value) { + // set field index with matching binders, if any. + c.bindings = newTargetStruct(reflect.ValueOf(c.initRef), values...) + c.input = c.input[0:0] +} + +// Bind binds values to this controller, if you want to share +// binding values between controllers use the Engine's `Bind` function instead. +func (c *ControllerActivator) Bind(values ...interface{}) { + for _, val := range values { + if v := reflect.ValueOf(val); goodVal(v) { + c.input = append(c.input, v) + } + } +} - c.s = getServicesFor(reflect.ValueOf(c.initRef), c.Engine.Input) - // c.bindServices = getServicesBinderForStruct(c.Engine.binders, typ) +// BindTypeExists returns true if a binder responsible to +// bind and return a type of "typ" is already registered to this controller. +func (c *ControllerActivator) BindTypeExists(typ reflect.Type) bool { + for _, in := range c.input { + if equalTypes(in.Type(), typ) { + return true + } } + return false +} - c.analyzeAndRegisterMethods() +func (c *ControllerActivator) activate() { + c.SetBindings(c.input...) } +var emptyIn = []reflect.Value{} + func (c *ControllerActivator) Handle(method, path, funcName string, middleware ...context.Handler) error { - if method == "" || path == "" || funcName == "" || isReservedMethod(funcName) { + if method == "" || path == "" || funcName == "" || + c.isReservedMethod(funcName) { // isReservedMethod -> if it's already registered // by a previous Handle or analyze methods internally. return errSkip } - m, ok := c.methods[funcName] + m, ok := c.Type.MethodByName(funcName) if !ok { err := fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", funcName, c.FullName) @@ -176,105 +220,84 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . return err } - fmt.Printf("===============%s.%s==============\n", c.FullName, funcName) - funcIn := getInputArgsFromFunc(m.Type)[1:] // except the receiver, which is the controller pointer itself. + // add this as a reserved method name in order to + // be sure that the same func will not be registered again, even if a custom .Handle later on. + c.reservedMethods = append(c.reservedMethods, funcName) + + // fmt.Printf("===============%s.%s==============\n", c.FullName, funcName) - // get any binders for this func, if any, and - // take param binders, we can bind them because we know the path here. - // binders := joinBindersMap( - // getBindersForInput(c.Engine.binders, funcIn...), - // getPathParamsBindersForInput(tmpl.Params, funcIn...)) + funcIn := getInputArgsFromFunc(m.Type) // except the receiver, which is the controller pointer itself. - s := getServicesFor(m.Func, getPathParamsForInput(tmpl.Params, funcIn...)) - // s.AddSource(indirectVal(reflect.ValueOf(c.initRef)), c.Engine.Input...) + pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...) + funcBindings := newTargetFunc(m.Func, pathParams...) - typ := reflect.TypeOf(c.initRef) - elem := indirectTyp(typ) // the value, not the pointer. - hasInputBinders := len(s) > 0 - hasStructBinders := len(c.s) > 0 - n := len(funcIn) + 1 + elemTyp := indirectTyp(c.Type) // the element value, not the pointer. - // be, _ := typ.MethodByName("BeginRequest") - // en, _ := typ.MethodByName("EndRequest") - // beginIndex, endIndex := be.Index, en.Index + n := len(funcIn) handler := func(ctx context.Context) { // create a new controller instance of that type(>ptr). - ctrl := reflect.New(elem) - //ctrlAndCtxValues := []reflect.Value{ctrl, ctxValue[0]} - // ctrl.MethodByName("BeginRequest").Call(ctxValue) - //begin.Func.Call(ctrlAndCtxValues) + ctrl := reflect.New(elemTyp) b := ctrl.Interface().(BaseController) // the Interface(). is faster than MethodByName or pre-selected methods. // init the request. b.BeginRequest(ctx) - //ctrl.Method(beginIndex).Call(ctxValue) + // if begin request stopped the execution. if ctx.IsStopped() { return } - if hasStructBinders { - elem := ctrl.Elem() - c.s.FillStructStaticValues(elem) - } - - if !hasInputBinders { - methodfunc.DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) + if !c.bindings.Valid && !funcBindings.Valid { + DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) } else { - in := make([]reflect.Value, n, n) - // in[0] = ctrl.Elem() - in[0] = ctrl - s.FillFuncInput([]reflect.Value{reflect.ValueOf(ctx)}, &in) - methodfunc.DispatchFuncResult(ctx, m.Func.Call(in)) - // in := make([]reflect.Value, n, n) - // ctxValues := []reflect.Value{reflect.ValueOf(ctx)} - // for k, v := range binders { - // in[k] = v.BindFunc(ctxValues) - - // if ctx.IsStopped() { - // return - // } - // } - // methodfunc.DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(in)) + ctxValue := reflect.ValueOf(ctx) + + if c.bindings.Valid { + elem := ctrl.Elem() + c.bindings.Fill(elem, ctxValue) + if ctx.IsStopped() { + return + } + + // we do this in order to reduce in := make... + // if not func input binders, we execute the handler with empty input args. + if !funcBindings.Valid { + DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) + } + } + // otherwise, it has one or more valid input binders, + // make the input and call the func using those. + if funcBindings.Valid { + in := make([]reflect.Value, n, n) + in[0] = ctrl + funcBindings.Fill(&in, ctxValue) + if ctx.IsStopped() { + return + } + + DispatchFuncResult(ctx, m.Func.Call(in)) + } + } // end the request, don't check for stopped because this does the actual writing // if no response written already. b.EndRequest(ctx) - // ctrl.MethodByName("EndRequest").Call(ctxValue) - // end.Func.Call(ctrlAndCtxValues) - //ctrl.Method(endIndex).Call(ctxValue) } // register the handler now. - r := c.Router.Handle(method, path, append(middleware, handler)...) - // change the main handler's name in order to respect the controller's and give - // a proper debug message. - r.MainHandlerName = fmt.Sprintf("%s.%s", c.FullName, funcName) - // add this as a reserved method name in order to - // be sure that the same func will not be registered again, even if a custom .Handle later on. - reservedMethodNames = append(reservedMethodNames, funcName) - return nil -} - -func (c *ControllerActivator) analyzeAndRegisterMethods() { - for _, m := range c.methods { - funcName := m.Name - httpMethod, httpPath, err := parse(m) - if err != nil && err != errSkip { - err = fmt.Errorf("MVC: fail to parse the path and method for '%s.%s': %v", c.FullName, m.Name, err) - c.Router.GetReporter().AddErr(err) - continue - } + c.Router.Handle(method, path, append(middleware, handler)...). + // change the main handler's name in order to respect the controller's and give + // a proper debug message. + MainHandlerName = fmt.Sprintf("%s.%s", c.FullName, funcName) - c.Handle(httpMethod, httpPath, funcName) - } + return nil } const ( tokenBy = "By" - tokenWildcard = "Wildcard" // i.e ByWildcard + tokenWildcard = "Wildcard" // "ByWildcard". ) // word lexer, not characters. @@ -393,13 +416,15 @@ func methodTitle(httpMethod string) string { var errSkip = errors.New("skip") +var allMethods = append(router.AllMethods[0:], []string{"ALL", "ANY"}...) + func (p *parser) parse() (method, path string, err error) { funcArgPos := 0 path = "/" // take the first word and check for the method. w := p.lexer.next() - for _, httpMethod := range router.AllMethods { + for _, httpMethod := range allMethods { possibleMethodFuncName := methodTitle(httpMethod) if strings.Index(w, possibleMethodFuncName) == 0 { method = httpMethod @@ -437,9 +462,9 @@ func (p *parser) parse() (method, path string, err error) { continue } - // static path. path += "/" + strings.ToLower(w) + } return diff --git a/mvc2/controller_handler_test.go b/mvc2/controller_handle_test.go similarity index 70% rename from mvc2/controller_handler_test.go rename to mvc2/controller_handle_test.go index 3a48f8b4c1..26b77f17d1 100644 --- a/mvc2/controller_handler_test.go +++ b/mvc2/controller_handle_test.go @@ -1,34 +1,30 @@ package mvc2_test import ( - "fmt" "testing" - "time" "github.com/kataras/iris" "github.com/kataras/iris/httptest" - // "github.com/kataras/iris/mvc" - // "github.com/kataras/iris/mvc/activator/methodfunc" . "github.com/kataras/iris/mvc2" ) -type testController struct { +type testControllerHandle struct { C Service TestService reqField string } -func (c *testController) Get() string { +func (c *testControllerHandle) Get() string { return "index" } -func (c *testController) BeginRequest(ctx iris.Context) { +func (c *testControllerHandle) BeginRequest(ctx iris.Context) { c.C.BeginRequest(ctx) c.reqField = ctx.URLParam("reqfield") } -func (c *testController) OnActivate(t *ControllerActivator) { // OnActivate(t *mvc.TController) { +func (c *testControllerHandle) OnActivate(t *ControllerActivator) { // OnActivate(t *mvc.TController) { // t.Handle("GET", "/", "Get") t.Handle("GET", "/histatic", "HiStatic") t.Handle("GET", "/hiservice", "HiService") @@ -36,31 +32,29 @@ func (c *testController) OnActivate(t *ControllerActivator) { // OnActivate(t *m t.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") } -func (c *testController) HiStatic() string { +func (c *testControllerHandle) HiStatic() string { return c.reqField } -func (c *testController) HiService() string { +func (c *testControllerHandle) HiService() string { return c.Service.Say("hi") } -func (c *testController) HiParamBy(v string) string { +func (c *testControllerHandle) HiParamBy(v string) string { return v } -func (c *testController) HiParamEmptyInputBy() string { +func (c *testControllerHandle) HiParamEmptyInputBy() string { return "empty in but served with ctx.Params.Get('ps')=" + c.Ctx.Params().Get("ps") } -func TestControllerHandler(t *testing.T) { +func TestControllerHandle(t *testing.T) { app := iris.New() - // app.Controller("/", new(testController), &TestServiceImpl{prefix: "service:"}) + m := New() - m.Bind(&TestServiceImpl{prefix: "service:"}).Controller(app, new(testController)) - e := httptest.New(t, app, httptest.LogLevel("debug")) + m.Bind(&TestServiceImpl{prefix: "service:"}).Controller(app, new(testControllerHandle)) + e := httptest.New(t, app) - fmt.Printf("\n\n\n") - now := time.Now() // test the index, is not part of the current package's implementation but do it. e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("index") @@ -84,7 +78,4 @@ func TestControllerHandler(t *testing.T) { Body().Equal("value") e.GET("/hiparamempyinput/value").Expect().Status(httptest.StatusOK). Body().Equal("empty in but served with ctx.Params.Get('ps')=value") - - endTime := time.Now().Sub(now) - fmt.Printf("end at %dns\n", endTime.Nanoseconds()) } diff --git a/mvc2/controller_test.go b/mvc2/controller_test.go new file mode 100644 index 0000000000..05afef307e --- /dev/null +++ b/mvc2/controller_test.go @@ -0,0 +1,446 @@ +// black-box testing +package mvc2_test + +import ( + "reflect" + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router" + "github.com/kataras/iris/httptest" + . "github.com/kataras/iris/mvc2" +) + +type testController struct { + C +} + +var writeMethod = func(c C) { + c.Ctx.Writef(c.Ctx.Method()) +} + +func (c *testController) Get() { + writeMethod(c.C) +} +func (c *testController) Post() { + writeMethod(c.C) +} +func (c *testController) Put() { + writeMethod(c.C) +} +func (c *testController) Delete() { + writeMethod(c.C) +} +func (c *testController) Connect() { + writeMethod(c.C) +} +func (c *testController) Head() { + writeMethod(c.C) +} +func (c *testController) Patch() { + writeMethod(c.C) +} +func (c *testController) Options() { + writeMethod(c.C) +} +func (c *testController) Trace() { + writeMethod(c.C) +} + +type ( + testControllerAll struct{ C } + testControllerAny struct{ C } // exactly the same as All. +) + +func (c *testControllerAll) All() { + writeMethod(c.C) +} + +func (c *testControllerAny) Any() { + writeMethod(c.C) +} + +func TestControllerMethodFuncs(t *testing.T) { + app := iris.New() + + m := New() + m.Controller(app, new(testController)) + m.Controller(app.Party("/all"), new(testControllerAll)) + m.Controller(app.Party("/any"), new(testControllerAny)) + + e := httptest.New(t, app) + for _, method := range router.AllMethods { + + e.Request(method, "/").Expect().Status(iris.StatusOK). + Body().Equal(method) + + e.Request(method, "/all").Expect().Status(iris.StatusOK). + Body().Equal(method) + + e.Request(method, "/any").Expect().Status(iris.StatusOK). + Body().Equal(method) + } +} + +type testControllerBeginAndEndRequestFunc struct { + C + + Username string +} + +// called before of every method (Get() or Post()). +// +// useful when more than one methods using the +// same request values or context's function calls. +func (c *testControllerBeginAndEndRequestFunc) BeginRequest(ctx context.Context) { + c.C.BeginRequest(ctx) + c.Username = ctx.Params().Get("username") +} + +// called after every method (Get() or Post()). +func (c *testControllerBeginAndEndRequestFunc) EndRequest(ctx context.Context) { + ctx.Writef("done") // append "done" to the response + c.C.EndRequest(ctx) +} + +func (c *testControllerBeginAndEndRequestFunc) Get() { + c.Ctx.Writef(c.Username) +} + +func (c *testControllerBeginAndEndRequestFunc) Post() { + c.Ctx.Writef(c.Username) +} + +func TestControllerBeginAndEndRequestFunc(t *testing.T) { + app := iris.New() + New().Controller(app.Party("/profile/{username}"), new(testControllerBeginAndEndRequestFunc)) + + e := httptest.New(t, app) + usernames := []string{ + "kataras", + "makis", + "efi", + "rg", + "bill", + "whoisyourdaddy", + } + doneResponse := "done" + + for _, username := range usernames { + e.GET("/profile/" + username).Expect().Status(iris.StatusOK). + Body().Equal(username + doneResponse) + e.POST("/profile/" + username).Expect().Status(iris.StatusOK). + Body().Equal(username + doneResponse) + } +} + +func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) { + app := iris.New() + usernames := map[string]bool{ + "kataras": true, + "makis": false, + "efi": true, + "rg": false, + "bill": true, + "whoisyourdaddy": false, + } + middlewareCheck := func(ctx context.Context) { + for username, allow := range usernames { + if ctx.Params().Get("username") == username && allow { + ctx.Next() + return + } + } + + ctx.StatusCode(iris.StatusForbidden) + ctx.Writef("forbidden") + } + + New().Controller(app.Party("/profile/{username}", middlewareCheck), + new(testControllerBeginAndEndRequestFunc)) + + e := httptest.New(t, app) + + doneResponse := "done" + + for username, allow := range usernames { + getEx := e.GET("/profile/" + username).Expect() + if allow { + getEx.Status(iris.StatusOK). + Body().Equal(username + doneResponse) + } else { + getEx.Status(iris.StatusForbidden).Body().Equal("forbidden") + } + + postEx := e.POST("/profile/" + username).Expect() + if allow { + postEx.Status(iris.StatusOK). + Body().Equal(username + doneResponse) + } else { + postEx.Status(iris.StatusForbidden).Body().Equal("forbidden") + } + } +} + +type Model struct { + Username string +} + +type testControllerEndRequestAwareness struct { + C +} + +func (c *testControllerEndRequestAwareness) Get() { + username := c.Ctx.Params().Get("username") + c.Ctx.Values().Set(c.Ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(), + map[string]interface{}{ + "TestModel": Model{Username: username}, + "myModel": Model{Username: username + "2"}, + }) +} + +func writeModels(ctx context.Context, names ...string) { + if expected, got := len(names), len(ctx.GetViewData()); expected != got { + ctx.Writef("expected view data length: %d but got: %d for names: %s", expected, got, names) + return + } + + for _, name := range names { + + m, ok := ctx.GetViewData()[name] + if !ok { + ctx.Writef("fail load and set the %s", name) + return + } + + model, ok := m.(Model) + if !ok { + ctx.Writef("fail to override the %s' name by the tag", name) + return + } + + ctx.Writef(model.Username) + } +} + +func (c *testControllerEndRequestAwareness) EndRequest(ctx context.Context) { + writeModels(ctx, "TestModel", "myModel") + c.C.EndRequest(ctx) +} + +func TestControllerEndRequestAwareness(t *testing.T) { + app := iris.New() + New().Controller(app.Party("/era/{username}"), new(testControllerEndRequestAwareness)) + + e := httptest.New(t, app) + usernames := []string{ + "kataras", + "makis", + } + + for _, username := range usernames { + e.GET("/era/" + username).Expect().Status(iris.StatusOK). + Body().Equal(username + username + "2") + } +} + +type testBindType struct { + title string +} + +type testControllerBindStruct struct { + C + // should start with upper letter of course + TitlePointer *testBindType // should have the value of the "myTitlePtr" on test + TitleValue testBindType // should have the value of the "myTitleV" on test + Other string // just another type to check the field collection, should be empty +} + +func (t *testControllerBindStruct) Get() { + t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) +} + +type testControllerBindDeep struct { + testControllerBindStruct +} + +func (t *testControllerBindDeep) Get() { + // t.testControllerBindStruct.Get() + t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) +} +func TestControllerBind(t *testing.T) { + app := iris.New() + // app.Logger().SetLevel("debug") + + t1, t2 := "my pointer title", "val title" + // test bind pointer to pointer of the correct type + myTitlePtr := &testBindType{title: t1} + // test bind value to value of the correct type + myTitleV := testBindType{title: t2} + m := New() + m.Bind(myTitlePtr, myTitleV) + // or just app + m.Controller(app.Party("/"), new(testControllerBindStruct)) + m.Controller(app.Party("/deep"), new(testControllerBindDeep)) + + e := httptest.New(t, app) + expected := t1 + t2 + e.GET("/").Expect().Status(iris.StatusOK). + Body().Equal(expected) + e.GET("/deep").Expect().Status(iris.StatusOK). + Body().Equal(expected) +} + +type testCtrl0 struct { + testCtrl00 +} + +func (c *testCtrl0) Get() string { + return c.Ctx.Params().Get("username") +} + +func (c *testCtrl0) EndRequest(ctx context.Context) { + if c.TitlePointer == nil { + ctx.Writef("\nTitlePointer is nil!\n") + } else { + ctx.Writef(c.TitlePointer.title) + } + + //should be the same as `.testCtrl000.testCtrl0000.EndRequest(ctx)` + c.testCtrl00.EndRequest(ctx) +} + +type testCtrl00 struct { + testCtrl000 +} + +type testCtrl000 struct { + testCtrl0000 + + TitlePointer *testBindType +} + +type testCtrl0000 struct { + C +} + +func (c *testCtrl0000) EndRequest(ctx context.Context) { + ctx.Writef("finish") +} + +func TestControllerInsideControllerRecursively(t *testing.T) { + var ( + username = "gerasimos" + title = "mytitle" + expected = username + title + "finish" + ) + + app := iris.New() + New().Bind(&testBindType{title: title}). + Controller(app.Party("/user/{username}"), new(testCtrl0)) + + e := httptest.New(t, app) + e.GET("/user/" + username).Expect(). + Status(iris.StatusOK).Body().Equal(expected) +} + +type testControllerRelPathFromFunc struct{ C } + +func (c *testControllerRelPathFromFunc) EndRequest(ctx context.Context) { + ctx.Writef("%s:%s", ctx.Method(), ctx.Path()) + c.C.EndRequest(ctx) +} + +func (c *testControllerRelPathFromFunc) Get() {} +func (c *testControllerRelPathFromFunc) GetBy(int64) {} +func (c *testControllerRelPathFromFunc) GetAnythingByWildcard(string) {} + +func (c *testControllerRelPathFromFunc) GetLogin() {} +func (c *testControllerRelPathFromFunc) PostLogin() {} + +func (c *testControllerRelPathFromFunc) GetAdminLogin() {} + +func (c *testControllerRelPathFromFunc) PutSomethingIntoThis() {} + +func (c *testControllerRelPathFromFunc) GetSomethingBy(bool) {} +func (c *testControllerRelPathFromFunc) GetSomethingByBy(string, int) {} +func (c *testControllerRelPathFromFunc) GetSomethingNewBy(string, int) {} // two input arguments, one By which is the latest word. +func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} // two input arguments + +func TestControllerRelPathFromFunc(t *testing.T) { + app := iris.New() + New().Controller(app, new(testControllerRelPathFromFunc)) + + e := httptest.New(t, app) + e.GET("/").Expect().Status(iris.StatusOK). + Body().Equal("GET:/") + + e.GET("/42").Expect().Status(iris.StatusOK). + Body().Equal("GET:/42") + e.GET("/something/true").Expect().Status(iris.StatusOK). + Body().Equal("GET:/something/true") + e.GET("/something/false").Expect().Status(iris.StatusOK). + Body().Equal("GET:/something/false") + e.GET("/something/truee").Expect().Status(iris.StatusNotFound) + e.GET("/something/falsee").Expect().Status(iris.StatusNotFound) + e.GET("/something/kataras/42").Expect().Status(iris.StatusOK). + Body().Equal("GET:/something/kataras/42") + e.GET("/something/new/kataras/42").Expect().Status(iris.StatusOK). + Body().Equal("GET:/something/new/kataras/42") + e.GET("/something/true/else/this/42").Expect().Status(iris.StatusOK). + Body().Equal("GET:/something/true/else/this/42") + + e.GET("/login").Expect().Status(iris.StatusOK). + Body().Equal("GET:/login") + e.POST("/login").Expect().Status(iris.StatusOK). + Body().Equal("POST:/login") + e.GET("/admin/login").Expect().Status(iris.StatusOK). + Body().Equal("GET:/admin/login") + e.PUT("/something/into/this").Expect().Status(iris.StatusOK). + Body().Equal("PUT:/something/into/this") + e.GET("/42").Expect().Status(iris.StatusOK). + Body().Equal("GET:/42") + e.GET("/anything/here").Expect().Status(iris.StatusOK). + Body().Equal("GET:/anything/here") +} + +type testControllerActivateListener struct { + C + + TitlePointer *testBindType +} + +func (c *testControllerActivateListener) OnActivate(ca *ControllerActivator) { + if !ca.BindTypeExists(reflect.TypeOf(&testBindType{})) { + ca.Bind(&testBindType{ + title: "default title", + }) + } +} + +func (c *testControllerActivateListener) Get() string { + return c.TitlePointer.title +} + +func TestControllerActivateListener(t *testing.T) { + app := iris.New() + New().Controller(app, new(testControllerActivateListener)) + New().Bind(&testBindType{ // will bind to all controllers under this .New() MVC Engine. + title: "my title", + }).Controller(app.Party("/manual"), new(testControllerActivateListener)) + // or + New().Controller(app.Party("/manual2"), &testControllerActivateListener{ + TitlePointer: &testBindType{ + title: "my title", + }, + }) + + e := httptest.New(t, app) + e.GET("/").Expect().Status(iris.StatusOK). + Body().Equal("default title") + e.GET("/manual").Expect().Status(iris.StatusOK). + Body().Equal("my title") + e.GET("/manual2").Expect().Status(iris.StatusOK). + Body().Equal("my title") +} diff --git a/mvc2/engine.go b/mvc2/engine.go index f40816e010..f421b26fc9 100644 --- a/mvc2/engine.go +++ b/mvc2/engine.go @@ -4,6 +4,7 @@ import ( "errors" "reflect" + "github.com/kataras/golog" "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" ) @@ -15,8 +16,6 @@ var ( ) type Engine struct { - binders []*InputBinder - Input []reflect.Value } @@ -24,80 +23,53 @@ func New() *Engine { return new(Engine) } +func (e *Engine) Bind(values ...interface{}) *Engine { + for _, val := range values { + if v := reflect.ValueOf(val); goodVal(v) { + e.Input = append(e.Input, v) + } + } + + return e +} + func (e *Engine) Child() *Engine { child := New() // copy the current parent's ctx func binders and services to this new child. - // if l := len(e.binders); l > 0 { - // binders := make([]*InputBinder, l, l) - // copy(binders, e.binders) - // child.binders = binders - // } if l := len(e.Input); l > 0 { input := make([]reflect.Value, l, l) copy(input, e.Input) child.Input = input } - return child -} - -func (e *Engine) Bind(binders ...interface{}) *Engine { - for _, binder := range binders { - // typ := resolveBinderType(binder) - - // var ( - // b *InputBinder - // err error - // ) - - // if typ == functionType { - // b, err = MakeFuncInputBinder(binder) - // } else if typ == serviceType { - // b, err = MakeServiceInputBinder(binder) - // } else { - // err = errBad - // } - - // if err != nil { - // continue - // } - - // e.binders = append(e.binders, b) - e.Input = append(e.Input, reflect.ValueOf(binder)) - } - - return e -} - -// BindTypeExists returns true if a binder responsible to -// bind and return a type of "typ" is already registered. -func (e *Engine) BindTypeExists(typ reflect.Type) bool { - // for _, b := range e.binders { - // if equalTypes(b.BindType, typ) { - // return true - // } - // } - for _, in := range e.Input { - if equalTypes(in.Type(), typ) { - return true - } - } - return false + return child } func (e *Engine) Handler(handler interface{}) context.Handler { - h, _ := MakeHandler(handler, e.binders) // it logs errors already, so on any error the "h" will be nil. + h, err := MakeHandler(handler, e.Input...) + if err != nil { + golog.Errorf("mvc handler: %v", err) + } return h } -type ActivateListener interface { - OnActivate(*ControllerActivator) -} +func (e *Engine) Controller(router router.Party, controller BaseController, onActivate ...func(*ControllerActivator)) { + ca := newControllerActivator(router, controller, e.Input...) -func (e *Engine) Controller(router router.Party, controller BaseController) { - ca := newControllerActivator(e, router, controller) - if al, ok := controller.(ActivateListener); ok { - al.OnActivate(ca) + // give a priority to the "onActivate" + // callbacks, if any. + for _, cb := range onActivate { + cb(ca) } + + // check if controller has an "OnActivate" function + // which accepts the controller activator and call it. + if activateListener, ok := controller.(interface { + OnActivate(*ControllerActivator) + }); ok { + activateListener.OnActivate(ca) + } + + ca.activate() } diff --git a/mvc2/mvc_test.go b/mvc2/engine_handler_test.go similarity index 89% rename from mvc2/mvc_test.go rename to mvc2/engine_handler_test.go index e33f11b0f2..547de0c914 100644 --- a/mvc2/mvc_test.go +++ b/mvc2/engine_handler_test.go @@ -8,7 +8,7 @@ import ( . "github.com/kataras/iris/mvc2" ) -func TestMvcInAndHandler(t *testing.T) { +func TestMvcEngineInAndHandler(t *testing.T) { m := New().Bind(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) var ( diff --git a/mvc2/handler_out.go b/mvc2/func_result.go similarity index 97% rename from mvc2/handler_out.go rename to mvc2/func_result.go index eb7999ad4d..d03319495c 100644 --- a/mvc2/handler_out.go +++ b/mvc2/func_result.go @@ -394,18 +394,18 @@ func (r View) Dispatch(ctx context.Context) { // r as Response view. // In order to respect any c.Ctx.ViewData that may called manually before; dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey() if ctx.Values().Get(dataKey) == nil { - // if no c.Ctx.ViewData then it's empty do a - // pure set, it's faster. + // if no c.Ctx.ViewData set-ed before (the most common scenario) then do a + // simple set, it's faster. ctx.Values().Set(dataKey, r.Data) } else { // else check if r.Data is map or struct, if struct convert it to map, - // do a range loop and set the data one by one. - // context.Map is actually a map[string]interface{} but we have to make that check; + // do a range loop and modify the data one by one. + // context.Map is actually a map[string]interface{} but we have to make that check: if m, ok := r.Data.(map[string]interface{}); ok { setViewData(ctx, m) } else if m, ok := r.Data.(context.Map); ok { setViewData(ctx, m) - } else if structs.IsStruct(r.Data) { + } else if indirectVal(reflect.ValueOf(r.Data)).Kind() == reflect.Struct { setViewData(ctx, structs.Map(r)) } } diff --git a/mvc2/func_result_test.go b/mvc2/func_result_test.go new file mode 100644 index 0000000000..5df3b07224 --- /dev/null +++ b/mvc2/func_result_test.go @@ -0,0 +1,275 @@ +package mvc2_test + +import ( + "errors" + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/httptest" + . "github.com/kataras/iris/mvc2" +) + +// activator/methodfunc/func_caller.go. +// and activator/methodfunc/func_result_dispatcher.go + +type testControllerMethodResult struct { + C +} + +func (c *testControllerMethodResult) Get() Result { + return Response{ + Text: "Hello World!", + } +} + +func (c *testControllerMethodResult) GetWithStatus() Response { // or Result again, no problem. + return Response{ + Text: "This page doesn't exist", + Code: iris.StatusNotFound, + } +} + +type testCustomStruct struct { + Name string `json:"name" xml:"name"` + Age int `json:"age" xml:"age"` +} + +func (c *testControllerMethodResult) GetJson() Result { + var err error + if c.Ctx.URLParamExists("err") { + err = errors.New("error here") + } + return Response{ + Err: err, // if err != nil then it will fire the error's text with a BadRequest. + Object: testCustomStruct{Name: "Iris", Age: 2}, + } +} + +var things = []string{"thing 0", "thing 1", "thing 2"} + +func (c *testControllerMethodResult) GetThingWithTryBy(index int) Result { + failure := Response{ + Text: "thing does not exist", + Code: iris.StatusNotFound, + } + + return Try(func() Result { + // if panic because of index exceed the slice + // then the "failure" response will be returned instead. + return Response{Text: things[index]} + }, failure) +} + +func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) Result { + return Try(func() Result { + // if panic because of index exceed the slice + // then the default failure response will be returned instead (400 bad request). + return Response{Text: things[index]} + }) +} + +func TestControllerMethodResult(t *testing.T) { + app := iris.New() + New().Controller(app, new(testControllerMethodResult)) + + e := httptest.New(t, app) + + e.GET("/").Expect().Status(iris.StatusOK). + Body().Equal("Hello World!") + + e.GET("/with/status").Expect().Status(iris.StatusNotFound). + Body().Equal("This page doesn't exist") + + e.GET("/json").Expect().Status(iris.StatusOK). + JSON().Equal(iris.Map{ + "name": "Iris", + "age": 2, + }) + + e.GET("/json").WithQuery("err", true).Expect(). + Status(iris.StatusBadRequest). + Body().Equal("error here") + + e.GET("/thing/with/try/1").Expect(). + Status(iris.StatusOK). + Body().Equal("thing 1") + // failure because of index exceed the slice + e.GET("/thing/with/try/3").Expect(). + Status(iris.StatusNotFound). + Body().Equal("thing does not exist") + + e.GET("/thing/with/try/default/3").Expect(). + Status(iris.StatusBadRequest). + Body().Equal("Bad Request") +} + +type testControllerMethodResultTypes struct { + C +} + +func (c *testControllerMethodResultTypes) GetText() string { + return "text" +} + +func (c *testControllerMethodResultTypes) GetStatus() int { + return iris.StatusBadGateway +} + +func (c *testControllerMethodResultTypes) GetTextWithStatusOk() (string, int) { + return "OK", iris.StatusOK +} + +// tests should have output arguments mixed +func (c *testControllerMethodResultTypes) GetStatusWithTextNotOkBy(first string, second string) (int, string) { + return iris.StatusForbidden, "NOT_OK_" + first + second +} + +func (c *testControllerMethodResultTypes) GetTextAndContentType() (string, string) { + return "text", "text/html" +} + +type testControllerMethodCustomResult struct { + HTML string +} + +// The only one required function to make that a custom Response dispatcher. +func (r testControllerMethodCustomResult) Dispatch(ctx context.Context) { + ctx.HTML(r.HTML) +} + +func (c *testControllerMethodResultTypes) GetCustomResponse() testControllerMethodCustomResult { + return testControllerMethodCustomResult{"text"} +} + +func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) { + return testControllerMethodCustomResult{"OK"}, iris.StatusOK +} + +func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) { + return testControllerMethodCustomResult{"internal server error"}, iris.StatusInternalServerError +} + +func (c *testControllerMethodResultTypes) GetCustomStruct() testCustomStruct { + return testCustomStruct{"Iris", 2} +} + +func (c *testControllerMethodResultTypes) GetCustomStructWithStatusNotOk() (testCustomStruct, int) { + return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError +} + +func (c *testControllerMethodResultTypes) GetCustomStructWithContentType() (testCustomStruct, string) { + return testCustomStruct{"Iris", 2}, "text/xml" +} + +func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCustomStruct, err error) { + s = testCustomStruct{"Iris", 2} + if c.Ctx.URLParamExists("err") { + err = errors.New("omit return of testCustomStruct and fire error") + } + + // it should send the testCustomStruct as JSON if error is nil + // otherwise it should fire the default error(BadRequest) with the error's text. + return +} + +func TestControllerMethodResultTypes(t *testing.T) { + app := iris.New() + New().Controller(app, new(testControllerMethodResultTypes)) + + e := httptest.New(t, app, httptest.LogLevel("debug")) + + e.GET("/text").Expect().Status(iris.StatusOK). + Body().Equal("text") + + e.GET("/status").Expect().Status(iris.StatusBadGateway) + + e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK). + Body().Equal("OK") + + e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden). + Body().Equal("NOT_OK_firstsecond") + // Author's note: <-- if that fails means that the last binder called for both input args, + // see path_param_binder.go + + e.GET("/text/and/content/type").Expect().Status(iris.StatusOK). + ContentType("text/html", "utf-8"). + Body().Equal("text") + + e.GET("/custom/response").Expect().Status(iris.StatusOK). + ContentType("text/html", "utf-8"). + Body().Equal("text") + e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK). + ContentType("text/html", "utf-8"). + Body().Equal("OK") + e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). + ContentType("text/html", "utf-8"). + Body().Equal("internal server error") + + expectedResultFromCustomStruct := map[string]interface{}{ + "name": "Iris", + "age": 2, + } + e.GET("/custom/struct").Expect().Status(iris.StatusOK). + JSON().Equal(expectedResultFromCustomStruct) + e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). + JSON().Equal(expectedResultFromCustomStruct) + e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK). + ContentType("text/xml", "utf-8") + e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK). + JSON().Equal(expectedResultFromCustomStruct) + e.GET("/custom/struct/with/error").WithQuery("err", true).Expect(). + Status(iris.StatusBadRequest). // the default status code if error is not nil + // the content should be not JSON it should be the status code's text + // it will fire the error's text + Body().Equal("omit return of testCustomStruct and fire error") +} + +type testControllerViewResultRespectCtxViewData struct { + T *testing.T + C +} + +func (t *testControllerViewResultRespectCtxViewData) BeginRequest(ctx context.Context) { + t.C.BeginRequest(ctx) + ctx.ViewData("name_begin", "iris_begin") +} + +func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx context.Context) { + t.C.EndRequest(ctx) + // check if data is not overridden by return View {Data: context.Map...} + + dataWritten := ctx.GetViewData() + if dataWritten == nil { + t.T.Fatalf("view data is nil, both BeginRequest and Get failed to write the data") + return + } + + if dataWritten["name_begin"] == nil { + t.T.Fatalf(`view data[name_begin] is nil, + BeginRequest's ctx.ViewData call have been overridden by Get's return View {Data: }. + Total view data: %v`, dataWritten) + } + + if dataWritten["name"] == nil { + t.T.Fatalf("view data[name] is nil, Get's return View {Data: } didn't work. Total view data: %v", dataWritten) + } +} + +func (t *testControllerViewResultRespectCtxViewData) Get() Result { + return View{ + Name: "doesnt_exists.html", + Data: context.Map{"name": "iris"}, // we care about this only. + Code: iris.StatusInternalServerError, + } +} + +func TestControllerViewResultRespectCtxViewData(t *testing.T) { + app := iris.New() + New().Controller(app, new(testControllerViewResultRespectCtxViewData), func(ca *ControllerActivator) { + ca.Bind(t) + }) + e := httptest.New(t, app) + + e.GET("/").Expect().Status(iris.StatusInternalServerError) +} diff --git a/mvc2/handler.go b/mvc2/handler.go index 85be1d523c..e35ab7bdb7 100644 --- a/mvc2/handler.go +++ b/mvc2/handler.go @@ -3,10 +3,10 @@ package mvc2 import ( "fmt" "reflect" + "runtime" "github.com/kataras/golog" "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc/activator/methodfunc" ) // checks if "handler" is context.Handler; func(context.Context). @@ -28,18 +28,13 @@ func validateHandler(handler interface{}) error { return nil } -var ( - contextTyp = reflect.TypeOf(context.NewContext(nil)) - emptyIn = []reflect.Value{} -) - -// MustMakeHandler calls the `MakeHandler` and returns its first resultthe low-level handler), see its docs. -// It panics on error. -func MustMakeHandler(handler interface{}, binders ...interface{}) context.Handler { - h, err := MakeHandler(handler, binders...) +// MustMakeHandler calls the `MakeHandler` and panics on any error. +func MustMakeHandler(handler interface{}, bindValues ...reflect.Value) context.Handler { + h, err := MakeHandler(handler, bindValues...) if err != nil { panic(err) } + return h } @@ -48,9 +43,8 @@ func MustMakeHandler(handler interface{}, binders ...interface{}) context.Handle // custom structs, Result(View | Response) and anything that you already know that mvc implementation supports, // and returns a low-level `context/iris.Handler` which can be used anywhere in the Iris Application, // as middleware or as simple route handler or party handler or subdomain handler-router. -func MakeHandler(handler interface{}, binders ...interface{}) (context.Handler, error) { +func MakeHandler(handler interface{}, bindValues ...reflect.Value) (context.Handler, error) { if err := validateHandler(handler); err != nil { - golog.Errorf("mvc handler: %v", err) return nil, err } @@ -59,71 +53,39 @@ func MakeHandler(handler interface{}, binders ...interface{}) (context.Handler, return h, nil } - inputBinders := make([]reflect.Value, len(binders), len(binders)) - - for i := range binders { - inputBinders[i] = reflect.ValueOf(binders[i]) - } + fn := reflect.ValueOf(handler) + n := fn.Type().NumIn() - return makeHandler(reflect.ValueOf(handler), inputBinders), nil + if n == 0 { + h := func(ctx context.Context) { + DispatchFuncResult(ctx, fn.Call(emptyIn)) + } - // typ := indirectTyp(reflect.TypeOf(handler)) - // n := typ.NumIn() - // typIn := make([]reflect.Type, n, n) - // for i := 0; i < n; i++ { - // typIn[i] = typ.In(i) - // } + return h, nil + } - // m := getBindersForInput(binders, typIn...) - // if len(m) != n { - // err := fmt.Errorf("input arguments length(%d) of types(%s) and valid binders length(%d) are not equal", n, typIn, len(m)) - // golog.Errorf("mvc handler: %v", err) - // return nil, err - // } + s := newTargetFunc(fn, bindValues...) + if !s.Valid { + pc := fn.Pointer() + fpc := runtime.FuncForPC(pc) + callerFileName, callerLineNumber := fpc.FileLine(pc) + callerName := fpc.Name() - // return makeHandler(reflect.ValueOf(handler), m), nil -} + err := fmt.Errorf("input arguments length(%d) and valid binders length(%d) are not equal for typeof '%s' which is defined at %s:%d by %s", + n, len(s.Inputs), fn.Type().String(), callerFileName, callerLineNumber, callerName) + return nil, err + } -func makeHandler(fn reflect.Value, inputBinders []reflect.Value) context.Handler { - inLen := fn.Type().NumIn() + h := func(ctx context.Context) { + in := make([]reflect.Value, n, n) - if inLen == 0 { - return func(ctx context.Context) { - methodfunc.DispatchFuncResult(ctx, fn.Call(emptyIn)) + s.Fill(&in, reflect.ValueOf(ctx)) + if ctx.IsStopped() { + return } + DispatchFuncResult(ctx, fn.Call(in)) } - s := getServicesFor(fn, inputBinders) - if len(s) == 0 { - golog.Errorf("mvc handler: input arguments length(%d) and valid binders length(%d) are not equal", inLen, len(s)) - return nil - } + return h, nil - n := fn.Type().NumIn() - // contextIndex := -1 - // if n > 0 { - // if isContext(fn.Type().In(0)) { - // contextIndex = 0 - // } - // } - return func(ctx context.Context) { - ctxValue := []reflect.Value{reflect.ValueOf(ctx)} - - in := make([]reflect.Value, n, n) - // if contextIndex >= 0 { - // in[contextIndex] = ctxValue[0] - // } - // ctxValues := []reflect.Value{reflect.ValueOf(ctx)} - // for k, v := range m { - // in[k] = v.BindFunc(ctxValues) - // if ctx.IsStopped() { - // return - // } - // } - // methodfunc.DispatchFuncResult(ctx, fn.Call(in)) - - s.FillFuncInput(ctxValue, &in) - - methodfunc.DispatchFuncResult(ctx, fn.Call(in)) - } } diff --git a/mvc2/handler_out_test.go b/mvc2/handler_out_test.go deleted file mode 100644 index bf5abda300..0000000000 --- a/mvc2/handler_out_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package mvc2_test - -// import ( -// "errors" -// "testing" - -// "github.com/kataras/iris" -// "github.com/kataras/iris/context" -// "github.com/kataras/iris/httptest" -// "github.com/kataras/iris/mvc2" -// ) - -// // activator/methodfunc/func_caller.go. -// // and activator/methodfunc/func_result_dispatcher.go - -// type testControllerMethodResult struct { -// mvc2.C -// } - -// func (c *testControllerMethodResult) Get() mvc2.Result { -// return mvc2.Response{ -// Text: "Hello World!", -// } -// } - -// func (c *testControllerMethodResult) GetWithStatus() mvc2.Response { // or mvc.Result again, no problem. -// return mvc2.Response{ -// Text: "This page doesn't exist", -// Code: iris.StatusNotFound, -// } -// } - -// type testCustomStruct struct { -// Name string `json:"name" xml:"name"` -// Age int `json:"age" xml:"age"` -// } - -// func (c *testControllerMethodResult) GetJson() mvc2.Result { -// var err error -// if c.Ctx.URLParamExists("err") { -// err = errors.New("error here") -// } -// return mvc2.Response{ -// Err: err, // if err != nil then it will fire the error's text with a BadRequest. -// Object: testCustomStruct{Name: "Iris", Age: 2}, -// } -// } - -// var things = []string{"thing 0", "thing 1", "thing 2"} - -// func (c *testControllerMethodResult) GetThingWithTryBy(index int) mvc2.Result { -// failure := mvc2.Response{ -// Text: "thing does not exist", -// Code: iris.StatusNotFound, -// } - -// return mvc2.Try(func() mvc2.Result { -// // if panic because of index exceed the slice -// // then the "failure" response will be returned instead. -// return mvc2.Response{Text: things[index]} -// }, failure) -// } - -// func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) mvc2.Result { -// return mvc2.Try(func() mvc2.Result { -// // if panic because of index exceed the slice -// // then the default failure response will be returned instead (400 bad request). -// return mvc2.Response{Text: things[index]} -// }) -// } - -// func TestControllerMethodResult(t *testing.T) { -// app := iris.New() -// app.Controller("/", new(testControllerMethodResult)) - -// e := httptest.New(t, app) - -// e.GET("/").Expect().Status(iris.StatusOK). -// Body().Equal("Hello World!") - -// e.GET("/with/status").Expect().Status(iris.StatusNotFound). -// Body().Equal("This page doesn't exist") - -// e.GET("/json").Expect().Status(iris.StatusOK). -// JSON().Equal(iris.Map{ -// "name": "Iris", -// "age": 2, -// }) - -// e.GET("/json").WithQuery("err", true).Expect(). -// Status(iris.StatusBadRequest). -// Body().Equal("error here") - -// e.GET("/thing/with/try/1").Expect(). -// Status(iris.StatusOK). -// Body().Equal("thing 1") -// // failure because of index exceed the slice -// e.GET("/thing/with/try/3").Expect(). -// Status(iris.StatusNotFound). -// Body().Equal("thing does not exist") - -// e.GET("/thing/with/try/default/3").Expect(). -// Status(iris.StatusBadRequest). -// Body().Equal("Bad Request") -// } - -// type testControllerMethodResultTypes struct { -// mvc2.C -// } - -// func (c *testControllerMethodResultTypes) GetText() string { -// return "text" -// } - -// func (c *testControllerMethodResultTypes) GetStatus() int { -// return iris.StatusBadGateway -// } - -// func (c *testControllerMethodResultTypes) GetTextWithStatusOk() (string, int) { -// return "OK", iris.StatusOK -// } - -// // tests should have output arguments mixed -// func (c *testControllerMethodResultTypes) GetStatusWithTextNotOkBy(first string, second string) (int, string) { -// return iris.StatusForbidden, "NOT_OK_" + first + second -// } - -// func (c *testControllerMethodResultTypes) GetTextAndContentType() (string, string) { -// return "text", "text/html" -// } - -// type testControllerMethodCustomResult struct { -// HTML string -// } - -// // The only one required function to make that a custom Response dispatcher. -// func (r testControllerMethodCustomResult) Dispatch(ctx context.Context) { -// ctx.HTML(r.HTML) -// } - -// func (c *testControllerMethodResultTypes) GetCustomResponse() testControllerMethodCustomResult { -// return testControllerMethodCustomResult{"text"} -// } - -// func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) { -// return testControllerMethodCustomResult{"OK"}, iris.StatusOK -// } - -// func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) { -// return testControllerMethodCustomResult{"internal server error"}, iris.StatusInternalServerError -// } - -// func (c *testControllerMethodResultTypes) GetCustomStruct() testCustomStruct { -// return testCustomStruct{"Iris", 2} -// } - -// func (c *testControllerMethodResultTypes) GetCustomStructWithStatusNotOk() (testCustomStruct, int) { -// return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError -// } - -// func (c *testControllerMethodResultTypes) GetCustomStructWithContentType() (testCustomStruct, string) { -// return testCustomStruct{"Iris", 2}, "text/xml" -// } - -// func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCustomStruct, err error) { -// s = testCustomStruct{"Iris", 2} -// if c.Ctx.URLParamExists("err") { -// err = errors.New("omit return of testCustomStruct and fire error") -// } - -// // it should send the testCustomStruct as JSON if error is nil -// // otherwise it should fire the default error(BadRequest) with the error's text. -// return -// } - -// func TestControllerMethodResultTypes(t *testing.T) { -// app := iris.New() -// app.Controller("/", new(testControllerMethodResultTypes)) - -// e := httptest.New(t, app) - -// e.GET("/text").Expect().Status(iris.StatusOK). -// Body().Equal("text") - -// e.GET("/status").Expect().Status(iris.StatusBadGateway) - -// e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK). -// Body().Equal("OK") - -// e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden). -// Body().Equal("NOT_OK_firstsecond") - -// e.GET("/text/and/content/type").Expect().Status(iris.StatusOK). -// ContentType("text/html", "utf-8"). -// Body().Equal("text") - -// e.GET("/custom/response").Expect().Status(iris.StatusOK). -// ContentType("text/html", "utf-8"). -// Body().Equal("text") -// e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK). -// ContentType("text/html", "utf-8"). -// Body().Equal("OK") -// e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). -// ContentType("text/html", "utf-8"). -// Body().Equal("internal server error") - -// expectedResultFromCustomStruct := map[string]interface{}{ -// "name": "Iris", -// "age": 2, -// } -// e.GET("/custom/struct").Expect().Status(iris.StatusOK). -// JSON().Equal(expectedResultFromCustomStruct) -// e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). -// JSON().Equal(expectedResultFromCustomStruct) -// e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK). -// ContentType("text/xml", "utf-8") -// e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK). -// JSON().Equal(expectedResultFromCustomStruct) -// e.GET("/custom/struct/with/error").WithQuery("err", true).Expect(). -// Status(iris.StatusBadRequest). // the default status code if error is not nil -// // the content should be not JSON it should be the status code's text -// // it will fire the error's text -// Body().Equal("omit return of testCustomStruct and fire error") -// } - -// type testControllerViewResultRespectCtxViewData struct { -// T *testing.T -// mvc2.C -// } - -// func (t *testControllerViewResultRespectCtxViewData) BeginRequest(ctx context.Context) { -// t.C.BeginRequest(ctx) -// ctx.ViewData("name_begin", "iris_begin") -// } - -// func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx context.Context) { -// t.C.EndRequest(ctx) -// // check if data is not overridden by return mvc.View {Data: context.Map...} - -// dataWritten := ctx.GetViewData() -// if dataWritten == nil { -// t.T.Fatalf("view data is nil, both BeginRequest and Get failed to write the data") -// return -// } - -// if dataWritten["name_begin"] == nil { -// t.T.Fatalf(`view data[name_begin] is nil, -// BeginRequest's ctx.ViewData call have been overridden by Get's return mvc.View {Data: }. -// Total view data: %v`, dataWritten) -// } - -// if dataWritten["name"] == nil { -// t.T.Fatalf("view data[name] is nil, Get's return mvc.View {Data: } didn't work. Total view data: %v", dataWritten) -// } -// } - -// func (t *testControllerViewResultRespectCtxViewData) Get() mvc2.Result { -// return mvc2.View{ -// Name: "doesnt_exists.html", -// Data: context.Map{"name": "iris"}, // we care about this only. -// Code: iris.StatusInternalServerError, -// } -// } - -// func TestControllerViewResultRespectCtxViewData(t *testing.T) { -// app := iris.New() -// app.Controller("/", new(testControllerViewResultRespectCtxViewData), t) -// e := httptest.New(t, app) - -// e.GET("/").Expect().Status(iris.StatusInternalServerError) -// } diff --git a/mvc2/handler_test.go b/mvc2/handler_test.go index 358a38c1d5..ee9962fd34 100644 --- a/mvc2/handler_test.go +++ b/mvc2/handler_test.go @@ -4,6 +4,7 @@ package mvc2_test import ( "fmt" + "reflect" "testing" "github.com/kataras/iris" @@ -69,19 +70,10 @@ var ( ) func TestMakeHandler(t *testing.T) { - // binders := []*InputBinder{ - // // #1 - // MustMakeFuncInputBinder(testBinderFuncUserStruct), - // // #2 - // MustMakeServiceInputBinder(testBinderService), - // // #3 - // MustMakeFuncInputBinder(testBinderFuncParam), - // } - var ( - h1 = MustMakeHandler(testConsumeUserHandler, testBinderFuncUserStruct) - h2 = MustMakeHandler(testConsumeServiceHandler, testBinderService) - h3 = MustMakeHandler(testConsumeParamHandler, testBinderFuncParam) + h1 = MustMakeHandler(testConsumeUserHandler, reflect.ValueOf(testBinderFuncUserStruct)) + h2 = MustMakeHandler(testConsumeServiceHandler, reflect.ValueOf(testBinderService)) + h3 = MustMakeHandler(testConsumeParamHandler, reflect.ValueOf(testBinderFuncParam)) ) testAppWithMvcHandlers(t, h1, h2, h3) diff --git a/mvc2/binder_in_path_param.go b/mvc2/path_param_binder.go similarity index 51% rename from mvc2/binder_in_path_param.go rename to mvc2/path_param_binder.go index 521965641d..1018cfb799 100644 --- a/mvc2/binder_in_path_param.go +++ b/mvc2/path_param_binder.go @@ -1,7 +1,6 @@ package mvc2 import ( - "fmt" "reflect" "github.com/kataras/iris/context" @@ -10,14 +9,7 @@ import ( "github.com/kataras/iris/core/router/macro/interpreter/ast" ) -func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { - n := funcTyp.NumIn() - funcIn := make([]reflect.Type, n, n) - for i := 0; i < n; i++ { - funcIn[i] = funcTyp.In(i) - } - return funcIn -} +// for methods inside a controller. func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) (values []reflect.Value) { if len(funcIn) == 0 || len(params) == 0 { @@ -30,72 +22,49 @@ func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) in := funcIn[funcInIdx] paramType := p.Type paramName := p.Name + // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) + if paramType.Assignable(in.Kind()) { + // fmt.Printf("path_param_binder.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) + values = append(values, makeFuncParamGetter(paramType, paramName)) + } - // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) - if p.Type.Assignable(in.Kind()) { - - // b = append(b, &InputBinder{ - // BindType: in, // or p.Type.Kind, should be the same. - // BindFunc: func(ctx []reflect.Value) reflect.Value { - // // I don't like this ctx[0].Interface(0) - // // it will be slow, and silly because we have ctx already - // // before the bindings at serve-time, so we will create - // // a func for each one of the param types, they are just 4 so - // // it worths some dublications. - // return getParamValueFromType(ctx[0].Interface(), paramType, paramName) - // }, - // }) - - var fn interface{} - - if paramType == ast.ParamTypeInt { - fn = func(ctx context.Context) int { - v, _ := ctx.Params().GetInt(paramName) - return v - } - } else if paramType == ast.ParamTypeLong { - fn = func(ctx context.Context) int64 { - v, _ := ctx.Params().GetInt64(paramName) - return v - } - - } else if paramType == ast.ParamTypeBoolean { - fn = func(ctx context.Context) bool { - v, _ := ctx.Params().GetBool(paramName) - return v - } - - } else { - // string, path... - fn = func(ctx context.Context) string { - return ctx.Params().Get(paramName) - } - } - - fmt.Printf("binder_in_path_param.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) - values = append(values, reflect.ValueOf(fn)) + funcInIdx++ + } - // inputBinder, err := MakeFuncInputBinder(fn) - // if err != nil { - // fmt.Printf("err on make func binder: %v\n", err.Error()) - // continue - // } + return +} - // if m == nil { - // m = make(bindersMap, 0) - // } +func makeFuncParamGetter(paramType ast.ParamType, paramName string) reflect.Value { + var fn interface{} - // // fmt.Printf("set param input binder for func arg index: %d\n", funcInIdx) - // m[funcInIdx] = inputBinder + switch paramType { + case ast.ParamTypeInt: + fn = func(ctx context.Context) int { + v, _ := ctx.Params().GetInt(paramName) + return v + } + case ast.ParamTypeLong: + fn = func(ctx context.Context) int64 { + v, _ := ctx.Params().GetInt64(paramName) + return v + } + case ast.ParamTypeBoolean: + fn = func(ctx context.Context) bool { + v, _ := ctx.Params().GetBool(paramName) + return v + } + default: + // string, path... + fn = func(ctx context.Context) string { + return ctx.Params().Get(paramName) } - - funcInIdx++ } - return - // return m + return reflect.ValueOf(fn) } +// for raw handlers, independent of a controller. + // PathParams is the context's named path parameters, see `PathParamsBinder` too. type PathParams = context.RequestParams diff --git a/mvc2/binder_in_path_param_test.go b/mvc2/path_param_binder_test.go similarity index 100% rename from mvc2/binder_in_path_param_test.go rename to mvc2/path_param_binder_test.go diff --git a/mvc2/reflect.go b/mvc2/reflect.go index 69f533fbf4..4f6080e803 100644 --- a/mvc2/reflect.go +++ b/mvc2/reflect.go @@ -1,6 +1,13 @@ package mvc2 -import "reflect" +import ( + "reflect" + + "github.com/kataras/iris/context" + "github.com/kataras/pkg/zerocheck" +) + +var contextTyp = reflect.TypeOf(context.NewContext(nil)) func isContext(inTyp reflect.Type) bool { return inTyp.String() == "context.Context" // I couldn't find another way; context/context.go is not exported. @@ -29,22 +36,11 @@ func goodVal(v reflect.Value) bool { return v.IsValid() } -func isFunc(typ reflect.Type) bool { - return typ.Kind() == reflect.Func -} - -/* -// no f. this, it's too complicated and it will be harder to maintain later on: -func isSliceAndExpectedItem(got reflect.Type, in []reflect.Type, currentBindersIdx int) bool { - kind := got.Kind() - // if got result is slice or array. - return (kind == reflect.Slice || kind == reflect.Array) && - // if has expected next input. - len(in)-1 > currentBindersIdx && - // if the current input's type is not the same as got (if it's not a slice of that types or anything else). - equalTypes(got, in[currentBindersIdx]) +func isFunc(kindable interface { + Kind() reflect.Kind +}) bool { + return kindable.Kind() == reflect.Func } -*/ func equalTypes(got reflect.Type, expected reflect.Type) bool { if got == expected { @@ -59,8 +55,16 @@ func equalTypes(got reflect.Type, expected reflect.Type) bool { return false } -// for controller only. +func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { + n := funcTyp.NumIn() + funcIn := make([]reflect.Type, n, n) + for i := 0; i < n; i++ { + funcIn[i] = funcTyp.In(i) + } + return funcIn +} +// for controller's fields only. func structFieldIgnored(f reflect.StructField) bool { if !f.Anonymous { return true // if not anonymous(embedded), ignore it. @@ -76,22 +80,28 @@ type field struct { Name string // the actual name // this could be empty, but in our cases it's not, - // it's filled with the service and it's filled from the lookupFields' caller. + // it's filled with the bind object (as service which means as static value) + // and it's filled from the lookupFields' caller. AnyValue reflect.Value } -func lookupFields(typ reflect.Type, parentIndex int) (fields []field) { - for i, n := 0, typ.NumField(); i < n; i++ { - f := typ.Field(i) +func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { + if elemTyp.Kind() != reflect.Struct { + return + } + + for i, n := 0, elemTyp.NumField(); i < n; i++ { + f := elemTyp.Field(i) - if f.Type.Kind() == reflect.Struct && !structFieldIgnored(f) { - fields = append(fields, lookupFields(f.Type, i)...) + if indirectTyp(f.Type).Kind() == reflect.Struct && + !structFieldIgnored(f) { + fields = append(fields, lookupFields(f.Type, append(parentIndex, i))...) continue } index := []int{i} - if parentIndex >= 0 { - index = append([]int{parentIndex}, index...) + if len(parentIndex) > 0 { + index = append(parentIndex, i) } field := field{ @@ -105,3 +115,16 @@ func lookupFields(typ reflect.Type, parentIndex int) (fields []field) { return } + +func lookupNonZeroFieldsValues(v reflect.Value) (bindValues []reflect.Value) { + elem := indirectVal(v) + fields := lookupFields(indirectTyp(v.Type()), nil) + for _, f := range fields { + + if fieldVal := elem.FieldByIndex(f.Index); f.Type.Kind() == reflect.Ptr && !zerocheck.IsZero(fieldVal) { + bindValues = append(bindValues, fieldVal) + } + } + + return +} diff --git a/mvc2/service.go b/mvc2/service.go deleted file mode 100644 index 03ab07e54f..0000000000 --- a/mvc2/service.go +++ /dev/null @@ -1,206 +0,0 @@ -package mvc2 - -import ( - "fmt" - "reflect" -) - -type service struct { - Type reflect.Type - Value reflect.Value - StructFieldIndex []int - - // for func input. - ReturnValue func(ctx []reflect.Value) reflect.Value - FuncInputIndex int - FuncInputContextIndex int -} - -type services []*service - -func (serv *services) AddSource(dest reflect.Value, source ...reflect.Value) { - fmt.Println("--------------AddSource------------") - if len(source) == 0 { - return - } - - typ := indirectTyp(dest.Type()) //indirectTyp(reflect.TypeOf(dest)) - _serv := *serv - - if typ.Kind() == reflect.Func { - n := typ.NumIn() - for i := 0; i < n; i++ { - - inTyp := typ.In(i) - if isContext(inTyp) { - _serv = append(_serv, &service{FuncInputContextIndex: i}) - continue - } - - for _, s := range source { - gotTyp := s.Type() - - service := service{ - Type: gotTyp, - Value: s, - FuncInputIndex: i, - FuncInputContextIndex: -1, - } - - if s.Type().Kind() == reflect.Func { - fmt.Printf("Source is Func\n") - returnValue, outType, err := makeReturnValue(s) - if err != nil { - fmt.Printf("Err on makeReturnValue: %v\n", err) - continue - } - gotTyp = outType - service.ReturnValue = returnValue - } - - fmt.Printf("Types: In=%s vs Got=%s\n", inTyp.String(), gotTyp.String()) - if equalTypes(gotTyp, inTyp) { - service.Type = gotTyp - fmt.Printf("Bind In=%s->%s for func\n", inTyp.String(), gotTyp.String()) - _serv = append(_serv, &service) - - break - } - } - } - fmt.Printf("[1] Bind %d for %s\n", len(_serv), typ.String()) - *serv = _serv - - return - } - - if typ.Kind() == reflect.Struct { - fields := lookupFields(typ, -1) - for _, f := range fields { - for _, s := range source { - gotTyp := s.Type() - - service := service{ - Type: gotTyp, - Value: s, - StructFieldIndex: f.Index, - FuncInputContextIndex: -1, - } - - if s.Type().Kind() == reflect.Func { - returnValue, outType, err := makeReturnValue(s) - if err != nil { - continue - } - gotTyp = outType - service.ReturnValue = returnValue - } - - if equalTypes(gotTyp, f.Type) { - service.Type = gotTyp - _serv = append(_serv, &service) - fmt.Printf("[2] Bind In=%s->%s for struct field[%d]\n", f.Type, gotTyp.String(), f.Index) - break - } - } - } - fmt.Printf("[2] Bind %d for %s\n", len(_serv), typ.String()) - *serv = _serv - - return - } -} - -func (serv services) FillStructStaticValues(elem reflect.Value) { - if len(serv) == 0 { - return - } - - for _, s := range serv { - if len(s.StructFieldIndex) > 0 { - // fmt.Printf("FillStructStaticValues for index: %d\n", s.StructFieldIndex) - elem.FieldByIndex(s.StructFieldIndex).Set(s.Value) - } - } -} - -func (serv services) FillStructDynamicValues(elem reflect.Value, ctx []reflect.Value) { - if len(serv) == 0 { - return - } - - for _, s := range serv { - if len(s.StructFieldIndex) > 0 { - elem.FieldByIndex(s.StructFieldIndex).Set(s.ReturnValue(ctx)) - } - } -} - -func (serv services) FillFuncInput(ctx []reflect.Value, destIn *[]reflect.Value) { - if len(serv) == 0 { - return - } - - in := *destIn - for _, s := range serv { - if s.ReturnValue != nil { - in[s.FuncInputIndex] = s.ReturnValue(ctx) - continue - } - - in[s.FuncInputIndex] = s.Value - if s.FuncInputContextIndex >= 0 { - in[s.FuncInputContextIndex] = ctx[0] - } - } - - *destIn = in -} - -func makeReturnValue(fn reflect.Value) (func([]reflect.Value) reflect.Value, reflect.Type, error) { - typ := indirectTyp(fn.Type()) - - // invalid if not a func. - if typ.Kind() != reflect.Func { - return nil, typ, errBad - } - - // invalid if not returns one single value. - if typ.NumOut() != 1 { - return nil, typ, errBad - } - - // invalid if input args length is not one. - if typ.NumIn() != 1 { - return nil, typ, errBad - } - - // invalid if that single input arg is not a typeof context.Context. - if !isContext(typ.In(0)) { - return nil, typ, errBad - } - - outTyp := typ.Out(0) - zeroOutVal := reflect.New(outTyp).Elem() - - bf := func(ctxValue []reflect.Value) reflect.Value { - // []reflect.Value{reflect.ValueOf(ctx)} - results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler. - if len(results) == 0 { - return zeroOutVal - } - - v := results[0] - if !v.IsValid() { - return zeroOutVal - } - return v - } - - return bf, outTyp, nil -} - -func getServicesFor(dest reflect.Value, source []reflect.Value) (s services) { - s.AddSource(dest, source...) - return s -} diff --git a/mvc2/session_controller.go b/mvc2/session_controller.go index 5ea5f5c442..6ca5bce4b0 100644 --- a/mvc2/session_controller.go +++ b/mvc2/session_controller.go @@ -1,11 +1,11 @@ package mvc2 import ( - "github.com/kataras/iris/context" - "github.com/kataras/iris/sessions" "reflect" "github.com/kataras/golog" + "github.com/kataras/iris/context" + "github.com/kataras/iris/sessions" ) var defaultManager = sessions.New(sessions.Config{}) @@ -25,8 +25,8 @@ type SessionController struct { // It makes sure that its "Manager" field is filled // even if the caller didn't provide any sessions manager via the `app.Controller` function. func (s *SessionController) OnActivate(ca *ControllerActivator) { - if !ca.Engine.BindTypeExists(reflect.TypeOf(defaultManager)) { - ca.Engine.Bind(defaultManager) + if !ca.BindTypeExists(reflect.TypeOf(defaultManager)) { + ca.Bind(defaultManager) golog.Warnf(`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, therefore this controller is using the default sessions manager instead. Please refer to the documentation to learn how you can provide the session manager`) From 54ed53c54cc94811c3cfeec7f6192a1950346373 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 10 Dec 2017 07:26:01 +0200 Subject: [PATCH 12/79] add a ridiculous simple 'context#String' func which will return a very simple string representatin of the current request --- context/context.go | 51 ++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/context/context.go b/context/context.go index 19ea0f9ac1..0905dc7253 100644 --- a/context/context.go +++ b/context/context.go @@ -19,6 +19,7 @@ import ( "regexp" "strconv" "strings" + "sync/atomic" "time" "github.com/fatih/structs" @@ -813,8 +814,12 @@ type Context interface { Application() Application // String returns the string representation of this request. - // Each context has a unique string representation, so this can be used - // as an "ID" as well, if needed. + // Each context has a unique string representation. + // It can be used for simple debugging scenarions, i.e print context as string. + // + // What it returns? A number which declares the length of the + // total `String` calls per executable application, followed + // by the remote IP (the client) and finally the method:url. String() string } @@ -867,10 +872,10 @@ type Map map[string]interface{} // +------------------------------------------------------------+ type context struct { - // the unique id, it's empty until `String` function is called, + // the unique id, it's zero until `String` function is called, // it's here to cache the random, unique context's id, although `String` // returns more than this. - id string + id uint64 // the http.ResponseWriter wrapped by custom writer. writer ResponseWriter // the original http.Request @@ -2735,19 +2740,6 @@ func (ctx *context) Exec(method string, path string) { } } -// String returns the string representation of this request. -// Each context has a unique string representation, so this can be used -// as an "ID" as well, if needed. -func (ctx *context) String() (s string) { - if ctx.id == "" { - // set the id here. - - s = "..." - } - - return -} - // Application returns the iris app instance which belongs to this context. // Worth to notice that this function returns an interface // of the Application, which contains methods that are safe @@ -2756,3 +2748,28 @@ func (ctx *context) String() (s string) { func (ctx *context) Application() Application { return ctx.app } + +var lastCapturedContextID uint64 + +// LastCapturedContextID returns the total number of `context#String` calls. +func LastCapturedContextID() uint64 { + return atomic.LoadUint64(&lastCapturedContextID) +} + +// String returns the string representation of this request. +// Each context has a unique string representation. +// It can be used for simple debugging scenarions, i.e print context as string. +// +// What it returns? A number which declares the length of the +// total `String` calls per executable application, followed +// by the remote IP (the client) and finally the method:url. +func (ctx *context) String() string { + if ctx.id == 0 { + // set the id here. + forward := atomic.AddUint64(&lastCapturedContextID, 1) + ctx.id = forward + } + + return fmt.Sprintf("[%d] %s ▶ %s:%s", + ctx.id, ctx.RemoteAddr(), ctx.Method(), ctx.Request().RequestURI) +} From a4a23c8de8b3904a3f53f378e36aaccaf287f487 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 11 Dec 2017 06:24:55 +0200 Subject: [PATCH 13/79] add an overview of the new semantics at the mvc2/README.md --- mvc2/README.md | 118 ++++++++++++++++++++++++++++++++++++++++ mvc2/controller.go | 2 +- mvc2/controller_test.go | 3 + 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 mvc2/README.md diff --git a/mvc2/README.md b/mvc2/README.md new file mode 100644 index 0000000000..715abce83f --- /dev/null +++ b/mvc2/README.md @@ -0,0 +1,118 @@ +# MVC Internals + +* `MakeHandler` - accepts a function which accepts any input and outputs any result, and any optional values that will be used as binders, if needed they will be converted in order to be faster at serve-time. Returns a `context/iris#Handler` and a non-nil error if passed function cannot be wrapped to a raw `context/iris#Handler` +* `Engine` - The "manager" of the controllers and handlers, can be grouped and an `Engine` can have any number of children. + * `Engine#Bind` Binds values to be used inside on one or more handlers and controllers + * `Engine#Handler` - Creates and returns a new mvc handler, which accept any input parameters (calculated by the binders) and output any result which will be sent as a response to the HTTP Client. Calls the `MakeHandler` with the Engine's `Input` values as the binders + * `Engine#Controller` - Creates and activates a controller based on a struct which has the `C` as an embedded , anonymous, field and defines methods to be used as routes. Can accept any optional activator listeners in order to bind any custom routes or change the bindings, called once at startup +* `C` + * Struct fields with `Struct Binding` + * Methods with `Dynamic Binding` + + +Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/mvc. + +## Binding + +First of all, they can be binded to `func input arguments` (custom handlers) or `struct fields` (controllers). We will use the term `Input` for both of them. + +```go +// consume the user here as func input argument. +func myHandler(user User) {} + +type myController struct { + C + + // consume the user here, as struct field. + user User +} +``` + +If the input is an interface then it will check if the binding is completed this interface +and it will be binded as expected. + +Two types of binders are supported: + +### Dynamic Binding + +`ReturnValue`, should return a single value, no pointer to, if the consumer Input (`struct field` or `func input argument`) expects `User` then it will be binded on each request, this is a dynamic binding based on the `Context`. + +```go +type User struct { + Username string +} + +myBinder := func(ctx iris.Context) User { + return User { + Username: ctx.Params().Get("username"), + } +} + +myHandler := func(user User) { + // ... +} +``` + +### Static Binding + +`Value (Service)`, this is used to bind a value instance, like a service or a database connection. + +```go +// optional but we declare interface most of the times to +// be easier to switch from production to testing or local and visa versa. +// If input is an interface then it will check if the binding is completed this interface +// and it will be binded as expected. +type Service interface { + Do() string +} + +type myProductionService struct { + text string +} +func (s *myProductionService) Do() string { + return s.text +} + +myService := &myProductionService{text: "something"} + +myHandler := func(service Service) { + // ... +} +``` + +### Bind + +#### For Handlers + +MakeHandler is used to create a handler based on a function which can accept any input arguments and export any output arguments, the input arguments can be dynamic path parameters or custom [binders](#binding). + +```go +h, err := MakeHandler(myHandler, reflect.ValueOf(myBinder)) +``` + +Values passed in `Bind` are binded to all handlers and controllers that are expected a type of the returned value, in this case the myBinder indicates a dynamic/serve-time function which returns a User, as shown above. + +```go +m := New().Bind(myBinder) + +h := m.Handler(myHandler) +``` + +#### For Controllers + +```go +app := iris.New() +New().Bind(myBinder).Controller(app, new(myController)) +// ... +``` + +```go +sub := app.Party("/sub") +New().Controller(sub, &myController{service: myService}) +``` + +```go +New().Controller(sub.Party("/subsub"), new(myController), func(ca *ControllerActivator) { + ca.Bind(myService) +}) +``` \ No newline at end of file diff --git a/mvc2/controller.go b/mvc2/controller.go index 452e68c890..6244674e41 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -166,8 +166,8 @@ func (c *ControllerActivator) analyze() { // SetBindings will override any bindings with the new "values". func (c *ControllerActivator) SetBindings(values ...reflect.Value) { // set field index with matching binders, if any. + c.input = values c.bindings = newTargetStruct(reflect.ValueOf(c.initRef), values...) - c.input = c.input[0:0] } // Bind binds values to this controller, if you want to share diff --git a/mvc2/controller_test.go b/mvc2/controller_test.go index 05afef307e..2e0cd38e47 100644 --- a/mvc2/controller_test.go +++ b/mvc2/controller_test.go @@ -403,6 +403,9 @@ func TestControllerRelPathFromFunc(t *testing.T) { Body().Equal("GET:/42") e.GET("/anything/here").Expect().Status(iris.StatusOK). Body().Equal("GET:/anything/here") + + e.GET("/params/without/keyword/param1/param2").Expect().Status(iris.StatusOK). + Body().Equal("PUT:/params/without/keyword/param1/param2") } type testControllerActivateListener struct { From a741726944ee68ae13a7a9fda4469eb2d80ccde0 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 11 Dec 2017 06:51:52 +0200 Subject: [PATCH 14/79] split the controller's method lexer and parser to another file to simplify the code, we need more steps to simplify it enough before pushing to master --- mvc2/controller.go | 303 ++++--------------------------- mvc2/controller_method_parser.go | 236 ++++++++++++++++++++++++ mvc2/controller_test.go | 2 - mvc2/reflect.go | 11 ++ 4 files changed, 282 insertions(+), 270 deletions(-) create mode 100644 mvc2/controller_method_parser.go diff --git a/mvc2/controller.go b/mvc2/controller.go index 6244674e41..ff7fb964f8 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -1,16 +1,12 @@ package mvc2 import ( - "errors" "fmt" "reflect" - "strings" - "unicode" "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" "github.com/kataras/iris/core/router/macro" - "github.com/kataras/iris/core/router/macro/interpreter/ast" ) type BaseController interface { @@ -70,8 +66,9 @@ type ControllerActivator struct { // in order to not create a new type like `ActivationPayload` for the `OnActivate`. Router router.Party - initRef BaseController // the BaseController as it's passed from the end-dev. - Type reflect.Type // raw type of the BaseController (initRef). + // initRef BaseController // the BaseController as it's passed from the end-dev. + Value reflect.Value // the BaseController's Value. + Type reflect.Type // raw type of the BaseController (initRef). // FullName it's the last package path segment + "." + the Name. // i.e: if login-example/user/controller.go, the FullName is "user.Controller". FullName string @@ -90,28 +87,36 @@ type ControllerActivator struct { bindings *targetStruct } -var emptyMethod = reflect.Method{} - func newControllerActivator(router router.Party, controller BaseController, bindValues ...reflect.Value) *ControllerActivator { + // the following will make sure that if + // the controller's has set-ed pointer struct fields by the end-dev + // we will include them to the bindings. + // set bindings to the non-zero pointer fields' values that may be set-ed by + // the end-developer when declaring the controller, + // activate listeners needs them in order to know if something set-ed already or not, + // look `BindTypeExists`. + + var ( + val = reflect.ValueOf(controller) + typ = val.Type() + + fullName = getNameOf(typ) + ) + c := &ControllerActivator{ - Router: router, - initRef: controller, + Router: router, + Value: val, + Type: typ, + FullName: fullName, reservedMethods: []string{ "BeginRequest", "EndRequest", "OnActivate", }, - // the following will make sure that if - // the controller's has set-ed pointer struct fields by the end-dev - // we will include them to the bindings. - // set bindings to the non-zero pointer fields' values that may be set-ed by - // the end-developer when declaring the controller, - // activate listeners needs them in order to know if something set-ed already or not, - // look `BindTypeExists`. - input: append(lookupNonZeroFieldsValues(reflect.ValueOf(controller)), bindValues...), + input: append(lookupNonZeroFieldsValues(val), bindValues...), } - c.analyze() + c.parseMethods() return c } @@ -125,49 +130,31 @@ func (c *ControllerActivator) isReservedMethod(name string) bool { return false } -func (c *ControllerActivator) analyze() { - // set full name. - - // first instance value, needed to validate - // the actual type of the controller field - // and to collect and save the instance's persistence fields' - // values later on. - typ := reflect.TypeOf(c.initRef) // type with pointer - elemTyp := indirectTyp(typ) - - ctrlName := elemTyp.Name() - pkgPath := elemTyp.PkgPath() - fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName - c.FullName = fullName - c.Type = typ - +func (c *ControllerActivator) parseMethods() { // register all available, exported methods to handlers if possible. - n := typ.NumMethod() + n := c.Type.NumMethod() for i := 0; i < n; i++ { - m := typ.Method(i) - funcName := m.Name + m := c.Type.Method(i) - if c.isReservedMethod(funcName) { - continue - } + httpMethod, httpPath, err := parseMethod(m, c.isReservedMethod) + if err != nil { + if err != errSkip { + err = fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.FullName, m.Name, err) + c.Router.GetReporter().AddErr(err) - httpMethod, httpPath, err := parse(m) - if err != nil && err != errSkip { - err = fmt.Errorf("MVC: fail to parse the path and method for '%s.%s': %v", c.FullName, m.Name, err) - c.Router.GetReporter().AddErr(err) + } continue } - c.Handle(httpMethod, httpPath, funcName) + c.Handle(httpMethod, httpPath, m.Name) } - } // SetBindings will override any bindings with the new "values". func (c *ControllerActivator) SetBindings(values ...reflect.Value) { // set field index with matching binders, if any. c.input = values - c.bindings = newTargetStruct(reflect.ValueOf(c.initRef), values...) + c.bindings = newTargetStruct(c.Value, values...) } // Bind binds values to this controller, if you want to share @@ -294,223 +281,3 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . return nil } - -const ( - tokenBy = "By" - tokenWildcard = "Wildcard" // "ByWildcard". -) - -// word lexer, not characters. -type lexer struct { - words []string - cur int -} - -func newLexer(s string) *lexer { - l := new(lexer) - l.reset(s) - return l -} - -func (l *lexer) reset(s string) { - l.cur = -1 - var words []string - if s != "" { - end := len(s) - start := -1 - - for i, n := 0, end; i < n; i++ { - c := rune(s[i]) - if unicode.IsUpper(c) { - // it doesn't count the last uppercase - if start != -1 { - end = i - words = append(words, s[start:end]) - } - start = i - continue - } - end = i + 1 - } - - if end > 0 && len(s) >= end { - words = append(words, s[start:end]) - } - } - - l.words = words -} - -func (l *lexer) next() (w string) { - cur := l.cur + 1 - - if w = l.peek(cur); w != "" { - l.cur++ - } - - return -} - -func (l *lexer) skip() { - if cur := l.cur + 1; cur < len(l.words) { - l.cur = cur - } else { - l.cur = len(l.words) - 1 - } -} - -func (l *lexer) peek(idx int) string { - if idx < len(l.words) { - return l.words[idx] - } - return "" -} - -func (l *lexer) peekNext() (w string) { - return l.peek(l.cur + 1) -} - -func (l *lexer) peekPrev() (w string) { - if l.cur > 0 { - cur := l.cur - 1 - w = l.words[cur] - } - - return w -} - -var posWords = map[int]string{ - 0: "", - 1: "first", - 2: "second", - 3: "third", - 4: "forth", - 5: "five", - 6: "sixth", - 7: "seventh", - 8: "eighth", - 9: "ninth", -} - -func genParamKey(argIdx int) string { - return "param" + posWords[argIdx] // paramfirst, paramsecond... -} - -type parser struct { - lexer *lexer - fn reflect.Method -} - -func parse(fn reflect.Method) (method, path string, err error) { - p := &parser{ - fn: fn, - lexer: newLexer(fn.Name), - } - return p.parse() -} - -func methodTitle(httpMethod string) string { - httpMethodFuncName := strings.Title(strings.ToLower(httpMethod)) - return httpMethodFuncName -} - -var errSkip = errors.New("skip") - -var allMethods = append(router.AllMethods[0:], []string{"ALL", "ANY"}...) - -func (p *parser) parse() (method, path string, err error) { - funcArgPos := 0 - path = "/" - // take the first word and check for the method. - w := p.lexer.next() - - for _, httpMethod := range allMethods { - possibleMethodFuncName := methodTitle(httpMethod) - if strings.Index(w, possibleMethodFuncName) == 0 { - method = httpMethod - break - } - } - - if method == "" { - // this is not a valid method to parse, we just skip it, - // it may be used for end-dev's use cases. - return "", "", errSkip - } - - for { - w := p.lexer.next() - if w == "" { - break - } - - if w == tokenBy { - funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver. - - // No need for these: - // ByBy will act like /{param:type}/{param:type} as users expected - // if func input arguments are there, else act By like normal path /by. - // - // if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path - // a.relPath += "/" + strings.ToLower(w) - // continue - // } - - if path, err = p.parsePathParam(path, w, funcArgPos); err != nil { - return "", "", err - } - - continue - } - // static path. - path += "/" + strings.ToLower(w) - - } - - return -} - -func (p *parser) parsePathParam(path string, w string, funcArgPos int) (string, error) { - typ := p.fn.Type - - if typ.NumIn() <= funcArgPos { - - // By found but input arguments are not there, so act like /by path without restricts. - path += "/" + strings.ToLower(w) - return path, nil - } - - var ( - paramKey = genParamKey(funcArgPos) // paramfirst, paramsecond... - paramType = ast.ParamTypeString // default string - ) - - // string, int... - goType := typ.In(funcArgPos).Name() - nextWord := p.lexer.peekNext() - - if nextWord == tokenWildcard { - p.lexer.skip() // skip the Wildcard word. - paramType = ast.ParamTypePath - } else if pType := ast.LookupParamTypeFromStd(goType); pType != ast.ParamTypeUnExpected { - // it's not wildcard, so check base on our available macro types. - paramType = pType - } else { - return "", errors.New("invalid syntax for " + p.fn.Name) - } - - // /{paramfirst:path}, /{paramfirst:long}... - path += fmt.Sprintf("/{%s:%s}", paramKey, paramType.String()) - - if nextWord == "" && typ.NumIn() > funcArgPos+1 { - // By is the latest word but func is expected - // more path parameters values, i.e: - // GetBy(name string, age int) - // The caller (parse) doesn't need to know - // about the incremental funcArgPos because - // it will not need it. - return p.parsePathParam(path, nextWord, funcArgPos+1) - } - - return path, nil -} diff --git a/mvc2/controller_method_parser.go b/mvc2/controller_method_parser.go new file mode 100644 index 0000000000..63d7a67174 --- /dev/null +++ b/mvc2/controller_method_parser.go @@ -0,0 +1,236 @@ +package mvc2 + +import ( + "errors" + "fmt" + "reflect" + "strings" + "unicode" + + "github.com/kataras/iris/core/router" + "github.com/kataras/iris/core/router/macro/interpreter/ast" +) + +const ( + tokenBy = "By" + tokenWildcard = "Wildcard" // "ByWildcard". +) + +// word lexer, not characters. +type methodLexer struct { + words []string + cur int +} + +func newMethodLexer(s string) *methodLexer { + l := new(methodLexer) + l.reset(s) + return l +} + +func (l *methodLexer) reset(s string) { + l.cur = -1 + var words []string + if s != "" { + end := len(s) + start := -1 + + for i, n := 0, end; i < n; i++ { + c := rune(s[i]) + if unicode.IsUpper(c) { + // it doesn't count the last uppercase + if start != -1 { + end = i + words = append(words, s[start:end]) + } + start = i + continue + } + end = i + 1 + } + + if end > 0 && len(s) >= end { + words = append(words, s[start:end]) + } + } + + l.words = words +} + +func (l *methodLexer) next() (w string) { + cur := l.cur + 1 + + if w = l.peek(cur); w != "" { + l.cur++ + } + + return +} + +func (l *methodLexer) skip() { + if cur := l.cur + 1; cur < len(l.words) { + l.cur = cur + } else { + l.cur = len(l.words) - 1 + } +} + +func (l *methodLexer) peek(idx int) string { + if idx < len(l.words) { + return l.words[idx] + } + return "" +} + +func (l *methodLexer) peekNext() (w string) { + return l.peek(l.cur + 1) +} + +func (l *methodLexer) peekPrev() (w string) { + if l.cur > 0 { + cur := l.cur - 1 + w = l.words[cur] + } + + return w +} + +var posWords = map[int]string{ + 0: "", + 1: "first", + 2: "second", + 3: "third", + 4: "forth", + 5: "five", + 6: "sixth", + 7: "seventh", + 8: "eighth", + 9: "ninth", +} + +func genParamKey(argIdx int) string { + return "param" + posWords[argIdx] // paramfirst, paramsecond... +} + +type methodParser struct { + lexer *methodLexer + fn reflect.Method +} + +func parseMethod(fn reflect.Method, skipper func(string) bool) (method, path string, err error) { + if skipper(fn.Name) { + return "", "", errSkip + } + + p := &methodParser{ + fn: fn, + lexer: newMethodLexer(fn.Name), + } + return p.parse() +} + +func methodTitle(httpMethod string) string { + httpMethodFuncName := strings.Title(strings.ToLower(httpMethod)) + return httpMethodFuncName +} + +var errSkip = errors.New("skip") + +var allMethods = append(router.AllMethods[0:], []string{"ALL", "ANY"}...) + +func (p *methodParser) parse() (method, path string, err error) { + funcArgPos := 0 + path = "/" + // take the first word and check for the method. + w := p.lexer.next() + + for _, httpMethod := range allMethods { + possibleMethodFuncName := methodTitle(httpMethod) + if strings.Index(w, possibleMethodFuncName) == 0 { + method = httpMethod + break + } + } + + if method == "" { + // this is not a valid method to parse, we just skip it, + // it may be used for end-dev's use cases. + return "", "", errSkip + } + + for { + w := p.lexer.next() + if w == "" { + break + } + + if w == tokenBy { + funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver. + + // No need for these: + // ByBy will act like /{param:type}/{param:type} as users expected + // if func input arguments are there, else act By like normal path /by. + // + // if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path + // a.relPath += "/" + strings.ToLower(w) + // continue + // } + + if path, err = p.parsePathParam(path, w, funcArgPos); err != nil { + return "", "", err + } + + continue + } + // static path. + path += "/" + strings.ToLower(w) + + } + + return +} + +func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (string, error) { + typ := p.fn.Type + + if typ.NumIn() <= funcArgPos { + + // By found but input arguments are not there, so act like /by path without restricts. + path += "/" + strings.ToLower(w) + return path, nil + } + + var ( + paramKey = genParamKey(funcArgPos) // paramfirst, paramsecond... + paramType = ast.ParamTypeString // default string + ) + + // string, int... + goType := typ.In(funcArgPos).Name() + nextWord := p.lexer.peekNext() + + if nextWord == tokenWildcard { + p.lexer.skip() // skip the Wildcard word. + paramType = ast.ParamTypePath + } else if pType := ast.LookupParamTypeFromStd(goType); pType != ast.ParamTypeUnExpected { + // it's not wildcard, so check base on our available macro types. + paramType = pType + } else { + return "", errors.New("invalid syntax for " + p.fn.Name) + } + + // /{paramfirst:path}, /{paramfirst:long}... + path += fmt.Sprintf("/{%s:%s}", paramKey, paramType.String()) + + if nextWord == "" && typ.NumIn() > funcArgPos+1 { + // By is the latest word but func is expected + // more path parameters values, i.e: + // GetBy(name string, age int) + // The caller (parse) doesn't need to know + // about the incremental funcArgPos because + // it will not need it. + return p.parsePathParam(path, nextWord, funcArgPos+1) + } + + return path, nil +} diff --git a/mvc2/controller_test.go b/mvc2/controller_test.go index 2e0cd38e47..677222e5f2 100644 --- a/mvc2/controller_test.go +++ b/mvc2/controller_test.go @@ -404,8 +404,6 @@ func TestControllerRelPathFromFunc(t *testing.T) { e.GET("/anything/here").Expect().Status(iris.StatusOK). Body().Equal("GET:/anything/here") - e.GET("/params/without/keyword/param1/param2").Expect().Status(iris.StatusOK). - Body().Equal("PUT:/params/without/keyword/param1/param2") } type testControllerActivateListener struct { diff --git a/mvc2/reflect.go b/mvc2/reflect.go index 4f6080e803..4405d7840d 100644 --- a/mvc2/reflect.go +++ b/mvc2/reflect.go @@ -2,6 +2,7 @@ package mvc2 import ( "reflect" + "strings" "github.com/kataras/iris/context" "github.com/kataras/pkg/zerocheck" @@ -55,6 +56,16 @@ func equalTypes(got reflect.Type, expected reflect.Type) bool { return false } +func getNameOf(typ reflect.Type) string { + elemTyp := indirectTyp(typ) + + typName := elemTyp.Name() + pkgPath := elemTyp.PkgPath() + fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName + + return fullname +} + func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { n := funcTyp.NumIn() funcIn := make([]reflect.Type, n, n) From c7e9736f6e5e2c6f48c573c9eef334f116c0983e Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 12 Dec 2017 14:33:39 +0200 Subject: [PATCH 15/79] add some comments but I just released that we may not need controller's input field and we can bind directly via the targetStruct binder, next step is to implement that behavior --- mvc2/controller.go | 95 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/mvc2/controller.go b/mvc2/controller.go index ff7fb964f8..14dbd86f4a 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -9,6 +9,15 @@ import ( "github.com/kataras/iris/core/router/macro" ) +// BaseController is the controller interface, +// which the main request `C` will implement automatically. +// End-dev doesn't need to have any knowledge of this if she/he doesn't want to implement +// a new Controller type. +// Controller looks the whole flow as one handler, so `ctx.Next` +// inside `BeginRequest` is not be respected. +// Alternative way to check if a middleware was procceed successfully +// and called its `ctx.Next` is the `ctx.Proceed(handler) bool`. +// You have to navigate to the `context/context#Proceed` function's documentation. type BaseController interface { BeginRequest(context.Context) EndRequest(context.Context) @@ -59,8 +68,9 @@ func (c *C) BeginRequest(ctx context.Context) { c.Ctx = ctx } // EndRequest does nothing, is here to complete the `BaseController` interface. func (c *C) EndRequest(ctx context.Context) {} +// ControllerActivator returns a new controller type info description. +// Its functionality can be overriden by the end-dev. type ControllerActivator struct { - Engine *Engine // the router is used on the `Activate` and can be used by end-dev on the `OnActivate` // to register any custom controller's functions as handlers but we will need it here // in order to not create a new type like `ActivationPayload` for the `OnActivate`. @@ -88,6 +98,14 @@ type ControllerActivator struct { } func newControllerActivator(router router.Party, controller BaseController, bindValues ...reflect.Value) *ControllerActivator { + var ( + val = reflect.ValueOf(controller) + typ = val.Type() + + // the full name of the controller, it's its type including the package path. + fullName = getNameOf(typ) + ) + // the following will make sure that if // the controller's has set-ed pointer struct fields by the end-dev // we will include them to the bindings. @@ -95,31 +113,37 @@ func newControllerActivator(router router.Party, controller BaseController, bind // the end-developer when declaring the controller, // activate listeners needs them in order to know if something set-ed already or not, // look `BindTypeExists`. - - var ( - val = reflect.ValueOf(controller) - typ = val.Type() - - fullName = getNameOf(typ) - ) + bindValues = append(lookupNonZeroFieldsValues(val), bindValues...) c := &ControllerActivator{ + // give access to the Router to the end-devs if they need it for some reason, + // i.e register done handlers. Router: router, Value: val, Type: typ, FullName: fullName, + // set some methods that end-dev cann't use accidentally + // to register a route via the `Handle`, + // all available exported and compatible methods + // are being appended to the slice at the `parseMethods`, + // if a new method is registered via `Handle` its function name + // is also appended to that slice. reservedMethods: []string{ "BeginRequest", "EndRequest", "OnActivate", }, - input: append(lookupNonZeroFieldsValues(val), bindValues...), + // set the input as []reflect.Value in order to be able + // to check if a bind type is already exists, or even + // override the structBindings that are being generated later on. + input: bindValues, } c.parseMethods() return c } +// checks if a method is already registered. func (c *ControllerActivator) isReservedMethod(name string) bool { for _, s := range c.reservedMethods { if s == name { @@ -130,8 +154,9 @@ func (c *ControllerActivator) isReservedMethod(name string) bool { return false } +// register all available, exported methods to handlers if possible. func (c *ControllerActivator) parseMethods() { - // register all available, exported methods to handlers if possible. + n := c.Type.NumMethod() for i := 0; i < n; i++ { m := c.Type.Method(i) @@ -184,49 +209,65 @@ func (c *ControllerActivator) activate() { var emptyIn = []reflect.Value{} -func (c *ControllerActivator) Handle(method, path, funcName string, middleware ...context.Handler) error { +// Handle registers a route based on a http method, the route's path +// and a function name that belongs to the controller, it accepts +// a forth, optionally, variadic parameter which is the before handlers. +// +// Just like `APIBuilder`, it returns the `*router.Route`, if failed +// then it logs the errors and it returns nil, you can check the errors +// programmatically by the `APIBuilder#GetReporter`. +func (c *ControllerActivator) Handle(method, path, funcName string, middleware ...context.Handler) *router.Route { if method == "" || path == "" || funcName == "" || c.isReservedMethod(funcName) { // isReservedMethod -> if it's already registered // by a previous Handle or analyze methods internally. - return errSkip + return nil } + // get the method from the controller type. m, ok := c.Type.MethodByName(funcName) if !ok { err := fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", funcName, c.FullName) c.Router.GetReporter().AddErr(err) - return err + return nil } + // parse a route template which contains the parameters organised. tmpl, err := macro.Parse(path, c.Router.Macros()) if err != nil { err = fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.FullName, funcName, err) c.Router.GetReporter().AddErr(err) - return err + return nil } // add this as a reserved method name in order to // be sure that the same func will not be registered again, even if a custom .Handle later on. c.reservedMethods = append(c.reservedMethods, funcName) - // fmt.Printf("===============%s.%s==============\n", c.FullName, funcName) - - funcIn := getInputArgsFromFunc(m.Type) // except the receiver, which is the controller pointer itself. + // get the function's input. + funcIn := getInputArgsFromFunc(m.Type) + // get the path parameters bindings from the template, + // use the function's input except the receiver which is the + // end-dev's controller pointer. pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...) + // get the function's input arguments' bindings. funcBindings := newTargetFunc(m.Func, pathParams...) - elemTyp := indirectTyp(c.Type) // the element value, not the pointer. + // the element value, not the pointer. + elemTyp := indirectTyp(c.Type) + // we will make use of 'n' to make a slice of reflect.Value + // to pass into if the function has input arguments that + // are will being filled by the funcBindings. n := len(funcIn) - handler := func(ctx context.Context) { // create a new controller instance of that type(>ptr). ctrl := reflect.New(elemTyp) - b := ctrl.Interface().(BaseController) // the Interface(). is faster than MethodByName or pre-selected methods. + // the Interface(). is faster than MethodByName or pre-selected methods. + b := ctrl.Interface().(BaseController) // init the request. b.BeginRequest(ctx) @@ -268,16 +309,20 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . } - // end the request, don't check for stopped because this does the actual writing - // if no response written already. + if ctx.IsStopped() { + return + } + b.EndRequest(ctx) } // register the handler now. - c.Router.Handle(method, path, append(middleware, handler)...). + route := c.Router.Handle(method, path, append(middleware, handler)...) + if route != nil { // change the main handler's name in order to respect the controller's and give // a proper debug message. - MainHandlerName = fmt.Sprintf("%s.%s", c.FullName, funcName) + route.MainHandlerName = fmt.Sprintf("%s.%s", c.FullName, funcName) + } - return nil + return route } From c151b8c2f2ca97a2e98a07d83f5206909225d830 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 13 Dec 2017 06:17:28 +0200 Subject: [PATCH 16/79] performance close to handlers if no bindings but even if bindings except service (new feature is that we can bind functions as well) is x1.1 faster than the previous mvc implementation - make BaseController (so and C) optionally but not break the existing APIs that using iris.C or mvc.C --- .../controllers/values_controller.go | 23 +++++ _benchmarks/iris-mvc2/main.go | 17 ++++ configuration.go | 2 +- core/memstore/memstore.go | 7 ++ mvc2/bind.go | 40 +++++--- mvc2/bind_values.go | 99 +++++++++++++++++++ mvc2/controller.go | 94 +++++++++--------- mvc2/controller_test.go | 3 +- mvc2/engine.go | 6 +- mvc2/reflect.go | 10 +- mvc2/session_controller.go | 13 +-- 11 files changed, 239 insertions(+), 75 deletions(-) create mode 100644 _benchmarks/iris-mvc2/controllers/values_controller.go create mode 100644 _benchmarks/iris-mvc2/main.go create mode 100644 mvc2/bind_values.go diff --git a/_benchmarks/iris-mvc2/controllers/values_controller.go b/_benchmarks/iris-mvc2/controllers/values_controller.go new file mode 100644 index 0000000000..08eab10770 --- /dev/null +++ b/_benchmarks/iris-mvc2/controllers/values_controller.go @@ -0,0 +1,23 @@ +package controllers + +// ValuesController is the equivalent +// `ValuesController` of the .net core 2.0 mvc application. +type ValuesController struct{} + +/* on windows tests(older) the Get was: +func (vc *ValuesController) Get() { + // id,_ := vc.Params.GetInt("id") + // vc.Ctx.WriteString("value") +} +but as Iris is always going better, now supports return values as well*/ + +// Get handles "GET" requests to "api/values/{id}". +func (vc *ValuesController) Get() string { + return "value" +} + +// Put handles "PUT" requests to "api/values/{id}". +func (vc *ValuesController) Put() {} + +// Delete handles "DELETE" requests to "api/values/{id}". +func (vc *ValuesController) Delete() {} diff --git a/_benchmarks/iris-mvc2/main.go b/_benchmarks/iris-mvc2/main.go new file mode 100644 index 0000000000..6366ac2c32 --- /dev/null +++ b/_benchmarks/iris-mvc2/main.go @@ -0,0 +1,17 @@ +package main + +/// TODO: remove this on the "master" branch, or even replace it +// with the "iris-mvc" (the new implementatioin is even faster, close to handlers version, +// with bindings or without). + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/_benchmarks/iris-mvc2/controllers" + "github.com/kataras/iris/mvc2" +) + +func main() { + app := iris.New() + mvc2.New().Controller(app.Party("/api/values/{id}"), new(controllers.ValuesController)) + app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) +} diff --git a/configuration.go b/configuration.go index a171ae3e92..885555f2e2 100644 --- a/configuration.go +++ b/configuration.go @@ -32,7 +32,7 @@ func init() { filename := homeConfigurationFilename(".yml") c, err := parseYAML(filename) if err != nil { - // this error will be occured the first time that the configuration + // this error will be occurred the first time that the configuration // file doesn't exist. // Create the YAML-ONLY global configuration file now using the default configuration 'c'. // This is useful when we run multiple iris servers that share the same diff --git a/core/memstore/memstore.go b/core/memstore/memstore.go index 93e5a55797..2e161560d4 100644 --- a/core/memstore/memstore.go +++ b/core/memstore/memstore.go @@ -25,6 +25,13 @@ type ( Store []Entry ) +// GetByKindOrNil will try to get this entry's value of "k" kind, +// if value is not that kind it will NOT try to convert it the "k", instead +// it will return nil, except if boolean; then it will return false +// even if the value was not bool. +// +// If the "k" kind is not a string or int or int64 or bool +// then it will return the raw value of the entry as it's. func (e Entry) GetByKindOrNil(k reflect.Kind) interface{} { switch k { case reflect.String: diff --git a/mvc2/bind.go b/mvc2/bind.go index d305dc5009..3ee9cc5a6d 100644 --- a/mvc2/bind.go +++ b/mvc2/bind.go @@ -86,6 +86,20 @@ func makeBindObject(v reflect.Value) (b bindObject, err error) { return } +// newContextBindObject is being used on both targetFunc and targetStruct. +// if the func's input argument or the struct's field is a type of Context +// then we can do a fast binding using the ctxValue +// which is used as slice of reflect.Value, because of the final method's `Call`. +func newContextBindObject() *bindObject { + return &bindObject{ + Type: contextTyp, + BindType: functionResultType, + ReturnValue: func(ctxValue []reflect.Value) reflect.Value { + return ctxValue[0] + }, + } +} + func (b *bindObject) IsAssignable(to reflect.Type) bool { return equalTypes(b.Type, to) } @@ -120,6 +134,15 @@ func newTargetStruct(v reflect.Value, bindValues ...reflect.Value) *targetStruct fields := lookupFields(typ, nil) for _, f := range fields { + // if it's context then bind it directly here and continue to the next field. + if isContext(f.Type) { + s.Fields = append(s.Fields, &targetField{ + FieldIndex: f.Index, + Object: newContextBindObject(), + }) + continue + } + for _, val := range bindValues { // the binded values to the struct's fields. b, err := makeBindObject(val) @@ -147,12 +170,9 @@ func newTargetStruct(v reflect.Value, bindValues ...reflect.Value) *targetStruct func (s *targetStruct) Fill(destElem reflect.Value, ctx ...reflect.Value) { for _, f := range s.Fields { f.Object.Assign(ctx, func(v reflect.Value) { - // defer func() { - // if err := recover(); err != nil { - // fmt.Printf("for index: %#v on: %s where num fields are: %d\n", - // f.FieldIndex, f.Object.Type.String(), destElem.NumField()) - // } - // }() + // if isContext(v.Type()) { + // println("WTF BIND CONTEXT TYPE WHEN BASE CONTROLLER?") + // } destElem.FieldByIndex(f.FieldIndex).Set(v) }) } @@ -187,13 +207,7 @@ func newTargetFunc(fn reflect.Value, bindValues ...reflect.Value) *targetFunc { if isContext(inTyp) { s.Inputs = append(s.Inputs, &targetFuncInput{ InputIndex: i, - Object: &bindObject{ - Type: contextTyp, - BindType: functionResultType, - ReturnValue: func(ctxValue []reflect.Value) reflect.Value { - return ctxValue[0] - }, - }, + Object: newContextBindObject(), }) continue } diff --git a/mvc2/bind_values.go b/mvc2/bind_values.go new file mode 100644 index 0000000000..2a7bd41a2c --- /dev/null +++ b/mvc2/bind_values.go @@ -0,0 +1,99 @@ +package mvc2 + +import ( + "reflect" +) + +/// TODO: +// create another package because these bindings things are useful +// for other libraries I'm working on, so something like github.com/kataras/di +// will be great, combine these with the bind.go and controller's inside handler +// but generic things. + +type ValueStore []reflect.Value + +// Bind binds values to this controller, if you want to share +// binding values between controllers use the Engine's `Bind` function instead. +func (bv *ValueStore) Bind(values ...interface{}) { + for _, val := range values { + bv.bind(reflect.ValueOf(val)) + } +} + +func (bv *ValueStore) bind(v reflect.Value) { + if !goodVal(v) { + return + } + + *bv = append(*bv, v) +} + +// Unbind unbinds a binding value based on the type, +// it returns true if at least one field is not binded anymore. +// +// The "n" indicates the number of elements to remove, if <=0 then it's 1, +// this is useful because you may have bind more than one value to two or more fields +// with the same type. +func (bv *ValueStore) Unbind(value interface{}, n int) bool { + return bv.unbind(reflect.TypeOf(value), n) +} + +func (bv *ValueStore) unbind(typ reflect.Type, n int) (ok bool) { + input := *bv + for i, in := range input { + if equalTypes(in.Type(), typ) { + ok = true + input = input[:i+copy(input[i:], input[i+1:])] + if n > 1 { + continue + } + break + } + } + + *bv = input + + return +} + +// BindExists returns true if a binder responsible to +// bind and return a type of "typ" is already registered to this controller. +func (bv *ValueStore) BindExists(value interface{}) bool { + return bv.bindTypeExists(reflect.TypeOf(value)) +} + +func (bv *ValueStore) bindTypeExists(typ reflect.Type) bool { + input := *bv + for _, in := range input { + if equalTypes(in.Type(), typ) { + return true + } + } + return false +} + +// BindIfNotExists bind a value to the controller's field with the same type, +// if it's not binded already. +// +// Returns false if binded already or the value is not the proper one for binding, +// otherwise true. +func (bv *ValueStore) BindIfNotExists(value interface{}) bool { + return bv.bindIfNotExists(reflect.ValueOf(value)) +} + +func (bv *ValueStore) bindIfNotExists(v reflect.Value) bool { + var ( + typ = v.Type() // no element, raw things here. + ) + + if !goodVal(v) { + return false + } + + if bv.bindTypeExists(typ) { + return false + } + + bv.bind(v) + return true +} diff --git a/mvc2/controller.go b/mvc2/controller.go index 14dbd86f4a..e024841c5e 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -62,8 +62,10 @@ type C struct { var _ BaseController = &C{} -// BeginRequest starts the request by initializing the `Context` field. -func (c *C) BeginRequest(ctx context.Context) { c.Ctx = ctx } +// BeginRequest does nothing anymore, is here to complet ethe `BaseController` interface. +// BaseController is not required anymore, `Ctx` is binded automatically by the engine's +// wrapped Handler. +func (c *C) BeginRequest(ctx context.Context) {} // EndRequest does nothing, is here to complete the `BaseController` interface. func (c *C) EndRequest(ctx context.Context) {} @@ -91,13 +93,13 @@ type ControllerActivator struct { // are used to build the bindings, and we need this field // because we have 3 states (Engine.Input, OnActivate, Bind) // that we can add or override binding values. - input []reflect.Value + ValueStore // TODO: or ... this is dirty code I will have to re format it a bit tomorrow. // the bindings that comes from input (and Engine) and can be binded to the controller's(initRef) fields. bindings *targetStruct } -func newControllerActivator(router router.Party, controller BaseController, bindValues ...reflect.Value) *ControllerActivator { +func newControllerActivator(router router.Party, controller interface{}, bindValues ...reflect.Value) *ControllerActivator { var ( val = reflect.ValueOf(controller) typ = val.Type() @@ -128,6 +130,9 @@ func newControllerActivator(router router.Party, controller BaseController, bind // are being appended to the slice at the `parseMethods`, // if a new method is registered via `Handle` its function name // is also appended to that slice. + // + // TODO: now that BaseController is totally optionally + // we have to check if BeginRequest and EndRequest should be here. reservedMethods: []string{ "BeginRequest", "EndRequest", @@ -136,10 +141,9 @@ func newControllerActivator(router router.Party, controller BaseController, bind // set the input as []reflect.Value in order to be able // to check if a bind type is already exists, or even // override the structBindings that are being generated later on. - input: bindValues, + ValueStore: bindValues, } - c.parseMethods() return c } @@ -178,33 +182,13 @@ func (c *ControllerActivator) parseMethods() { // SetBindings will override any bindings with the new "values". func (c *ControllerActivator) SetBindings(values ...reflect.Value) { // set field index with matching binders, if any. - c.input = values + c.ValueStore = values c.bindings = newTargetStruct(c.Value, values...) } -// Bind binds values to this controller, if you want to share -// binding values between controllers use the Engine's `Bind` function instead. -func (c *ControllerActivator) Bind(values ...interface{}) { - for _, val := range values { - if v := reflect.ValueOf(val); goodVal(v) { - c.input = append(c.input, v) - } - } -} - -// BindTypeExists returns true if a binder responsible to -// bind and return a type of "typ" is already registered to this controller. -func (c *ControllerActivator) BindTypeExists(typ reflect.Type) bool { - for _, in := range c.input { - if equalTypes(in.Type(), typ) { - return true - } - } - return false -} - func (c *ControllerActivator) activate() { - c.SetBindings(c.input...) + c.SetBindings(c.ValueStore...) + c.parseMethods() } var emptyIn = []reflect.Value{} @@ -255,32 +239,50 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // get the function's input arguments' bindings. funcBindings := newTargetFunc(m.Func, pathParams...) - // the element value, not the pointer. - elemTyp := indirectTyp(c.Type) - // we will make use of 'n' to make a slice of reflect.Value // to pass into if the function has input arguments that // are will being filled by the funcBindings. n := len(funcIn) - handler := func(ctx context.Context) { + // the element value, not the pointer, wil lbe used to create a + // new controller on each incoming request. + elemTyp := indirectTyp(c.Type) + + implementsBase := isBaseController(c.Type) + handler := func(ctx context.Context) { // create a new controller instance of that type(>ptr). ctrl := reflect.New(elemTyp) - // the Interface(). is faster than MethodByName or pre-selected methods. - b := ctrl.Interface().(BaseController) - // init the request. - b.BeginRequest(ctx) - - // if begin request stopped the execution. - if ctx.IsStopped() { - return + + // // the Interface(). is faster than MethodByName or pre-selected methods. + // b := ctrl.Interface().(BaseController) + // // init the request. + // b.BeginRequest(ctx) + + // // if begin request stopped the execution. + // if ctx.IsStopped() { + // return + // } + + if implementsBase { + // the Interface(). is faster than MethodByName or pre-selected methods. + b := ctrl.Interface().(BaseController) + // init the request. + b.BeginRequest(ctx) + + // if begin request stopped the execution. + if ctx.IsStopped() { + return + } + + // EndRequest will be called at any case except the `BeginRequest` is + // stopped. + defer b.EndRequest(ctx) } if !c.bindings.Valid && !funcBindings.Valid { DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) } else { ctxValue := reflect.ValueOf(ctx) - if c.bindings.Valid { elem := ctrl.Elem() c.bindings.Fill(elem, ctxValue) @@ -309,11 +311,11 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . } - if ctx.IsStopped() { - return - } + // if ctx.IsStopped() { + // return + // } - b.EndRequest(ctx) + // b.EndRequest(ctx) } // register the handler now. diff --git a/mvc2/controller_test.go b/mvc2/controller_test.go index 677222e5f2..3c48c6a9fa 100644 --- a/mvc2/controller_test.go +++ b/mvc2/controller_test.go @@ -2,7 +2,6 @@ package mvc2_test import ( - "reflect" "testing" "github.com/kataras/iris" @@ -413,7 +412,7 @@ type testControllerActivateListener struct { } func (c *testControllerActivateListener) OnActivate(ca *ControllerActivator) { - if !ca.BindTypeExists(reflect.TypeOf(&testBindType{})) { + if !ca.BindExists(&testBindType{}) { ca.Bind(&testBindType{ title: "default title", }) diff --git a/mvc2/engine.go b/mvc2/engine.go index f421b26fc9..50693b3a0f 100644 --- a/mvc2/engine.go +++ b/mvc2/engine.go @@ -37,8 +37,8 @@ func (e *Engine) Child() *Engine { child := New() // copy the current parent's ctx func binders and services to this new child. - if l := len(e.Input); l > 0 { - input := make([]reflect.Value, l, l) + if n := len(e.Input); n > 0 { + input := make([]reflect.Value, n, n) copy(input, e.Input) child.Input = input } @@ -54,7 +54,7 @@ func (e *Engine) Handler(handler interface{}) context.Handler { return h } -func (e *Engine) Controller(router router.Party, controller BaseController, onActivate ...func(*ControllerActivator)) { +func (e *Engine) Controller(router router.Party, controller interface{}, onActivate ...func(*ControllerActivator)) { ca := newControllerActivator(router, controller, e.Input...) // give a priority to the "onActivate" diff --git a/mvc2/reflect.go b/mvc2/reflect.go index 4405d7840d..1da5818787 100644 --- a/mvc2/reflect.go +++ b/mvc2/reflect.go @@ -8,10 +8,16 @@ import ( "github.com/kataras/pkg/zerocheck" ) -var contextTyp = reflect.TypeOf(context.NewContext(nil)) +var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem() + +func isBaseController(ctrlTyp reflect.Type) bool { + return ctrlTyp.Implements(baseControllerTyp) +} + +var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem() func isContext(inTyp reflect.Type) bool { - return inTyp.String() == "context.Context" // I couldn't find another way; context/context.go is not exported. + return inTyp.Implements(contextTyp) } func indirectVal(v reflect.Value) reflect.Value { diff --git a/mvc2/session_controller.go b/mvc2/session_controller.go index 6ca5bce4b0..8c88f8636d 100644 --- a/mvc2/session_controller.go +++ b/mvc2/session_controller.go @@ -1,9 +1,6 @@ package mvc2 import ( - "reflect" - - "github.com/kataras/golog" "github.com/kataras/iris/context" "github.com/kataras/iris/sessions" ) @@ -25,11 +22,11 @@ type SessionController struct { // It makes sure that its "Manager" field is filled // even if the caller didn't provide any sessions manager via the `app.Controller` function. func (s *SessionController) OnActivate(ca *ControllerActivator) { - if !ca.BindTypeExists(reflect.TypeOf(defaultManager)) { - ca.Bind(defaultManager) - golog.Warnf(`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, -therefore this controller is using the default sessions manager instead. -Please refer to the documentation to learn how you can provide the session manager`) + if didntBindManually := ca.BindIfNotExists(defaultManager); didntBindManually { + ca.Router.GetReporter().Add( + `MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, + therefore this controller is using the default sessions manager instead. + Please refer to the documentation to learn how you can provide the session manager`) } } From a38e29b1bf919b26540803ee39be9831009f81a6 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 14 Dec 2017 03:30:39 +0200 Subject: [PATCH 17/79] add test for binding a whole function as an input argument on the handler's function - worked --- mvc2/handler_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/mvc2/handler_test.go b/mvc2/handler_test.go index ee9962fd34..205b1602a5 100644 --- a/mvc2/handler_test.go +++ b/mvc2/handler_test.go @@ -101,3 +101,29 @@ func testAppWithMvcHandlers(t *testing.T, h1, h2, h3 iris.Handler) { e.GET("/param/the_param_value").Expect().Status(httptest.StatusOK). Body().Equal("param is: the_param_value") } + +// TestBindFunctionAsFunctionInputArgument tests to bind +// a whole dynamic function based on the current context +// as an input argument in the mvc-like handler's function. +func TestBindFunctionAsFunctionInputArgument(t *testing.T) { + app := iris.New() + postsBinder := func(ctx iris.Context) func(string) string { + return ctx.PostValue // or FormValue, the same here. + } + + h := MustMakeHandler(func(get func(string) string) string { + // send the `ctx.PostValue/FormValue("username")` value + // to the client. + return get("username") + }, + // bind the function binder. + reflect.ValueOf(postsBinder)) + + app.Post("/", h) + + e := httptest.New(t, app) + + expectedUsername := "kataras" + e.POST("/").WithFormField("username", expectedUsername). + Expect().Status(iris.StatusOK).Body().Equal(expectedUsername) +} From 8d4721a5a855ebce0d53621b939886dac6f63078 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 14 Dec 2017 05:56:23 +0200 Subject: [PATCH 18/79] start of the vue + mvc example and a simple session binding --- .../tutorial/vuejs-todo-mvc/src/todo/item.go | 24 +++ .../vuejs-todo-mvc/src/todo/service.go | 53 ++++++ .../src/web/controllers/todo_controller.go | 73 ++++++++ .../tutorial/vuejs-todo-mvc/src/web/main.go | 36 ++++ .../vuejs-todo-mvc/src/web/public/css/index | 2 + .../vuejs-todo-mvc/src/web/public/index.html | 63 +++++++ .../vuejs-todo-mvc/src/web/public/js/app.js | 157 ++++++++++++++++++ .../vuejs-todo-mvc/src/web/public/js/lib/vue | 2 + mvc2/controller.go | 1 - mvc2/controller_test.go | 9 + mvc2/session_binder.go | 17 ++ mvc2/session_controller.go | 4 +- 12 files changed, 438 insertions(+), 3 deletions(-) create mode 100644 _examples/tutorial/vuejs-todo-mvc/src/todo/item.go create mode 100644 _examples/tutorial/vuejs-todo-mvc/src/todo/service.go create mode 100644 _examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go create mode 100644 _examples/tutorial/vuejs-todo-mvc/src/web/main.go create mode 100644 _examples/tutorial/vuejs-todo-mvc/src/web/public/css/index create mode 100644 _examples/tutorial/vuejs-todo-mvc/src/web/public/index.html create mode 100644 _examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js create mode 100644 _examples/tutorial/vuejs-todo-mvc/src/web/public/js/lib/vue create mode 100644 mvc2/session_binder.go diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go new file mode 100644 index 0000000000..744880c4c7 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go @@ -0,0 +1,24 @@ +package todo + +type State uint32 + +const ( + StateActive State = iota + StateCompleted +) + +func ParseState(s string) State { + switch s { + case "completed": + return StateCompleted + default: + return StateActive + } +} + +type Item struct { + OwnerID string + ID int64 + Body string + CurrentState State +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go new file mode 100644 index 0000000000..3a8f75a6a9 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go @@ -0,0 +1,53 @@ +package todo + +type Service interface { + GetByID(id int64) (Item, bool) + GetByOwner(owner string) []Item + Complete(item Item) bool + Save(newItem Item) error +} + +type MemoryService struct { + items map[int64]Item +} + +func (s *MemoryService) getLatestID() (id int64) { + for k := range s.items { + if k > id { + id = k + } + } + + return +} + +func (s *MemoryService) GetByID(id int64) (Item, bool) { + item, found := s.items[id] + return item, found +} + +func (s *MemoryService) GetByOwner(owner string) (items []Item) { + for _, item := range s.items { + if item.OwnerID != owner { + continue + } + items = append(items, item) + } + return +} + +func (s *MemoryService) Complete(item Item) bool { + item.CurrentState = StateCompleted + return s.Save(item) == nil +} + +func (s *MemoryService) Save(newItem Item) error { + if newItem.ID == 0 { + // create + newItem.ID = s.getLatestID() + 1 + } + + // full replace here for the shake of simplicy) + s.items[newItem.ID] = newItem + return nil +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go new file mode 100644 index 0000000000..69923ea224 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -0,0 +1,73 @@ +package controllers + +import ( + "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo" + + "github.com/kataras/iris" + mvc "github.com/kataras/iris/mvc2" + "github.com/kataras/iris/sessions" +) + +// TodoController is our TODO app's web controller. +type TodoController struct { + service todo.Service + + session *sessions.Session +} + +// OnActivate called once before the server ran, can bind custom +// things to the controller. +func (c *TodoController) OnActivate(ca *mvc.ControllerActivator) { + // this could be binded to a controller's function input argument + // if any, or struct field if any: + ca.Bind(func(ctx iris.Context) todo.Item { + // ctx.ReadForm(&item) + var ( + owner = ctx.PostValue("owner") + body = ctx.PostValue("body") + state = ctx.PostValue("state") + ) + + return todo.Item{ + OwnerID: owner, + Body: body, + CurrentState: todo.ParseState(state), + } + }) + +} + +// Get handles the GET: /todo route. +func (c *TodoController) Get() []todo.Item { + return c.service.GetByOwner(c.session.ID()) +} + +// PutCompleteBy handles the PUT: /todo/complete/{id:long} route. +func (c *TodoController) PutCompleteBy(id int64) int { + item, found := c.service.GetByID(id) + if !found { + return iris.StatusNotFound + } + + if item.OwnerID != c.session.ID() { + return iris.StatusForbidden + } + + if !c.service.Complete(item) { + return iris.StatusBadRequest + } + + return iris.StatusOK +} + +// Post handles the POST: /todo route. +func (c *TodoController) Post(newItem todo.Item) int { + if newItem.OwnerID != c.session.ID() { + return iris.StatusForbidden + } + + if err := c.service.Save(newItem); err != nil { + return iris.StatusBadRequest + } + return iris.StatusOK +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go new file mode 100644 index 0000000000..ee3c875e48 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo" + "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/web/controllers" + + "github.com/kataras/iris" + mvc "github.com/kataras/iris/mvc2" + "github.com/kataras/iris/sessions" +) + +func main() { + app := iris.New() + // serve our app in public, public folder + // contains the client-side vue.js application, + // no need for any server-side template here, + // actually if you're going to just use vue without any + // back-end services, you can just stop afer this line and start the server. + app.StaticWeb("/", "./public") + + sess := sessions.New(sessions.Config{ + Cookie: "_iris_session", + }) + + m := mvc.New() + + // any bindings here... + m.Bind(mvc.Session(sess)) + + m.Bind(new(todo.MemoryService)) + // controllers registration here... + m.Controller(app.Party("/todo"), new(controllers.TodoController)) + + // start the web server at http://localhost:8080 + app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker, iris.WithOptimizations) +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/css/index b/_examples/tutorial/vuejs-todo-mvc/src/web/public/css/index new file mode 100644 index 0000000000..3aea0097d7 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/css/index @@ -0,0 +1,2 @@ +index.css is not here to reduce the disk space for the examples. +https://unpkg.com/todomvc-app-css@2.0.4/index.css is used instead. \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html new file mode 100644 index 0000000000..f282a65d30 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html @@ -0,0 +1,63 @@ + + + + + Iris + Vue.js • TodoMVC + + + + + + + +
+
+

todos

+ +
+
+ +
    +
  • +
    + + + +
    + +
  • +
+
+
+ + {{ remaining }} {{ remaining | pluralize }} left + + + +
+
+
+

Double-click to edit a todo

+
+ + + + \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js new file mode 100644 index 0000000000..248c5333bc --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js @@ -0,0 +1,157 @@ +// Full spec-compliant TodoMVC with localStorage persistence +// and hash-based routing in ~120 effective lines of JavaScript. + +// localStorage persistence +var STORAGE_KEY = 'todos-vuejs-2.0' +var todoStorage = { + fetch: function () { + var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') + todos.forEach(function (todo, index) { + todo.id = index + }) + todoStorage.uid = todos.length + return todos + }, + save: function (todos) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)) + } +} + +// visibility filters +var filters = { + all: function (todos) { + return todos + }, + active: function (todos) { + return todos.filter(function (todo) { + return !todo.completed + }) + }, + completed: function (todos) { + return todos.filter(function (todo) { + return todo.completed + }) + } +} + +// app Vue instance +var app = new Vue({ + // app initial state + data: { + todos: todoStorage.fetch(), + newTodo: '', + editedTodo: null, + visibility: 'all' + }, + + // watch todos change for localStorage persistence + watch: { + todos: { + handler: function (todos) { + todoStorage.save(todos) + }, + deep: true + } + }, + + // computed properties + // http://vuejs.org/guide/computed.html + computed: { + filteredTodos: function () { + return filters[this.visibility](this.todos) + }, + remaining: function () { + return filters.active(this.todos).length + }, + allDone: { + get: function () { + return this.remaining === 0 + }, + set: function (value) { + this.todos.forEach(function (todo) { + todo.completed = value + }) + } + } + }, + + filters: { + pluralize: function (n) { + return n === 1 ? 'item' : 'items' + } + }, + + // methods that implement data logic. + // note there's no DOM manipulation here at all. + methods: { + addTodo: function () { + var value = this.newTodo && this.newTodo.trim() + if (!value) { + return + } + this.todos.push({ + id: todoStorage.uid++, + title: value, + completed: false + }) + this.newTodo = '' + }, + + removeTodo: function (todo) { + this.todos.splice(this.todos.indexOf(todo), 1) + }, + + editTodo: function (todo) { + this.beforeEditCache = todo.title + this.editedTodo = todo + }, + + doneEdit: function (todo) { + if (!this.editedTodo) { + return + } + this.editedTodo = null + todo.title = todo.title.trim() + if (!todo.title) { + this.removeTodo(todo) + } + }, + + cancelEdit: function (todo) { + this.editedTodo = null + todo.title = this.beforeEditCache + }, + + removeCompleted: function () { + this.todos = filters.active(this.todos) + } + }, + + // a custom directive to wait for the DOM to be updated + // before focusing on the input field. + // http://vuejs.org/guide/custom-directive.html + directives: { + 'todo-focus': function (el, binding) { + if (binding.value) { + el.focus() + } + } + } +}) + +// handle routing +function onHashChange () { + var visibility = window.location.hash.replace(/#\/?/, '') + if (filters[visibility]) { + app.visibility = visibility + } else { + window.location.hash = '' + app.visibility = 'all' + } +} + +window.addEventListener('hashchange', onHashChange) +onHashChange() + +// mount +app.$mount('.todoapp') \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/lib/vue b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/lib/vue new file mode 100644 index 0000000000..8da03f4896 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/lib/vue @@ -0,0 +1,2 @@ +vue.js is not here to reduce the disk space for the examples. +Instead https://vuejs.org/js/vue.js is used instead. \ No newline at end of file diff --git a/mvc2/controller.go b/mvc2/controller.go index e024841c5e..4afad118a9 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -160,7 +160,6 @@ func (c *ControllerActivator) isReservedMethod(name string) bool { // register all available, exported methods to handlers if possible. func (c *ControllerActivator) parseMethods() { - n := c.Type.NumMethod() for i := 0; i < n; i++ { m := c.Type.Method(i) diff --git a/mvc2/controller_test.go b/mvc2/controller_test.go index 3c48c6a9fa..d0079f6be3 100644 --- a/mvc2/controller_test.go +++ b/mvc2/controller_test.go @@ -260,6 +260,12 @@ func (t *testControllerBindStruct) Get() { t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) } +// test if context can be binded to the controller's function +// without need to declare it to a struct if not needed. +func (t *testControllerBindStruct) GetCtx(ctx iris.Context) { + ctx.StatusCode(iris.StatusContinue) +} + type testControllerBindDeep struct { testControllerBindStruct } @@ -268,6 +274,7 @@ func (t *testControllerBindDeep) Get() { // t.testControllerBindStruct.Get() t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) } + func TestControllerBind(t *testing.T) { app := iris.New() // app.Logger().SetLevel("debug") @@ -287,6 +294,8 @@ func TestControllerBind(t *testing.T) { expected := t1 + t2 e.GET("/").Expect().Status(iris.StatusOK). Body().Equal(expected) + e.GET("/ctx").Expect().Status(iris.StatusContinue) + e.GET("/deep").Expect().Status(iris.StatusOK). Body().Equal(expected) } diff --git a/mvc2/session_binder.go b/mvc2/session_binder.go new file mode 100644 index 0000000000..173a3c8ca1 --- /dev/null +++ b/mvc2/session_binder.go @@ -0,0 +1,17 @@ +package mvc2 + +import ( + "github.com/kataras/iris/context" + "github.com/kataras/iris/sessions" +) + +// Session -> TODO: think of move all bindings to +// a different folder like "bindings" +// so it will be used as .Bind(bindings.Session(manager)) +// or let it here but change the rest of the binding names as well +// because they are not "binders", their result are binders to be percise. +func Session(sess *sessions.Sessions) func(context.Context) *sessions.Session { + return func(ctx context.Context) *sessions.Session { + return sess.Start(ctx) + } +} diff --git a/mvc2/session_controller.go b/mvc2/session_controller.go index 8c88f8636d..e369e877f3 100644 --- a/mvc2/session_controller.go +++ b/mvc2/session_controller.go @@ -5,7 +5,7 @@ import ( "github.com/kataras/iris/sessions" ) -var defaultManager = sessions.New(sessions.Config{}) +var defaultSessionManager = sessions.New(sessions.Config{}) // SessionController is a simple `Controller` implementation // which requires a binded session manager in order to give @@ -22,7 +22,7 @@ type SessionController struct { // It makes sure that its "Manager" field is filled // even if the caller didn't provide any sessions manager via the `app.Controller` function. func (s *SessionController) OnActivate(ca *ControllerActivator) { - if didntBindManually := ca.BindIfNotExists(defaultManager); didntBindManually { + if didntBindManually := ca.BindIfNotExists(defaultSessionManager); didntBindManually { ca.Router.GetReporter().Add( `MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, therefore this controller is using the default sessions manager instead. From 6775c560a0dde6bb106c2d961b83fa6dad39e30c Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 14 Dec 2017 23:04:42 +0200 Subject: [PATCH 19/79] create one generic package for dependency injection which can be used outside of Iris too - worked but unfished --- .../controllers/values_controller.go | 4 +- _examples/http_responsewriter/hero/app.go | 2 +- mvc2/bind.go | 254 ++---------------- mvc2/controller.go | 85 +++--- mvc2/controller_test.go | 4 +- mvc2/engine.go | 29 +- mvc2/func_result_test.go | 2 +- mvc2/handler.go | 7 +- mvc2/reflect.go | 4 + mvc2/session_controller.go | 2 +- mvc2/vendor/github.com/kataras/di/README.md | 7 + .../kataras/di/_examples/basic/main.go | 62 +++++ mvc2/vendor/github.com/kataras/di/di.go | 68 +++++ mvc2/vendor/github.com/kataras/di/doc.go | 1 + mvc2/vendor/github.com/kataras/di/func.go | 105 ++++++++ mvc2/vendor/github.com/kataras/di/object.go | 102 +++++++ mvc2/vendor/github.com/kataras/di/reflect.go | 199 ++++++++++++++ mvc2/vendor/github.com/kataras/di/struct.go | 84 ++++++ .../github.com/kataras/di/values.go} | 36 +-- 19 files changed, 722 insertions(+), 335 deletions(-) create mode 100644 mvc2/vendor/github.com/kataras/di/README.md create mode 100644 mvc2/vendor/github.com/kataras/di/_examples/basic/main.go create mode 100644 mvc2/vendor/github.com/kataras/di/di.go create mode 100644 mvc2/vendor/github.com/kataras/di/doc.go create mode 100644 mvc2/vendor/github.com/kataras/di/func.go create mode 100644 mvc2/vendor/github.com/kataras/di/object.go create mode 100644 mvc2/vendor/github.com/kataras/di/reflect.go create mode 100644 mvc2/vendor/github.com/kataras/di/struct.go rename mvc2/{bind_values.go => vendor/github.com/kataras/di/values.go} (67%) diff --git a/_benchmarks/iris-mvc2/controllers/values_controller.go b/_benchmarks/iris-mvc2/controllers/values_controller.go index 08eab10770..80ac5ba7b7 100644 --- a/_benchmarks/iris-mvc2/controllers/values_controller.go +++ b/_benchmarks/iris-mvc2/controllers/values_controller.go @@ -1,8 +1,10 @@ package controllers +// import "github.com/kataras/iris/mvc2" + // ValuesController is the equivalent // `ValuesController` of the .net core 2.0 mvc application. -type ValuesController struct{} +type ValuesController struct{} //{ mvc2.C } /* on windows tests(older) the Get was: func (vc *ValuesController) Get() { diff --git a/_examples/http_responsewriter/hero/app.go b/_examples/http_responsewriter/hero/app.go index f25bb9e1da..bf5c1ecc59 100644 --- a/_examples/http_responsewriter/hero/app.go +++ b/_examples/http_responsewriter/hero/app.go @@ -46,7 +46,7 @@ func main() { // using an io.Writer for automatic buffer management (i.e. hero built-in buffer pool), // iris context implements the io.Writer by its ResponseWriter - // which is an enhanced version of the standar http.ResponseWriter + // which is an enhanced version of the standard http.ResponseWriter // but still 100% compatible. template.UserListToWriter(userList, ctx) }) diff --git a/mvc2/bind.go b/mvc2/bind.go index 3ee9cc5a6d..5cfb859351 100644 --- a/mvc2/bind.go +++ b/mvc2/bind.go @@ -1,258 +1,34 @@ package mvc2 -import "reflect" - -type bindType uint32 - -const ( - objectType bindType = iota // simple assignable value. - functionResultType // dynamic value, depends on the context. +import ( + "github.com/kataras/di" + "reflect" ) -type bindObject struct { - Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' . - Value reflect.Value - - BindType bindType - ReturnValue func(ctx []reflect.Value) reflect.Value -} - -// makeReturnValue takes any function -// that accept a context and returns something -// and returns a binder function, which accepts the context as slice of reflect.Value -// and returns a reflect.Value for that. -// Iris uses to -// resolve and set the input parameters when a handler is executed. -// -// The "fn" can have the following form: -// `func(iris.Context) UserViewModel`. -// -// The return type of the "fn" 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 makeReturnValue(fn reflect.Value) (func([]reflect.Value) reflect.Value, reflect.Type, error) { - typ := indirectTyp(fn.Type()) - - // invalid if not a func. - if typ.Kind() != reflect.Func { - return nil, typ, errBad - } - - // invalid if not returns one single value. - if typ.NumOut() != 1 { - return nil, typ, errBad - } - - // invalid if input args length is not one. - if typ.NumIn() != 1 { - return nil, typ, errBad - } - - // invalid if that single input arg is not a typeof context.Context. - if !isContext(typ.In(0)) { - return nil, typ, errBad +var ( + typeChecker = func(fn reflect.Type) bool { + // invalid if that single input arg is not a typeof context.Context. + return isContext(fn.In(0)) } - outTyp := typ.Out(0) - zeroOutVal := reflect.New(outTyp).Elem() - - bf := func(ctxValue []reflect.Value) reflect.Value { - results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler. - if len(results) == 0 { - return zeroOutVal + hijacker = func(fieldOrFuncInput reflect.Type) (*di.BindObject, bool) { + if isContext(fieldOrFuncInput) { + return newContextBindObject(), true } - - v := results[0] - if !v.IsValid() { - return zeroOutVal - } - return v - } - - return bf, outTyp, nil -} - -func makeBindObject(v reflect.Value) (b bindObject, err error) { - if isFunc(v) { - b.BindType = functionResultType - b.ReturnValue, b.Type, err = makeReturnValue(v) - } else { - b.BindType = objectType - b.Type = v.Type() - b.Value = v + return nil, false } - - return -} +) // newContextBindObject is being used on both targetFunc and targetStruct. // if the func's input argument or the struct's field is a type of Context // then we can do a fast binding using the ctxValue // which is used as slice of reflect.Value, because of the final method's `Call`. -func newContextBindObject() *bindObject { - return &bindObject{ +func newContextBindObject() *di.BindObject { + return &di.BindObject{ Type: contextTyp, - BindType: functionResultType, + BindType: di.Dynamic, ReturnValue: func(ctxValue []reflect.Value) reflect.Value { return ctxValue[0] }, } } - -func (b *bindObject) IsAssignable(to reflect.Type) bool { - return equalTypes(b.Type, to) -} - -func (b *bindObject) Assign(ctx []reflect.Value, toSetter func(reflect.Value)) { - if b.BindType == functionResultType { - toSetter(b.ReturnValue(ctx)) - return - } - toSetter(b.Value) -} - -type ( - targetField struct { - Object *bindObject - FieldIndex []int - } - targetFuncInput struct { - Object *bindObject - InputIndex int - } -) - -type targetStruct struct { - Fields []*targetField - Valid bool // is True when contains fields and it's a valid target struct. -} - -func newTargetStruct(v reflect.Value, bindValues ...reflect.Value) *targetStruct { - typ := indirectTyp(v.Type()) - s := &targetStruct{} - - fields := lookupFields(typ, nil) - for _, f := range fields { - // if it's context then bind it directly here and continue to the next field. - if isContext(f.Type) { - s.Fields = append(s.Fields, &targetField{ - FieldIndex: f.Index, - Object: newContextBindObject(), - }) - continue - } - - for _, val := range bindValues { - // the binded values to the struct's fields. - b, err := makeBindObject(val) - - if err != nil { - return s // if error stop here. - } - - if b.IsAssignable(f.Type) { - // fmt.Printf("bind the object to the field: %s at index: %#v and type: %s\n", f.Name, f.Index, f.Type.String()) - s.Fields = append(s.Fields, &targetField{ - FieldIndex: f.Index, - Object: &b, - }) - break - } - - } - } - - s.Valid = len(s.Fields) > 0 - return s -} - -func (s *targetStruct) Fill(destElem reflect.Value, ctx ...reflect.Value) { - for _, f := range s.Fields { - f.Object.Assign(ctx, func(v reflect.Value) { - // if isContext(v.Type()) { - // println("WTF BIND CONTEXT TYPE WHEN BASE CONTROLLER?") - // } - destElem.FieldByIndex(f.FieldIndex).Set(v) - }) - } -} - -type targetFunc struct { - Inputs []*targetFuncInput - Valid bool // is True when contains func inputs and it's a valid target func. -} - -func newTargetFunc(fn reflect.Value, bindValues ...reflect.Value) *targetFunc { - typ := indirectTyp(fn.Type()) - s := &targetFunc{ - Valid: false, - } - - if !isFunc(typ) { - return s - } - - n := typ.NumIn() - - // function input can have many values of the same types, - // so keep track of them in order to not set a func input to a next bind value, - // i.e (string, string) with two different binder funcs because of the different param's name. - consumedValues := make(map[int]bool, n) - - for i := 0; i < n; i++ { - inTyp := typ.In(i) - - // if it's context then bind it directly here and continue to the next func's input arg. - if isContext(inTyp) { - s.Inputs = append(s.Inputs, &targetFuncInput{ - InputIndex: i, - Object: newContextBindObject(), - }) - continue - } - - for valIdx, val := range bindValues { - if _, shouldSkip := consumedValues[valIdx]; shouldSkip { - continue - } - inTyp := typ.In(i) - - // the binded values to the func's inputs. - b, err := makeBindObject(val) - - if err != nil { - return s // if error stop here. - } - - if b.IsAssignable(inTyp) { - // fmt.Printf("binded input index: %d for type: %s and value: %v with pointer: %v\n", - // i, b.Type.String(), val.String(), val.Pointer()) - s.Inputs = append(s.Inputs, &targetFuncInput{ - InputIndex: i, - Object: &b, - }) - - consumedValues[valIdx] = true - break - } - } - } - - s.Valid = len(s.Inputs) > 0 - return s -} - -func (s *targetFunc) Fill(in *[]reflect.Value, ctx ...reflect.Value) { - args := *in - for _, input := range s.Inputs { - input.Object.Assign(ctx, func(v reflect.Value) { - // fmt.Printf("assign input index: %d for value: %v\n", - // input.InputIndex, v.String()) - args[input.InputIndex] = v - }) - - } - - *in = args -} diff --git a/mvc2/controller.go b/mvc2/controller.go index 4afad118a9..4865644c19 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -4,6 +4,8 @@ import ( "fmt" "reflect" + "github.com/kataras/di" + "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" "github.com/kataras/iris/core/router/macro" @@ -89,17 +91,16 @@ type ControllerActivator struct { // the BeginRequest, EndRequest and OnActivate are reserved by the internal implementation. reservedMethods []string - // input are always empty after the `activate` - // are used to build the bindings, and we need this field - // because we have 3 states (Engine.Input, OnActivate, Bind) - // that we can add or override binding values. - ValueStore // TODO: or ... this is dirty code I will have to re format it a bit tomorrow. + // the bindings that comes from the Engine and the controller's filled fields if any. + // Can be binded to the the new controller's fields and method that is fired + // on incoming requests. + Dependencies *di.D - // the bindings that comes from input (and Engine) and can be binded to the controller's(initRef) fields. - bindings *targetStruct + // on activate. + injector *di.StructInjector } -func newControllerActivator(router router.Party, controller interface{}, bindValues ...reflect.Value) *ControllerActivator { +func newControllerActivator(router router.Party, controller interface{}, d *di.D) *ControllerActivator { var ( val = reflect.ValueOf(controller) typ = val.Type() @@ -115,7 +116,7 @@ func newControllerActivator(router router.Party, controller interface{}, bindVal // the end-developer when declaring the controller, // activate listeners needs them in order to know if something set-ed already or not, // look `BindTypeExists`. - bindValues = append(lookupNonZeroFieldsValues(val), bindValues...) + d.Values = append(lookupNonZeroFieldsValues(val), d.Values...) c := &ControllerActivator{ // give access to the Router to the end-devs if they need it for some reason, @@ -133,20 +134,22 @@ func newControllerActivator(router router.Party, controller interface{}, bindVal // // TODO: now that BaseController is totally optionally // we have to check if BeginRequest and EndRequest should be here. - reservedMethods: []string{ - "BeginRequest", - "EndRequest", - "OnActivate", - }, - // set the input as []reflect.Value in order to be able - // to check if a bind type is already exists, or even - // override the structBindings that are being generated later on. - ValueStore: bindValues, + reservedMethods: whatReservedMethods(typ), + Dependencies: d, } return c } +func whatReservedMethods(typ reflect.Type) []string { + methods := []string{"OnActivate"} + if isBaseController(typ) { + methods = append(methods, "BeginRequest", "EndRequest") + } + + return methods +} + // checks if a method is already registered. func (c *ControllerActivator) isReservedMethod(name string) bool { for _, s := range c.reservedMethods { @@ -178,15 +181,8 @@ func (c *ControllerActivator) parseMethods() { } } -// SetBindings will override any bindings with the new "values". -func (c *ControllerActivator) SetBindings(values ...reflect.Value) { - // set field index with matching binders, if any. - c.ValueStore = values - c.bindings = newTargetStruct(c.Value, values...) -} - func (c *ControllerActivator) activate() { - c.SetBindings(c.ValueStore...) + c.injector = c.Dependencies.Struct(c.Value) c.parseMethods() } @@ -236,11 +232,13 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // end-dev's controller pointer. pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...) // get the function's input arguments' bindings. - funcBindings := newTargetFunc(m.Func, pathParams...) + funcDependencies := c.Dependencies.Clone() + funcDependencies.Add(pathParams...) + funcInjector := funcDependencies.Func(m.Func) // we will make use of 'n' to make a slice of reflect.Value // to pass into if the function has input arguments that - // are will being filled by the funcBindings. + // are will being filled by the funcDependencies. n := len(funcIn) // the element value, not the pointer, wil lbe used to create a // new controller on each incoming request. @@ -249,19 +247,8 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . implementsBase := isBaseController(c.Type) handler := func(ctx context.Context) { - // create a new controller instance of that type(>ptr). ctrl := reflect.New(elemTyp) - // // the Interface(). is faster than MethodByName or pre-selected methods. - // b := ctrl.Interface().(BaseController) - // // init the request. - // b.BeginRequest(ctx) - - // // if begin request stopped the execution. - // if ctx.IsStopped() { - // return - // } - if implementsBase { // the Interface(). is faster than MethodByName or pre-selected methods. b := ctrl.Interface().(BaseController) @@ -273,34 +260,32 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . return } - // EndRequest will be called at any case except the `BeginRequest` is - // stopped. defer b.EndRequest(ctx) } - if !c.bindings.Valid && !funcBindings.Valid { + if !c.injector.Valid && !funcInjector.Valid { DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) } else { ctxValue := reflect.ValueOf(ctx) - if c.bindings.Valid { + if c.injector.Valid { elem := ctrl.Elem() - c.bindings.Fill(elem, ctxValue) + c.injector.InjectElem(elem, ctxValue) if ctx.IsStopped() { return } // we do this in order to reduce in := make... // if not func input binders, we execute the handler with empty input args. - if !funcBindings.Valid { + if !funcInjector.Valid { DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) } } // otherwise, it has one or more valid input binders, // make the input and call the func using those. - if funcBindings.Valid { + if funcInjector.Valid { in := make([]reflect.Value, n, n) in[0] = ctrl - funcBindings.Fill(&in, ctxValue) + funcInjector.Inject(&in, ctxValue) if ctx.IsStopped() { return } @@ -309,12 +294,6 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . } } - - // if ctx.IsStopped() { - // return - // } - - // b.EndRequest(ctx) } // register the handler now. diff --git a/mvc2/controller_test.go b/mvc2/controller_test.go index d0079f6be3..785cf13342 100644 --- a/mvc2/controller_test.go +++ b/mvc2/controller_test.go @@ -421,8 +421,8 @@ type testControllerActivateListener struct { } func (c *testControllerActivateListener) OnActivate(ca *ControllerActivator) { - if !ca.BindExists(&testBindType{}) { - ca.Bind(&testBindType{ + if !ca.Dependencies.BindExists(&testBindType{}) { + ca.Dependencies.Bind(&testBindType{ title: "default title", }) } diff --git a/mvc2/engine.go b/mvc2/engine.go index 50693b3a0f..59454752f3 100644 --- a/mvc2/engine.go +++ b/mvc2/engine.go @@ -2,9 +2,10 @@ package mvc2 import ( "errors" - "reflect" + "github.com/kataras/di" "github.com/kataras/golog" + "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" ) @@ -16,38 +17,28 @@ var ( ) type Engine struct { - Input []reflect.Value + dependencies *di.D } func New() *Engine { - return new(Engine) + return &Engine{ + dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker), + } } func (e *Engine) Bind(values ...interface{}) *Engine { - for _, val := range values { - if v := reflect.ValueOf(val); goodVal(v) { - e.Input = append(e.Input, v) - } - } - + e.dependencies.Bind(values...) return e } func (e *Engine) Child() *Engine { child := New() - - // copy the current parent's ctx func binders and services to this new child. - if n := len(e.Input); n > 0 { - input := make([]reflect.Value, n, n) - copy(input, e.Input) - child.Input = input - } - + child.dependencies = e.dependencies.Clone() return child } func (e *Engine) Handler(handler interface{}) context.Handler { - h, err := MakeHandler(handler, e.Input...) + h, err := MakeHandler(handler, e.dependencies.Values...) if err != nil { golog.Errorf("mvc handler: %v", err) } @@ -55,7 +46,7 @@ func (e *Engine) Handler(handler interface{}) context.Handler { } func (e *Engine) Controller(router router.Party, controller interface{}, onActivate ...func(*ControllerActivator)) { - ca := newControllerActivator(router, controller, e.Input...) + ca := newControllerActivator(router, controller, e.dependencies) // give a priority to the "onActivate" // callbacks, if any. diff --git a/mvc2/func_result_test.go b/mvc2/func_result_test.go index 5df3b07224..240a044ba6 100644 --- a/mvc2/func_result_test.go +++ b/mvc2/func_result_test.go @@ -267,7 +267,7 @@ func (t *testControllerViewResultRespectCtxViewData) Get() Result { func TestControllerViewResultRespectCtxViewData(t *testing.T) { app := iris.New() New().Controller(app, new(testControllerViewResultRespectCtxViewData), func(ca *ControllerActivator) { - ca.Bind(t) + ca.Dependencies.Bind(t) }) e := httptest.New(t, app) diff --git a/mvc2/handler.go b/mvc2/handler.go index e35ab7bdb7..994186779a 100644 --- a/mvc2/handler.go +++ b/mvc2/handler.go @@ -2,6 +2,7 @@ package mvc2 import ( "fmt" + "github.com/kataras/di" "reflect" "runtime" @@ -64,7 +65,7 @@ func MakeHandler(handler interface{}, bindValues ...reflect.Value) (context.Hand return h, nil } - s := newTargetFunc(fn, bindValues...) + s := di.MakeFuncInjector(fn, hijacker, typeChecker, bindValues...) if !s.Valid { pc := fn.Pointer() fpc := runtime.FuncForPC(pc) @@ -72,14 +73,14 @@ func MakeHandler(handler interface{}, bindValues ...reflect.Value) (context.Hand callerName := fpc.Name() err := fmt.Errorf("input arguments length(%d) and valid binders length(%d) are not equal for typeof '%s' which is defined at %s:%d by %s", - n, len(s.Inputs), fn.Type().String(), callerFileName, callerLineNumber, callerName) + n, s.Length, fn.Type().String(), callerFileName, callerLineNumber, callerName) return nil, err } h := func(ctx context.Context) { in := make([]reflect.Value, n, n) - s.Fill(&in, reflect.ValueOf(ctx)) + s.Inject(&in, reflect.ValueOf(ctx)) if ctx.IsStopped() { return } diff --git a/mvc2/reflect.go b/mvc2/reflect.go index 1da5818787..09d4b47d58 100644 --- a/mvc2/reflect.go +++ b/mvc2/reflect.go @@ -110,6 +110,10 @@ func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { for i, n := 0, elemTyp.NumField(); i < n; i++ { f := elemTyp.Field(i) + if f.PkgPath != "" { + continue // skip unexported. + } + if indirectTyp(f.Type).Kind() == reflect.Struct && !structFieldIgnored(f) { fields = append(fields, lookupFields(f.Type, append(parentIndex, i))...) diff --git a/mvc2/session_controller.go b/mvc2/session_controller.go index e369e877f3..8b7e815af4 100644 --- a/mvc2/session_controller.go +++ b/mvc2/session_controller.go @@ -22,7 +22,7 @@ type SessionController struct { // It makes sure that its "Manager" field is filled // even if the caller didn't provide any sessions manager via the `app.Controller` function. func (s *SessionController) OnActivate(ca *ControllerActivator) { - if didntBindManually := ca.BindIfNotExists(defaultSessionManager); didntBindManually { + if didntBindManually := ca.Dependencies.BindIfNotExists(defaultSessionManager); didntBindManually { ca.Router.GetReporter().Add( `MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, therefore this controller is using the default sessions manager instead. diff --git a/mvc2/vendor/github.com/kataras/di/README.md b/mvc2/vendor/github.com/kataras/di/README.md new file mode 100644 index 0000000000..cc27898703 --- /dev/null +++ b/mvc2/vendor/github.com/kataras/di/README.md @@ -0,0 +1,7 @@ +# UNFINISHED + +# BSD-3 License - Gerasimos Maropoulos. + +# INSIDE vendor folder + +Because it's not pushed to the https://github.com/kataras, yet. \ No newline at end of file diff --git a/mvc2/vendor/github.com/kataras/di/_examples/basic/main.go b/mvc2/vendor/github.com/kataras/di/_examples/basic/main.go new file mode 100644 index 0000000000..be75bb0acb --- /dev/null +++ b/mvc2/vendor/github.com/kataras/di/_examples/basic/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + + "github.com/kataras/di" +) + +type User struct { + Username string +} + +type Service interface { + save(user User) error +} + +type testService struct { + Name string +} + +func (s *testService) save(user User) error { + fmt.Printf("Saving user '%s' from %s Service\n", user.Username, s.Name) + return nil +} + +func myHandler(service Service) { + service.save(User{ + Username: "func: test username 2", + }) +} + +type userManager struct { + Service Service + + other string +} + +func (m *userManager) updateUsername(username string) { + m.Service.save(User{ + Username: "struct: " + username, + }) +} + +func main() { + // build state, when performance is not critical. + d := di.New() + d.Bind(&testService{"test service"}) + + structInjector := d.Struct(&userManager{}) + funcInjector := d.Func(myHandler) + + // + // at "serve-time", when performance is critical, this DI Binder works very fast. + // + // --- for struct's fields ---- + myManager := new(userManager) + structInjector.Inject(myManager) + myManager.updateUsername("test username 1") + + // --- for function's input ---- + funcInjector.Call() +} diff --git a/mvc2/vendor/github.com/kataras/di/di.go b/mvc2/vendor/github.com/kataras/di/di.go new file mode 100644 index 0000000000..89752231eb --- /dev/null +++ b/mvc2/vendor/github.com/kataras/di/di.go @@ -0,0 +1,68 @@ +package di + +import "reflect" + +type ( + Hijacker func(reflect.Type) (*BindObject, bool) + TypeChecker func(reflect.Type) bool +) + +type D struct { + Values + + hijacker Hijacker + goodFunc TypeChecker +} + +func New() *D { + return &D{} +} + +func (d *D) Hijack(fn Hijacker) *D { + d.hijacker = fn + return d +} + +func (d *D) GoodFunc(fn TypeChecker) *D { + d.goodFunc = fn + return d +} + +func (d *D) Clone() *D { + clone := New() + clone.hijacker = d.hijacker + clone.goodFunc = d.goodFunc + + // copy the current dynamic bindings (func binders) + // and static struct bindings (services) to this new child. + if n := len(d.Values); n > 0 { + values := make(Values, n, n) + copy(values, d.Values) + clone.Values = values + } + + return clone +} + +func (d *D) Struct(s interface{}) *StructInjector { + if s == nil { + return nil + } + v := valueOf(s) + + return MakeStructInjector( + v, + d.hijacker, + d.goodFunc, + d.Values..., + ) +} + +func (d *D) Func(fn interface{}) *FuncInjector { + return MakeFuncInjector( + valueOf(fn), + d.hijacker, + d.goodFunc, + d.Values..., + ) +} diff --git a/mvc2/vendor/github.com/kataras/di/doc.go b/mvc2/vendor/github.com/kataras/di/doc.go new file mode 100644 index 0000000000..4c56dd3ac5 --- /dev/null +++ b/mvc2/vendor/github.com/kataras/di/doc.go @@ -0,0 +1 @@ +package di \ No newline at end of file diff --git a/mvc2/vendor/github.com/kataras/di/func.go b/mvc2/vendor/github.com/kataras/di/func.go new file mode 100644 index 0000000000..78ae2b4e01 --- /dev/null +++ b/mvc2/vendor/github.com/kataras/di/func.go @@ -0,0 +1,105 @@ +package di + +import ( + "reflect" +) + +type ( + targetFuncInput struct { + Object *BindObject + InputIndex int + } + + FuncInjector struct { + // the original function, is being used + // only the .Call, which is refering to the same function, always. + fn reflect.Value + + inputs []*targetFuncInput + Length int + Valid bool // is True when contains func inputs and it's a valid target func. + } +) + +func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *FuncInjector { + typ := indirectTyp(fn.Type()) + s := &FuncInjector{ + fn: fn, + } + + if !isFunc(typ) { + return s + } + + n := typ.NumIn() + + // function input can have many values of the same types, + // so keep track of them in order to not set a func input to a next bind value, + // i.e (string, string) with two different binder funcs because of the different param's name. + consumedValues := make(map[int]bool, n) + + for i := 0; i < n; i++ { + inTyp := typ.In(i) + + if hijack != nil { + if b, ok := hijack(inTyp); ok && b != nil { + s.inputs = append(s.inputs, &targetFuncInput{ + InputIndex: i, + Object: b, + }) + + continue + } + } + + for valIdx, val := range values { + if _, shouldSkip := consumedValues[valIdx]; shouldSkip { + continue + } + inTyp := typ.In(i) + + // the binded values to the func's inputs. + b, err := MakeBindObject(val, goodFunc) + + if err != nil { + return s // if error stop here. + } + + if b.IsAssignable(inTyp) { + // fmt.Printf("binded input index: %d for type: %s and value: %v with pointer: %v\n", + // i, b.Type.String(), val.String(), val.Pointer()) + s.inputs = append(s.inputs, &targetFuncInput{ + InputIndex: i, + Object: &b, + }) + + consumedValues[valIdx] = true + break + } + } + } + + s.Length = n + s.Valid = len(s.inputs) > 0 + return s +} + +func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) { + args := *in + for _, input := range s.inputs { + input.Object.Assign(ctx, func(v reflect.Value) { + // fmt.Printf("assign input index: %d for value: %v\n", + // input.InputIndex, v.String()) + args[input.InputIndex] = v + }) + + } + + *in = args +} + +func (s *FuncInjector) Call(ctx ...reflect.Value) []reflect.Value { + in := make([]reflect.Value, s.Length, s.Length) + s.Inject(&in, ctx...) + return s.fn.Call(in) +} diff --git a/mvc2/vendor/github.com/kataras/di/object.go b/mvc2/vendor/github.com/kataras/di/object.go new file mode 100644 index 0000000000..eefb95910d --- /dev/null +++ b/mvc2/vendor/github.com/kataras/di/object.go @@ -0,0 +1,102 @@ +package di + +import ( + "errors" + "reflect" +) + +type bindType uint32 + +const ( + Static bindType = iota // simple assignable value, a static value. + Dynamic // dynamic value, depends on some input arguments from the caller. +) + +type BindObject struct { + Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' . + Value reflect.Value + + BindType bindType + ReturnValue func([]reflect.Value) reflect.Value +} + +// ReturnValueBad can be customized to skip func binders that are not allowed, a custom logic. +var ReturnValueBad = func(reflect.Type) bool { + return false +} + +func MakeBindObject(v reflect.Value, goodFunc TypeChecker) (b BindObject, err error) { + if isFunc(v) { + b.BindType = Dynamic + b.ReturnValue, b.Type, err = MakeReturnValue(v, goodFunc) + } else { + b.BindType = Static + b.Type = v.Type() + b.Value = v + } + + return +} + +var errBad = errors.New("bad") + +// MakeReturnValue takes any function +// that accept custom values and returns something, +// it returns a binder function, which accepts a slice of reflect.Value +// and returns a single one reflect.Value for that. +// It's being used to resolve the input parameters on a "x" consumer faster. +// +// The "fn" can have the following form: +// `func(myService) MyViewModel`. +// +// The return type of the "fn" should be a value instance, not a pointer, for your own protection. +// The binder function should return only one value. +func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Value) reflect.Value, reflect.Type, error) { + typ := indirectTyp(fn.Type()) + + // invalid if not a func. + if typ.Kind() != reflect.Func { + return nil, typ, errBad + } + + // invalid if not returns one single value. + if typ.NumOut() != 1 { + return nil, typ, errBad + } + + if goodFunc != nil { + if !goodFunc(typ) { + return nil, typ, errBad + } + } + + outTyp := typ.Out(0) + zeroOutVal := reflect.New(outTyp).Elem() + + bf := func(ctxValue []reflect.Value) reflect.Value { + results := fn.Call(ctxValue) + if len(results) == 0 { + return zeroOutVal + } + + v := results[0] + if !v.IsValid() { + return zeroOutVal + } + return v + } + + return bf, outTyp, nil +} + +func (b *BindObject) IsAssignable(to reflect.Type) bool { + return equalTypes(b.Type, to) +} + +func (b *BindObject) Assign(ctx []reflect.Value, toSetter func(reflect.Value)) { + if b.BindType == Dynamic { + toSetter(b.ReturnValue(ctx)) + return + } + toSetter(b.Value) +} diff --git a/mvc2/vendor/github.com/kataras/di/reflect.go b/mvc2/vendor/github.com/kataras/di/reflect.go new file mode 100644 index 0000000000..f56437f2c3 --- /dev/null +++ b/mvc2/vendor/github.com/kataras/di/reflect.go @@ -0,0 +1,199 @@ +package di + +import ( + "reflect" + "strings" +) + +var emptyIn = []reflect.Value{} + +// IsZero returns true if a value is nil, remember boolean's false is zero. +// Remember; fields to be checked should be exported otherwise it returns false. +func IsZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Struct: + zero := true + for i := 0; i < v.NumField(); i++ { + zero = zero && IsZero(v.Field(i)) + } + + if typ := v.Type(); typ != nil && v.IsValid() { + f, ok := typ.MethodByName("IsZero") + // if not found + // if has input arguments (1 is for the value receiver, so > 1 for the actual input args) + // if output argument is not boolean + // then skip this IsZero user-defined function. + if !ok || f.Type.NumIn() > 1 || f.Type.NumOut() != 1 && f.Type.Out(0).Kind() != reflect.Bool { + return zero + } + + method := v.Method(f.Index) + // no needed check but: + if method.IsValid() && !method.IsNil() { + // it shouldn't panic here. + zero = method.Call(emptyIn)[0].Interface().(bool) + } + } + + return zero + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + zero := true + for i := 0; i < v.Len(); i++ { + zero = zero && IsZero(v.Index(i)) + } + return zero + } + // if not any special type then use the reflect's .Zero + // usually for fields, but remember if it's boolean and it's false + // then it's zero, even if set-ed. + + if !v.CanInterface() { + // if can't interface, i.e return value from unexported field or method then return false + return false + } + zero := reflect.Zero(v.Type()) + return v.Interface() == zero.Interface() +} + +func indirectVal(v reflect.Value) reflect.Value { + return reflect.Indirect(v) +} + +func valueOf(o interface{}) reflect.Value { + if v, ok := o.(reflect.Value); ok { + return v + } + + return reflect.ValueOf(o) +} + +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(kindable interface { + Kind() reflect.Kind +}) bool { + return kindable.Kind() == reflect.Func +} + +func equalTypes(got reflect.Type, expected reflect.Type) bool { + if got == expected { + return true + } + // if accepts an interface, check if the given "got" type does + // implement this "expected" user handler's input argument. + if expected.Kind() == reflect.Interface { + // fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String()) + return got.Implements(expected) + } + return false +} + +func getNameOf(typ reflect.Type) string { + elemTyp := indirectTyp(typ) + + typName := elemTyp.Name() + pkgPath := elemTyp.PkgPath() + fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName + + return fullname +} + +func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { + n := funcTyp.NumIn() + funcIn := make([]reflect.Type, n, n) + for i := 0; i < n; i++ { + funcIn[i] = funcTyp.In(i) + } + return funcIn +} + +// for controller's fields only. +func structFieldIgnored(f reflect.StructField) bool { + if !f.Anonymous { + return true // if not anonymous(embedded), ignore it. + } + + s := f.Tag.Get("ignore") + return s == "true" // if has an ignore tag then ignore it. +} + +type field struct { + Type reflect.Type + Index []int // the index of the field, slice if it's part of a embedded struct + Name string // the actual name + + // this could be empty, but in our cases it's not, + // it's filled with the bind object (as service which means as static value) + // and it's filled from the lookupFields' caller. + AnyValue reflect.Value +} + +func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { + if elemTyp.Kind() != reflect.Struct { + return + } + + for i, n := 0, elemTyp.NumField(); i < n; i++ { + f := elemTyp.Field(i) + + if indirectTyp(f.Type).Kind() == reflect.Struct && + !structFieldIgnored(f) { + fields = append(fields, lookupFields(f.Type, append(parentIndex, i))...) + continue + } + + // skip unexported fields here, + // after the check for embedded structs, these can be binded if their + // fields are exported. + if f.PkgPath != "" { + continue + } + + index := []int{i} + if len(parentIndex) > 0 { + index = append(parentIndex, i) + } + + field := field{ + Type: f.Type, + Name: f.Name, + Index: index, + } + + fields = append(fields, field) + } + + return +} + +func lookupNonZeroFieldsValues(v reflect.Value) (bindValues []reflect.Value) { + elem := indirectVal(v) + fields := lookupFields(indirectTyp(v.Type()), nil) + for _, f := range fields { + + if fieldVal := elem.FieldByIndex(f.Index); f.Type.Kind() == reflect.Ptr && !IsZero(fieldVal) { + bindValues = append(bindValues, fieldVal) + } + } + + return +} diff --git a/mvc2/vendor/github.com/kataras/di/struct.go b/mvc2/vendor/github.com/kataras/di/struct.go new file mode 100644 index 0000000000..42bb655512 --- /dev/null +++ b/mvc2/vendor/github.com/kataras/di/struct.go @@ -0,0 +1,84 @@ +package di + +import "reflect" + +type ( + targetStructField struct { + Object *BindObject + FieldIndex []int + } + + StructInjector struct { + elemType reflect.Type + // + fields []*targetStructField + Valid bool // is True when contains fields and it's a valid target struct. + } +) + +func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *StructInjector { + s := &StructInjector{ + elemType: indirectTyp(v.Type()), + } + + fields := lookupFields(s.elemType, nil) + for _, f := range fields { + + if hijack != nil { + if b, ok := hijack(f.Type); ok && b != nil { + s.fields = append(s.fields, &targetStructField{ + FieldIndex: f.Index, + Object: b, + }) + + continue + } + } + + for _, val := range values { + // the binded values to the struct's fields. + b, err := MakeBindObject(val, goodFunc) + + if err != nil { + return s // if error stop here. + } + + if b.IsAssignable(f.Type) { + // fmt.Printf("bind the object to the field: %s at index: %#v and type: %s\n", f.Name, f.Index, f.Type.String()) + s.fields = append(s.fields, &targetStructField{ + FieldIndex: f.Index, + Object: &b, + }) + break + } + + } + } + + s.Valid = len(s.fields) > 0 + return s +} + +func (s *StructInjector) Inject(dest interface{}, ctx ...reflect.Value) { + if dest == nil { + return + } + + v := indirectVal(valueOf(dest)) + s.InjectElem(v, ctx...) +} + +func (s *StructInjector) InjectElem(destElem reflect.Value, ctx ...reflect.Value) { + for _, f := range s.fields { + f.Object.Assign(ctx, func(v reflect.Value) { + // fmt.Printf("%s for %s at index: %d\n", destElem.Type().String(), f.Object.Type.String(), f.FieldIndex) + destElem.FieldByIndex(f.FieldIndex).Set(v) + }) + } +} + +func (s *StructInjector) New(ctx ...reflect.Value) reflect.Value { + dest := reflect.New(s.elemType) + s.InjectElem(dest, ctx...) + return dest +} diff --git a/mvc2/bind_values.go b/mvc2/vendor/github.com/kataras/di/values.go similarity index 67% rename from mvc2/bind_values.go rename to mvc2/vendor/github.com/kataras/di/values.go index 2a7bd41a2c..a412e9b496 100644 --- a/mvc2/bind_values.go +++ b/mvc2/vendor/github.com/kataras/di/values.go @@ -1,26 +1,32 @@ -package mvc2 +package di import ( "reflect" ) -/// TODO: -// create another package because these bindings things are useful -// for other libraries I'm working on, so something like github.com/kataras/di -// will be great, combine these with the bind.go and controller's inside handler -// but generic things. +type Values []reflect.Value -type ValueStore []reflect.Value +func NewValues() Values { + return Values{} +} // Bind binds values to this controller, if you want to share // binding values between controllers use the Engine's `Bind` function instead. -func (bv *ValueStore) Bind(values ...interface{}) { +func (bv *Values) Bind(values ...interface{}) { for _, val := range values { bv.bind(reflect.ValueOf(val)) } } -func (bv *ValueStore) bind(v reflect.Value) { +// Add same as `Bind` but accepts reflect.Value +// instead. +func (bv *Values) Add(values ...reflect.Value) { + for _, v := range values { + bv.bind(v) + } +} + +func (bv *Values) bind(v reflect.Value) { if !goodVal(v) { return } @@ -34,11 +40,11 @@ func (bv *ValueStore) bind(v reflect.Value) { // The "n" indicates the number of elements to remove, if <=0 then it's 1, // this is useful because you may have bind more than one value to two or more fields // with the same type. -func (bv *ValueStore) Unbind(value interface{}, n int) bool { +func (bv *Values) Unbind(value interface{}, n int) bool { return bv.unbind(reflect.TypeOf(value), n) } -func (bv *ValueStore) unbind(typ reflect.Type, n int) (ok bool) { +func (bv *Values) unbind(typ reflect.Type, n int) (ok bool) { input := *bv for i, in := range input { if equalTypes(in.Type(), typ) { @@ -58,11 +64,11 @@ func (bv *ValueStore) unbind(typ reflect.Type, n int) (ok bool) { // BindExists returns true if a binder responsible to // bind and return a type of "typ" is already registered to this controller. -func (bv *ValueStore) BindExists(value interface{}) bool { +func (bv *Values) BindExists(value interface{}) bool { return bv.bindTypeExists(reflect.TypeOf(value)) } -func (bv *ValueStore) bindTypeExists(typ reflect.Type) bool { +func (bv *Values) bindTypeExists(typ reflect.Type) bool { input := *bv for _, in := range input { if equalTypes(in.Type(), typ) { @@ -77,11 +83,11 @@ func (bv *ValueStore) bindTypeExists(typ reflect.Type) bool { // // Returns false if binded already or the value is not the proper one for binding, // otherwise true. -func (bv *ValueStore) BindIfNotExists(value interface{}) bool { +func (bv *Values) BindIfNotExists(value interface{}) bool { return bv.bindIfNotExists(reflect.ValueOf(value)) } -func (bv *ValueStore) bindIfNotExists(v reflect.Value) bool { +func (bv *Values) bindIfNotExists(v reflect.Value) bool { var ( typ = v.Type() // no element, raw things here. ) From fcb5aec6310f3ff462835090717c60edd5517dc4 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Fri, 15 Dec 2017 20:28:06 +0200 Subject: [PATCH 20/79] remove the old 'mvc' folder - examples are not changed yet - add the 'di' package inside the mvc2 package - which will be renamed to 'mvc' on the next commit - new mvc.Application and some dublications removed - The new version will be version 9 because it will contain breaking changes (not to the end-developer's controllers but to the API they register them) - get ready for 'Christmas Edition' for believers --- .../src/web/controllers/todo_controller.go | 31 +- core/router/api_builder.go | 80 --- core/router/party.go | 58 -- go19.go | 129 ----- mvc/activator/activate_listener.go | 30 - mvc/activator/activator.go | 346 ------------ mvc/activator/binder.go | 108 ---- mvc/activator/field/field.go | 220 -------- mvc/activator/methodfunc/func_caller.go | 19 - mvc/activator/methodfunc/func_info.go | 102 ---- mvc/activator/methodfunc/func_lexer.go | 89 --- mvc/activator/methodfunc/func_parser.go | 213 ------- .../methodfunc/func_result_dispatcher.go | 230 -------- mvc/activator/methodfunc/methodfunc.go | 68 --- mvc/activator/model/model.go | 73 --- mvc/activator/persistence/persistence.go | 60 -- mvc/controller.go | 369 ------------ mvc/controller_test.go | 531 ------------------ mvc/go19.go | 26 - mvc/method_result.go | 58 -- mvc/method_result_response.go | 69 --- mvc/method_result_test.go | 271 --------- mvc/method_result_view.go | 104 ---- mvc/session_controller.go | 47 -- mvc/strutil.go | 38 -- mvc/strutil_test.go | 31 - mvc2/bind.go | 2 +- mvc2/controller.go | 68 ++- mvc2/controller_handle_test.go | 17 +- mvc2/controller_test.go | 37 +- mvc2/di/di.go | 92 +++ .../github.com/kataras => }/di/func.go | 9 +- .../github.com/kataras => }/di/object.go | 15 +- .../github.com/kataras => }/di/reflect.go | 43 +- .../github.com/kataras => }/di/struct.go | 4 +- .../github.com/kataras => }/di/values.go | 51 +- mvc2/engine.go | 43 +- mvc2/engine_handler_test.go | 3 +- mvc2/func_result.go | 4 +- mvc2/func_result_test.go | 8 +- mvc2/handler.go | 4 +- mvc2/ideas/1/main.go | 83 +++ mvc2/mvc.go | 90 +++ mvc2/path_param_binder_test.go | 6 +- mvc2/reflect.go | 123 ---- mvc2/{session_binder.go => session.go} | 0 mvc2/session_controller.go | 6 +- mvc2/vendor/github.com/kataras/di/README.md | 7 - .../kataras/di/_examples/basic/main.go | 62 -- mvc2/vendor/github.com/kataras/di/di.go | 68 --- mvc2/vendor/github.com/kataras/di/doc.go | 1 - sessions/session.go | 20 + 52 files changed, 463 insertions(+), 3803 deletions(-) delete mode 100644 mvc/activator/activate_listener.go delete mode 100644 mvc/activator/activator.go delete mode 100644 mvc/activator/binder.go delete mode 100644 mvc/activator/field/field.go delete mode 100644 mvc/activator/methodfunc/func_caller.go delete mode 100644 mvc/activator/methodfunc/func_info.go delete mode 100644 mvc/activator/methodfunc/func_lexer.go delete mode 100644 mvc/activator/methodfunc/func_parser.go delete mode 100644 mvc/activator/methodfunc/func_result_dispatcher.go delete mode 100644 mvc/activator/methodfunc/methodfunc.go delete mode 100644 mvc/activator/model/model.go delete mode 100644 mvc/activator/persistence/persistence.go delete mode 100644 mvc/controller.go delete mode 100644 mvc/controller_test.go delete mode 100644 mvc/go19.go delete mode 100644 mvc/method_result.go delete mode 100644 mvc/method_result_response.go delete mode 100644 mvc/method_result_test.go delete mode 100644 mvc/method_result_view.go delete mode 100644 mvc/session_controller.go delete mode 100644 mvc/strutil.go delete mode 100644 mvc/strutil_test.go create mode 100644 mvc2/di/di.go rename mvc2/{vendor/github.com/kataras => }/di/func.go (90%) rename mvc2/{vendor/github.com/kataras => }/di/object.go (87%) rename mvc2/{vendor/github.com/kataras => }/di/reflect.go (83%) rename mvc2/{vendor/github.com/kataras => }/di/struct.go (96%) rename mvc2/{vendor/github.com/kataras => }/di/values.go (52%) create mode 100644 mvc2/ideas/1/main.go create mode 100644 mvc2/mvc.go rename mvc2/{session_binder.go => session.go} (100%) delete mode 100644 mvc2/vendor/github.com/kataras/di/README.md delete mode 100644 mvc2/vendor/github.com/kataras/di/_examples/basic/main.go delete mode 100644 mvc2/vendor/github.com/kataras/di/di.go delete mode 100644 mvc2/vendor/github.com/kataras/di/doc.go diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go index 69923ea224..ce2b8869d8 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -10,17 +10,19 @@ import ( // TodoController is our TODO app's web controller. type TodoController struct { - service todo.Service + Service todo.Service - session *sessions.Session + Session *sessions.Session } -// OnActivate called once before the server ran, can bind custom -// things to the controller. -func (c *TodoController) OnActivate(ca *mvc.ControllerActivator) { +// BeforeActivate called once before the server ran, and before +// the routes and dependency binder builded. +// You can bind custom things to the controller, add new methods, add middleware, +// add dependencies to the struct or the method(s) and more. +func (c *TodoController) BeforeActivate(ca *mvc.ControllerActivator) { // this could be binded to a controller's function input argument // if any, or struct field if any: - ca.Bind(func(ctx iris.Context) todo.Item { + ca.Dependencies.Add(func(ctx iris.Context) todo.Item { // ctx.ReadForm(&item) var ( owner = ctx.PostValue("owner") @@ -35,25 +37,30 @@ func (c *TodoController) OnActivate(ca *mvc.ControllerActivator) { } }) + // ca.Router.Use(...).Done(...).Layout(...) + // TODO:(?) + // m := ca.Method("PutCompleteBy") + // m.Route.Use(...).Done(...) <- we don't have the route here but I can find something to solve this. + // m.Dependencies.Add(...) } // Get handles the GET: /todo route. func (c *TodoController) Get() []todo.Item { - return c.service.GetByOwner(c.session.ID()) + return c.Service.GetByOwner(c.Session.ID()) } // PutCompleteBy handles the PUT: /todo/complete/{id:long} route. func (c *TodoController) PutCompleteBy(id int64) int { - item, found := c.service.GetByID(id) + item, found := c.Service.GetByID(id) if !found { return iris.StatusNotFound } - if item.OwnerID != c.session.ID() { + if item.OwnerID != c.Session.ID() { return iris.StatusForbidden } - if !c.service.Complete(item) { + if !c.Service.Complete(item) { return iris.StatusBadRequest } @@ -62,11 +69,11 @@ func (c *TodoController) PutCompleteBy(id int64) int { // Post handles the POST: /todo route. func (c *TodoController) Post(newItem todo.Item) int { - if newItem.OwnerID != c.session.ID() { + if newItem.OwnerID != c.Session.ID() { return iris.StatusForbidden } - if err := c.service.Save(newItem); err != nil { + if err := c.Service.Save(newItem); err != nil { return iris.StatusBadRequest } return iris.StatusOK diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 5ce9150bd2..7105e9fccc 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -10,7 +10,6 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/router/macro" - "github.com/kataras/iris/mvc/activator" ) const ( @@ -478,85 +477,6 @@ func (api *APIBuilder) Any(relativePath string, handlers ...context.Handler) (ro return } -// Controller registers a `Controller` instance and returns the registered Routes. -// The "controller" receiver should embed a field of `Controller` in order -// to be compatible Iris `Controller`. -// -// It's just an alternative way of building an API for a specific -// path, the controller can register all type of http methods. -// -// Keep note that controllers are bit slow -// because of the reflection use however it's as fast as possible because -// it does preparation before the serve-time handler but still -// remains slower than the low-level handlers -// such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`. -// -// -// All fields that are tagged with iris:"persistence"` or binded -// are being persistence and kept the same between the different requests. -// -// An Example Controller can be: -// -// type IndexController struct { -// Controller -// } -// -// func (c *IndexController) Get() { -// c.Tmpl = "index.html" -// c.Data["title"] = "Index page" -// c.Data["message"] = "Hello world!" -// } -// -// Usage: app.Controller("/", new(IndexController)) -// -// -// Another example with bind: -// -// type UserController struct { -// Controller -// -// DB *DB -// CreatedAt time.Time -// -// } -// -// // Get serves using the User controller when HTTP Method is "GET". -// func (c *UserController) Get() { -// c.Tmpl = "user/index.html" -// c.Data["title"] = "User Page" -// c.Data["username"] = "kataras " + c.Params.Get("userid") -// c.Data["connstring"] = c.DB.Connstring -// c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds() -// } -// -// Usage: app.Controller("/user/{id:int}", new(UserController), db, time.Now()) -// Note: Binded values of context.Handler type are being recognised as middlewares by the router. -// -// Read more at `/mvc#Controller`. -func (api *APIBuilder) Controller(relativePath string, controller activator.BaseController, - bindValues ...interface{}) (routes []*Route) { - - registerFunc := func(method string, ifRelPath string, handlers ...context.Handler) { - relPath := relativePath + ifRelPath - r := api.HandleMany(method, relPath, handlers...) - routes = append(routes, r...) - } - - // bind any values to the controller's relative fields - // and set them on each new request controller, - // binder is an alternative method - // of the persistence data control which requires the - // user already set the values manually to controller's fields - // and tag them with `iris:"persistence"`. - // - // don't worry it will never be handled if empty values. - if err := activator.Register(controller, bindValues, registerFunc); err != nil { - api.reporter.Add("%v for path: '%s'", err, relativePath) - } - - return -} - // StaticCacheDuration expiration duration for INACTIVE file handlers, it's the only one global configuration // which can be changed. var StaticCacheDuration = 20 * time.Second diff --git a/core/router/party.go b/core/router/party.go index 84b17fb0e5..f1a1a9d04d 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -4,7 +4,6 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/router/macro" - "github.com/kataras/iris/mvc/activator" ) // Party is here to separate the concept of @@ -130,63 +129,6 @@ type Party interface { // (Get,Post,Put,Head,Patch,Options,Connect,Delete). Any(registeredPath string, handlers ...context.Handler) []*Route - // Controller registers a `Controller` instance and returns the registered Routes. - // The "controller" receiver should embed a field of `Controller` in order - // to be compatible Iris `Controller`. - // - // It's just an alternative way of building an API for a specific - // path, the controller can register all type of http methods. - // - // Keep note that controllers are bit slow - // because of the reflection use however it's as fast as possible because - // it does preparation before the serve-time handler but still - // remains slower than the low-level handlers - // such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`. - // - // - // All fields that are tagged with iris:"persistence"` or binded - // are being persistence and kept the same between the different requests. - // - // An Example Controller can be: - // - // type IndexController struct { - // Controller - // } - // - // func (c *IndexController) Get() { - // c.Tmpl = "index.html" - // c.Data["title"] = "Index page" - // c.Data["message"] = "Hello world!" - // } - // - // Usage: app.Controller("/", new(IndexController)) - // - // - // Another example with bind: - // - // type UserController struct { - // Controller - // - // DB *DB - // CreatedAt time.Time - // - // } - // - // // Get serves using the User controller when HTTP Method is "GET". - // func (c *UserController) Get() { - // c.Tmpl = "user/index.html" - // c.Data["title"] = "User Page" - // c.Data["username"] = "kataras " + c.Params.Get("userid") - // c.Data["connstring"] = c.DB.Connstring - // c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds() - // } - // - // Usage: app.Controller("/user/{id:int}", new(UserController), db, time.Now()) - // Note: Binded values of context.Handler type are being recognised as middlewares by the router. - // - // Read more at `/mvc#Controller`. - Controller(relativePath string, controller activator.BaseController, bindValues ...interface{}) []*Route - // StaticHandler returns a new Handler which is ready // to serve all kind of static files. // diff --git a/go19.go b/go19.go index 4df8568135..8a16665ea8 100644 --- a/go19.go +++ b/go19.go @@ -6,7 +6,6 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/host" "github.com/kataras/iris/core/router" - "github.com/kataras/iris/mvc" ) type ( @@ -48,132 +47,4 @@ type ( // // A shortcut for the `core/router#Party`, useful when `PartyFunc` is being used. Party = router.Party - - // Controller is the base controller for the high level controllers instances. - // - // This base controller is used as an alternative way of building - // APIs, the controller can register all type of http methods. - // - // Keep note that controllers are bit slow - // because of the reflection use however it's as fast as possible because - // it does preparation before the serve-time handler but still - // remains slower than the low-level handlers - // such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`. - // - // - // All fields that are tagged with iris:"persistence"` - // are being persistence and kept between the different requests, - // meaning that these data will not be reset-ed on each new request, - // they will be the same for all requests. - // - // An Example Controller can be: - // - // type IndexController struct { - // iris.Controller - // } - // - // func (c *IndexController) Get() { - // c.Tmpl = "index.html" - // c.Data["title"] = "Index page" - // c.Data["message"] = "Hello world!" - // } - // - // Usage: app.Controller("/", new(IndexController)) - // - // - // Another example with persistence data: - // - // type UserController struct { - // iris.Controller - // - // CreatedAt time.Time `iris:"persistence"` - // Title string `iris:"persistence"` - // DB *DB `iris:"persistence"` - // } - // - // // Get serves using the User controller when HTTP Method is "GET". - // func (c *UserController) Get() { - // c.Tmpl = "user/index.html" - // c.Data["title"] = c.Title - // c.Data["username"] = "kataras " + c.Params.Get("userid") - // c.Data["connstring"] = c.DB.Connstring - // c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds() - // } - // - // Usage: app.Controller("/user/{id:int}", &UserController{ - // CreatedAt: time.Now(), - // Title: "User page", - // DB: yourDB, - // }) - // - // Look `core/router#APIBuilder#Controller` method too. - // - // A shortcut for the `mvc#Controller`, - // useful when `app.Controller` method is being used. - // - // A Controller can be declared by importing - // the "github.com/kataras/iris/mvc" - // package for machines that have not installed go1.9 yet. - Controller = mvc.Controller - // SessionController is a simple `Controller` implementation - // which requires a binded session manager in order to give - // direct access to the current client's session via its `Session` field. - SessionController = mvc.SessionController - // C is the lightweight BaseController type as an alternative of the `Controller` struct type. - // It contains only the Name of the controller and the Context, it's the best option - // to balance the performance cost reflection uses - // if your controller uses the new func output values dispatcher feature; - // func(c *ExampleController) Get() string | - // (string, string) | - // (string, int) | - // int | - // (int, string | - // (string, error) | - // error | - // (int, error) | - // (customStruct, error) | - // customStruct | - // (customStruct, int) | - // (customStruct, string) | - // Result or (Result, error) - // where Get is an HTTP Method func. - // - // Look `core/router#APIBuilder#Controller` method too. - // - // A shortcut for the `mvc#C`, - // useful when `app.Controller` method is being used. - // - // A C controller can be declared by importing - // the "github.com/kataras/iris/mvc" as well. - C = mvc.C - // Response completes the `mvc/activator/methodfunc.Result` interface. - // It's being used as an alternative return value which - // wraps the status code, the content type, a content as bytes or as string - // and an error, it's smart enough to complete the request and send the correct response to the client. - // - // A shortcut for the `mvc#Response`, - // useful when return values from method functions, i.e - // GetHelloworld() iris.Response { iris.Response{ Text:"Hello World!", Code: 200 }} - Response = mvc.Response - // View completes the `mvc/activator/methodfunc.Result` interface. - // It's being used as an alternative return value which - // wraps the template file name, layout, (any) view data, status code and error. - // It's smart enough to complete the request and send the correct response to the client. - // - // A shortcut for the `mvc#View`, - // useful when return values from method functions, i.e - // GetUser() iris.View { iris.View{ Name:"user.html", Data: currentUser } } - View = mvc.View - // Result is a response dispatcher. - // All types that complete this interface - // can be returned as values from the method functions. - // A shortcut for the `mvc#Result` which is a shortcut for `mvc/activator/methodfunc#Result`, - // useful when return values from method functions, i.e - // GetUser() iris.Result { iris.Response{} or a custom iris.Result } - // Can be also used for the TryResult function. - Result = mvc.Result ) - -// Try is a shortcut for the function `mvc.Try` result. -// See more at `mvc#Try` documentation. -var Try = mvc.Try diff --git a/mvc/activator/activate_listener.go b/mvc/activator/activate_listener.go deleted file mode 100644 index 3e2a6120f0..0000000000 --- a/mvc/activator/activate_listener.go +++ /dev/null @@ -1,30 +0,0 @@ -package activator - -// CallOnActivate simply calls the "controller"'s `OnActivate(*TController)` function, -// if any. -// -// Look `activator.go#Register` and `ActivateListener` for more. -func CallOnActivate(controller interface{}, tController *TController) { - - if ac, ok := controller.(ActivateListener); ok { - ac.OnActivate(tController) - } -} - -// ActivateListener is an interface which should be declared -// on a Controller which needs to register or change the bind values -// that the caller-"user" has been passed to; via the `app.Controller`. -// If that interface is completed by a controller -// then the `OnActivate` function will be called ONCE, NOT in every request -// but ONCE at the application's lifecycle. -type ActivateListener interface { - // OnActivate accepts a pointer to the `TController`. - // - // The `Controller` can make use of the `OnActivate` function - // to register custom routes - // or modify the provided values that will be binded to the - // controller later on. - // - // Look `TController` for more. - OnActivate(*TController) -} diff --git a/mvc/activator/activator.go b/mvc/activator/activator.go deleted file mode 100644 index 4be839a4f5..0000000000 --- a/mvc/activator/activator.go +++ /dev/null @@ -1,346 +0,0 @@ -package activator - -import ( - "reflect" - "strings" - - "github.com/kataras/iris/core/router/macro" - "github.com/kataras/iris/mvc/activator/methodfunc" - "github.com/kataras/iris/mvc/activator/model" - "github.com/kataras/iris/mvc/activator/persistence" - - "github.com/kataras/golog" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/errors" -) - -type ( - // TController is the type of the controller, - // it contains all the necessary information to load - // and serve the controller to the outside world, - // think it as a "supervisor" of your Controller which - // cares about you. - TController struct { - // The name of the front controller struct. - Name string - // FullName it's the last package path segment + "." + the Name. - // i.e: if login-example/user/controller.go, the FullName is "user.Controller". - FullName string - // the type of the user/dev's "c" controller (interface{}). - Type reflect.Type - // it's the first passed value of the controller instance, - // we need this to collect and save the persistence fields' values. - Value reflect.Value - - valuePtr reflect.Value - // // Methods and handlers, available after the Activate, can be seted `OnActivate` event as well. - // Methods []methodfunc.MethodFunc - - Router RegisterFunc - - binder *binder // executed even before the BeginRequest if not nil. - modelController *model.Controller - persistenceController *persistence.Controller - } -) - -// the parent package should complete this "interface" -// it's not exported, so their functions -// but reflect doesn't care about it, so we are ok -// to compare the type of the base controller field -// with this "ctrl", see `buildTypeInfo` and `buildMethodHandler`. - -var ( - // ErrMissingControllerInstance is a static error which fired from `Controller` when - // the passed "c" instnace is not a valid type of `Controller` or `C`. - ErrMissingControllerInstance = errors.New("controller should have a field of mvc.Controller or mvc.C type") - // ErrInvalidControllerType fired when the "Controller" field is not - // the correct type. - ErrInvalidControllerType = errors.New("controller instance is not a valid implementation") -) - -// BaseController is the controller interface, -// which the main request `Controller` will implement automatically. -// End-User doesn't need to have any knowledge of this if she/he doesn't want to implement -// a new Controller type. -// Controller looks the whole flow as one handler, so `ctx.Next` -// inside `BeginRequest` is not be respected. -// Alternative way to check if a middleware was procceed successfully -// and called its `ctx.Next` is the `ctx.Proceed(handler) bool`. -// You have to navigate to the `context/context#Proceed` function's documentation. -type BaseController interface { - SetName(name string) - BeginRequest(ctx context.Context) - EndRequest(ctx context.Context) -} - -// ActivateController returns a new controller type info description. -func newController(base BaseController, router RegisterFunc) (*TController, error) { - // get and save the type. - typ := reflect.TypeOf(base) - if typ.Kind() != reflect.Ptr { - typ = reflect.PtrTo(typ) - } - - valPointer := reflect.ValueOf(base) // or value raw - - // first instance value, needed to validate - // the actual type of the controller field - // and to collect and save the instance's persistence fields' - // values later on. - val := reflect.Indirect(valPointer) - - ctrlName := val.Type().Name() - pkgPath := val.Type().PkgPath() - fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName - - t := &TController{ - Name: ctrlName, - FullName: fullName, - Type: typ, - Value: val, - valuePtr: valPointer, - Router: router, - binder: &binder{elemType: typ.Elem()}, - modelController: model.Load(typ), - persistenceController: persistence.Load(typ, val), - } - - return t, nil -} - -// BindValueTypeExists returns true if at least one type of "bindValue" -// is already binded to this `TController`. -func (t *TController) BindValueTypeExists(bindValue interface{}) bool { - valueTyp := reflect.TypeOf(bindValue) - for _, bindedValue := range t.binder.values { - // type already exists, remember: binding here is per-type. - if typ := reflect.TypeOf(bindedValue); typ == valueTyp || - (valueTyp.Kind() == reflect.Interface && typ.Implements(valueTyp)) { - return true - } - } - - return false -} - -// BindValue binds a value to a controller's field when request is served. -func (t *TController) BindValue(bindValues ...interface{}) { - for _, bindValue := range bindValues { - t.binder.bind(bindValue) - } -} - -// HandlerOf builds the handler for a type based on the specific method func. -func (t *TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler { - var ( - // shared, per-controller - elem = t.Type.Elem() - ctrlName = t.Name - hasBinder = !t.binder.isEmpty() - - hasPersistenceData = t.persistenceController != nil - hasModels = t.modelController != nil - // per-handler - handleRequest = methodFunc.MethodCall - ) - - return func(ctx context.Context) { - // create a new controller instance of that type(>ptr). - c := reflect.New(elem) - if hasBinder { - t.binder.handle(c) - } - - b := c.Interface().(BaseController) - b.SetName(ctrlName) - - // if has persistence data then set them - // before the end-developer's handler in order to be available there. - if hasPersistenceData { - t.persistenceController.Handle(c) - } - - // if previous (binded) handlers stopped the execution - // we should know that. - if ctx.IsStopped() { - return - } - - // init the request. - b.BeginRequest(ctx) - if ctx.IsStopped() { // if begin request stopped the execution - return - } - - // the most important, execute the specific function - // from the controller that is responsible to handle - // this request, by method and path. - handleRequest(ctx, c.Method(methodFunc.Index)) - // if had models, set them after the end-developer's handler. - if hasModels { - t.modelController.Handle(ctx, c) - } - - // end the request, don't check for stopped because this does the actual writing - // if no response written already. - b.EndRequest(ctx) - } -} - -func (t *TController) registerMethodFunc(m methodfunc.MethodFunc) { - var middleware context.Handlers - - if !t.binder.isEmpty() { - if m := t.binder.middleware; len(m) > 0 { - middleware = m - } - } - - h := t.HandlerOf(m) - if h == nil { - golog.Warnf("MVC %s: nil method handler found for %s", t.FullName, m.Name) - return - } - - registeredHandlers := append(middleware, h) - t.Router(m.HTTPMethod, m.RelPath, registeredHandlers...) - - golog.Debugf("MVC %s: %s %s maps to function[%d] '%s'", t.FullName, - m.HTTPMethod, - m.RelPath, - m.Index, - m.Name) -} - -func (t *TController) resolveAndRegisterMethods() { - // the actual method functions - // i.e for "GET" it's the `Get()`. - methods, err := methodfunc.Resolve(t.Type) - if err != nil { - golog.Errorf("MVC %s: %s", t.FullName, err.Error()) - return - } - // range over the type info's method funcs, - // build a new handler for each of these - // methods and register them to their - // http methods using the registerFunc, which is - // responsible to convert these into routes - // and add them to router via the APIBuilder. - for _, m := range methods { - t.registerMethodFunc(m) - } -} - -// Handle registers a method func but with a custom http method and relative route's path, -// it respects the rest of the controller's rules and guidelines. -func (t *TController) Handle(httpMethod, path, handlerFuncName string) bool { - cTyp := t.Type // with the pointer. - m, exists := cTyp.MethodByName(handlerFuncName) - if !exists { - golog.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", - handlerFuncName, t.FullName) - return false - } - - info := methodfunc.FuncInfo{ - Name: m.Name, - Trailing: m.Name, - Type: m.Type, - Index: m.Index, - HTTPMethod: httpMethod, - } - - tmpl, err := macro.Parse(path, macro.NewMap()) - if err != nil { - golog.Errorf("MVC: fail to parse the path for '%s.%s': %v", t.FullName, handlerFuncName, err) - return false - } - - paramKeys := make([]string, len(tmpl.Params), len(tmpl.Params)) - for i, param := range tmpl.Params { - paramKeys[i] = param.Name - } - - methodFunc, err := methodfunc.ResolveMethodFunc(info, paramKeys...) - if err != nil { - golog.Errorf("MVC: function '%s' inside the '%s' controller: %v", handlerFuncName, t.FullName, err) - return false - } - - methodFunc.RelPath = path - - t.registerMethodFunc(methodFunc) - return true -} - -// func (t *TController) getMethodFuncByName(funcName string) (methodfunc.MethodFunc, bool) { -// cVal := t.Value -// cTyp := t.Type // with the pointer. -// m, exists := cTyp.MethodByName(funcName) -// if !exists { -// golog.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", -// funcName, cTyp.String()) -// return methodfunc.MethodFunc{}, false -// } - -// fn := cVal.MethodByName(funcName) -// if !fn.IsValid() { -// golog.Errorf("MVC: function '%s' inside the '%s' controller has not a valid value", -// funcName, cTyp.String()) -// return methodfunc.MethodFunc{}, false -// } - -// info, ok := methodfunc.FetchFuncInfo(m) -// if !ok { -// golog.Errorf("MVC: could not resolve the func info from '%s'", funcName) -// return methodfunc.MethodFunc{}, false -// } - -// methodFunc, err := methodfunc.ResolveMethodFunc(info) -// if err != nil { -// golog.Errorf("MVC: %v", err) -// return methodfunc.MethodFunc{}, false -// } - -// return methodFunc, true -// } - -// // RegisterName registers a function by its name -// func (t *TController) RegisterName(funcName string) bool { -// methodFunc, ok := t.getMethodFuncByName(funcName) -// if !ok { -// return false -// } -// t.registerMethodFunc(methodFunc) -// return true -// } - -// RegisterFunc used by the caller to register the result routes. -type RegisterFunc func(httpMethod string, relPath string, handler ...context.Handler) - -// Register receives a "controller", -// a pointer of an instance which embeds the `Controller`, -// the value of "baseControllerFieldName" should be `Controller`. -func Register(controller BaseController, bindValues []interface{}, - registerFunc RegisterFunc) error { - - t, err := newController(controller, registerFunc) - if err != nil { - return err - } - - t.BindValue(bindValues...) - - CallOnActivate(controller, t) - - for _, bf := range t.binder.fields { - golog.Debugf("MVC %s: binder loaded for '%s' with value:\n%#v", - t.FullName, bf.GetFullName(), bf.GetValue()) - } - - t.resolveAndRegisterMethods() - - return nil -} diff --git a/mvc/activator/binder.go b/mvc/activator/binder.go deleted file mode 100644 index 81d64dd395..0000000000 --- a/mvc/activator/binder.go +++ /dev/null @@ -1,108 +0,0 @@ -package activator - -import ( - "reflect" - - "github.com/kataras/iris/mvc/activator/field" - - "github.com/kataras/iris/context" -) - -// binder accepts a value of something -// and tries to find its equalivent type -// inside the controller and sets that to it, -// after that each new instance of the controller will have -// this value on the specific field, like persistence data control does. - -type binder struct { - elemType reflect.Type - // values and fields are matched on the `match`. - values []interface{} - fields []field.Field - - // saves any middleware that may need to be passed to the router, - // statically, to gain performance. - middleware context.Handlers -} - -func (b *binder) bind(value interface{}) { - if value == nil { - return - } - - b.values = append(b.values, value) // keep values. - - b.match(value) -} - -func (b *binder) isEmpty() bool { - // if nothing valid found return nil, so the caller - // can omit the binder. - if len(b.fields) == 0 && len(b.middleware) == 0 { - return true - } - - return false -} - -func (b *binder) storeValueIfMiddleware(value reflect.Value) bool { - if value.CanInterface() { - if m, ok := value.Interface().(context.Handler); ok { - b.middleware = append(b.middleware, m) - return true - } - if m, ok := value.Interface().(func(context.Context)); ok { - b.middleware = append(b.middleware, m) - return true - } - } - return false -} - -func (b *binder) match(v interface{}) { - value := reflect.ValueOf(v) - // handlers will be recognised as middleware, not struct fields. - // End-Developer has the option to call any handler inside - // the controller's `BeginRequest` and `EndRequest`, the - // state is respected from the method handler already. - if b.storeValueIfMiddleware(value) { - // stored as middleware, continue to the next field, we don't have - // to bind anything here. - return - } - - matcher := func(elemField reflect.StructField) bool { - // If the controller's field is interface then check - // if the given binded value implements that interface. - // i.e MovieController { Service services.MovieService /* interface */ } - // app.Controller("/", new(MovieController), - // services.NewMovieMemoryService(...)) - // - // `services.NewMovieMemoryService` returns a `*MovieMemoryService` - // that implements the `MovieService` interface. - if elemField.Type.Kind() == reflect.Interface { - return value.Type().Implements(elemField.Type) - } - return elemField.Type == value.Type() - } - - handler := func(f *field.Field) { - f.Value = value - } - - b.fields = append(b.fields, field.LookupFields(b.elemType, matcher, handler)...) -} - -func (b *binder) handle(c reflect.Value) { - // we could make check for middlewares here but - // these could easly be used outside of the controller - // so we don't have to initialize a controller to call them - // so they don't belong actually here, we will register them to the - // router itself, before the controller's handler to gain performance, - // look `activator.go#RegisterMethodHandlers` for more. - - elem := c.Elem() // controller should always be a pointer at this state - for _, f := range b.fields { - f.SendTo(elem) - } -} diff --git a/mvc/activator/field/field.go b/mvc/activator/field/field.go deleted file mode 100644 index 1a3d041333..0000000000 --- a/mvc/activator/field/field.go +++ /dev/null @@ -1,220 +0,0 @@ -package field - -import ( - "reflect" -) - -// Field is a controller's field -// contains all the necessary, internal, information -// to work with. -type Field struct { - Name string // the field's original name - // but if a tag with `name: "other"` - // exist then this fill is filled, otherwise it's the same as the Name. - TagName string - Index int - Type reflect.Type - Value reflect.Value - - embedded *Field -} - -// GetIndex returns all the "dimensions" -// of the controller struct field's position that this field is referring to, -// recursively. -// Usage: elem.FieldByIndex(field.getIndex()) -// for example the {0,1} means that the field is on the second field of the first's -// field of this struct. -func (ff Field) GetIndex() []int { - deepIndex := []int{ff.Index} - - if emb := ff.embedded; emb != nil { - deepIndex = append(deepIndex, emb.GetIndex()...) - } - - return deepIndex -} - -// GetType returns the type of the referring field, recursively. -func (ff Field) GetType() reflect.Type { - typ := ff.Type - if emb := ff.embedded; emb != nil { - return emb.GetType() - } - - return typ -} - -// GetFullName returns the full name of that field -// i.e: UserController.SessionController.Manager, -// it's useful for debugging only. -func (ff Field) GetFullName() string { - name := ff.Name - - if emb := ff.embedded; emb != nil { - return name + "." + emb.GetFullName() - } - - return name -} - -// GetTagName returns the tag name of the referring field -// recursively. -func (ff Field) GetTagName() string { - name := ff.TagName - - if emb := ff.embedded; emb != nil { - return emb.GetTagName() - } - - return name -} - -// checkVal checks if that value -// is valid to be set-ed to the new controller's instance. -// Used on binder. -func checkVal(val reflect.Value) bool { - return val.IsValid() && (val.Kind() == reflect.Ptr && !val.IsNil()) && val.CanInterface() -} - -// GetValue returns a valid value of the referring field, recursively. -func (ff Field) GetValue() interface{} { - if ff.embedded != nil { - return ff.embedded.GetValue() - } - - if checkVal(ff.Value) { - return ff.Value.Interface() - } - - return "undefinied value" -} - -// SendTo should be used when this field or its embedded -// has a Value on it. -// It sets the field's value to the "elem" instance, it's the new controller. -func (ff Field) SendTo(elem reflect.Value) { - // note: - // we don't use the getters here - // because we do recursively search by our own here - // to be easier to debug if ever needed. - if embedded := ff.embedded; embedded != nil { - if ff.Index >= 0 { - embedded.SendTo(elem.Field(ff.Index)) - } - return - } - elemField := elem.Field(ff.Index) - if elemField.Kind() == reflect.Ptr && !elemField.IsNil() { - return - } - - elemField.Set(ff.Value) -} - -// lookupTagName checks if the "elemField" struct's field -// contains a tag `name`, if it contains then it returns its value -// otherwise returns the field's original Name. -func lookupTagName(elemField reflect.StructField) string { - vname := elemField.Name - - if taggedName, ok := elemField.Tag.Lookup("name"); ok { - vname = taggedName - } - return vname -} - -// LookupFields iterates all "elem"'s fields and its fields -// if structs, recursively. -// Compares them to the "matcher", if they passed -// then it executes the "handler" if any, -// the handler can change the field as it wants to. -// -// It finally returns that collection of the valid fields, can be empty. -func LookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*Field)) (fields []Field) { - for i, n := 0, elem.NumField(); i < n; i++ { - elemField := elem.Field(i) - if matcher(elemField) { - field := Field{ - Index: i, - Name: elemField.Name, - TagName: lookupTagName(elemField), - Type: elemField.Type, - } - - if handler != nil { - handler(&field) - } - - // we area inside the correct type - fields = append(fields, field) - continue - } - - f := lookupStructField(elemField.Type, matcher, handler) - if f != nil { - fields = append(fields, Field{ - Index: i, - Name: elemField.Name, - Type: elemField.Type, - embedded: f, - }) - } - - } - return -} - -// lookupStructField is here to search for embedded field only, -// is working with the "lookupFields". -// We could just one one function -// for both structured (embedded) fields and normal fields -// but we keep that as it's, a new function like this -// is easier for debugging, if ever needed. -func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*Field)) *Field { - // fmt.Printf("lookup struct for elem: %s\n", elem.Name()) - // ignore if that field is not a struct - if elem.Kind() != reflect.Struct { - return nil - } - - // search by fields. - for i, n := 0, elem.NumField(); i < n; i++ { - elemField := elem.Field(i) - - if matcher(elemField) { - // we area inside the correct type. - f := &Field{ - Index: i, - Name: elemField.Name, - TagName: lookupTagName(elemField), - Type: elemField.Type, - } - - if handler != nil { - handler(f) - } - - return f - } - - // if field is struct and the value is struct - // then try inside its fields for a compatible - // field type. - if elemField.Type.Kind() == reflect.Struct { // 3-level - elemFieldEmb := elem.Field(i) - f := lookupStructField(elemFieldEmb.Type, matcher, handler) - if f != nil { - fp := &Field{ - Index: i, - Name: elemFieldEmb.Name, - TagName: lookupTagName(elemFieldEmb), - Type: elemFieldEmb.Type, - embedded: f, - } - return fp - } - } - } - return nil -} diff --git a/mvc/activator/methodfunc/func_caller.go b/mvc/activator/methodfunc/func_caller.go deleted file mode 100644 index 108b40c0c4..0000000000 --- a/mvc/activator/methodfunc/func_caller.go +++ /dev/null @@ -1,19 +0,0 @@ -package methodfunc - -import ( - "reflect" - - "github.com/kataras/iris/context" -) - -// buildMethodCall builds the method caller. -// We have repeated code here but it's the only way -// to support more than one input arguments without performance cost compared to previous implementation. -// so it's hard-coded written to check the length of input args and their types. -func buildMethodCall(a *ast) func(ctx context.Context, f reflect.Value) { - // if func input arguments are more than one then - // use the Call method (slower). - return func(ctx context.Context, f reflect.Value) { - DispatchFuncResult(ctx, f.Call(a.paramValues(ctx))) - } -} diff --git a/mvc/activator/methodfunc/func_info.go b/mvc/activator/methodfunc/func_info.go deleted file mode 100644 index ca9672cc4e..0000000000 --- a/mvc/activator/methodfunc/func_info.go +++ /dev/null @@ -1,102 +0,0 @@ -package methodfunc - -import ( - "reflect" - "strings" - "unicode" -) - -var availableMethods = [...]string{ - "ANY", // will be registered using the `core/router#APIBuilder#Any` - "ALL", // same as ANY - "NONE", // offline route - // valid http methods - "GET", - "POST", - "PUT", - "DELETE", - "CONNECT", - "HEAD", - "PATCH", - "OPTIONS", - "TRACE", -} - -// FuncInfo is part of the `TController`, -// it contains the index for a specific http method, -// taken from user's controller struct. -type FuncInfo struct { - // Name is the map function name. - Name string - // Trailing is not empty when the Name contains - // characters after the titled method, i.e - // if Name = Get -> empty - // if Name = GetLogin -> Login - // if Name = GetUserPost -> UserPost - Trailing string - - // The Type of the method, includes the receivers. - Type reflect.Type - - // Index is the index of this function inside the controller type. - Index int - // HTTPMethod is the original http method that this - // function should be registered to and serve. - // i.e "GET","POST","PUT"... - HTTPMethod string -} - -// or resolve methods -func fetchInfos(typ reflect.Type) (methods []FuncInfo) { - // search the entire controller - // for any compatible method function - // and add that. - for i, n := 0, typ.NumMethod(); i < n; i++ { - m := typ.Method(i) - - if method, ok := FetchFuncInfo(m); ok { - methods = append(methods, method) - } - } - return -} - -// FetchFuncInfo returns a FuncInfo based on the method of the controller. -func FetchFuncInfo(m reflect.Method) (FuncInfo, bool) { - name := m.Name - - for _, method := range availableMethods { - possibleMethodFuncName := methodTitle(method) - - if strings.Index(name, possibleMethodFuncName) == 0 { - trailing := "" - // if has chars after the method itself - if lname, lmethod := len(name), len(possibleMethodFuncName); lname > lmethod { - ch := rune(name[lmethod]) - // if the next char is upper, otherise just skip the whole func info. - if unicode.IsUpper(ch) { - trailing = name[lmethod:] - } else { - continue - } - } - - info := FuncInfo{ - Name: name, - Trailing: trailing, - Type: m.Type, - HTTPMethod: method, - Index: m.Index, - } - return info, true - - } - } - - return FuncInfo{}, false -} - -func methodTitle(httpMethod string) string { - httpMethodFuncName := strings.Title(strings.ToLower(httpMethod)) - return httpMethodFuncName -} diff --git a/mvc/activator/methodfunc/func_lexer.go b/mvc/activator/methodfunc/func_lexer.go deleted file mode 100644 index e06352a767..0000000000 --- a/mvc/activator/methodfunc/func_lexer.go +++ /dev/null @@ -1,89 +0,0 @@ -package methodfunc - -import ( - "unicode" -) - -const ( - tokenBy = "By" - tokenWildcard = "Wildcard" // should be followed by "By", -) - -// word lexer, not characters. -type lexer struct { - words []string - cur int -} - -func newLexer(s string) *lexer { - l := new(lexer) - l.reset(s) - return l -} - -func (l *lexer) reset(trailing string) { - l.cur = -1 - var words []string - if trailing != "" { - end := len(trailing) - start := -1 - - for i, n := 0, end; i < n; i++ { - c := rune(trailing[i]) - if unicode.IsUpper(c) { - // it doesn't count the last uppercase - if start != -1 { - end = i - words = append(words, trailing[start:end]) - } - start = i - continue - } - end = i + 1 - } - - if end > 0 && len(trailing) >= end { - words = append(words, trailing[start:end]) - } - } - - l.words = words -} - -func (l *lexer) next() (w string) { - cur := l.cur + 1 - - if w = l.peek(cur); w != "" { - l.cur++ - } - - return -} - -func (l *lexer) skip() { - if cur := l.cur + 1; cur < len(l.words) { - l.cur = cur - } else { - l.cur = len(l.words) - 1 - } -} - -func (l *lexer) peek(idx int) string { - if idx < len(l.words) { - return l.words[idx] - } - return "" -} - -func (l *lexer) peekNext() (w string) { - return l.peek(l.cur + 1) -} - -func (l *lexer) peekPrev() (w string) { - if l.cur > 0 { - cur := l.cur - 1 - w = l.words[cur] - } - - return w -} diff --git a/mvc/activator/methodfunc/func_parser.go b/mvc/activator/methodfunc/func_parser.go deleted file mode 100644 index 6ce24e7e90..0000000000 --- a/mvc/activator/methodfunc/func_parser.go +++ /dev/null @@ -1,213 +0,0 @@ -package methodfunc - -import ( - "errors" - "fmt" - "reflect" - "strings" - - "github.com/kataras/iris/context" -) - -var posWords = map[int]string{ - 0: "", - 1: "first", - 2: "second", - 3: "third", - 4: "forth", - 5: "five", - 6: "sixth", - 7: "seventh", - 8: "eighth", - 9: "ninth", -} - -func genParamKey(argIdx int) string { - return "param" + posWords[argIdx] // paramfirst, paramsecond... -} - -const ( - paramTypeInt = "int" - paramTypeLong = "long" - paramTypeBoolean = "boolean" - paramTypeString = "string" - paramTypePath = "path" -) - -var macroTypes = map[string]string{ - "int": paramTypeInt, - "int64": paramTypeLong, - "bool": paramTypeBoolean, - "string": paramTypeString, - // there is "path" param type but it's being captured "on-air" - // "file" param type is not supported by the current implementation, yet - // but if someone ask for it I'll implement it, it's easy. -} - -type funcParser struct { - info FuncInfo - lexer *lexer -} - -func newFuncParser(info FuncInfo) *funcParser { - return &funcParser{ - info: info, - lexer: newLexer(info.Trailing), - } -} - -func (p *funcParser) parse() (*ast, error) { - a := new(ast) - funcArgPos := 0 - - for { - w := p.lexer.next() - if w == "" { - break - } - - if w == tokenBy { - funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver. - - // No need for these: - // ByBy will act like /{param:type}/{param:type} as users expected - // if func input arguments are there, else act By like normal path /by. - // - // if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path - // a.relPath += "/" + strings.ToLower(w) - // continue - // } - - if err := p.parsePathParam(a, w, funcArgPos); err != nil { - return nil, err - } - - continue - } - - a.relPath += "/" + strings.ToLower(w) - } - - // This fixes a problem when developer misses to append the keyword `By` - // to the method function when input arguments are declared (for path parameters binding). - // We could just use it with `By` keyword but this is not a good practise - // because what happens if we will become naive and declare something like - // Get(id int) and GetBy(username string) or GetBy(id int) ? it's not working because of duplication of the path. - // Docs are clear about that but we are humans, they may do a mistake by accident but - // framework will not allow that. - // So the best thing we can do to help prevent those errors is by printing that message - // below to the developer. - // Note: it should be at the end of the words loop because a.dynamic may be true later on. - if numIn := p.info.Type.NumIn(); numIn > 1 && !a.dynamic { - return nil, fmt.Errorf("found %d input arguments but keyword 'By' is missing from '%s'", - // -1 because end-developer wants to know the actual input arguments, without the struct holder. - numIn-1, p.info.Name) - } - return a, nil -} - -func (p *funcParser) parsePathParam(a *ast, w string, funcArgPos int) error { - typ := p.info.Type - - if typ.NumIn() <= funcArgPos { - // old: - // return nil, errors.New("keyword 'By' found but length of input receivers are not match for " + - // p.info.Name) - - // By found but input arguments are not there, so act like /by path without restricts. - a.relPath += "/" + strings.ToLower(w) - return nil - } - - var ( - paramKey = genParamKey(funcArgPos) // paramfirst, paramsecond... - paramType = paramTypeString // default string - ) - - // string, int... - goType := typ.In(funcArgPos).Name() - nextWord := p.lexer.peekNext() - - if nextWord == tokenWildcard { - p.lexer.skip() // skip the Wildcard word. - paramType = paramTypePath - } else if pType, ok := macroTypes[goType]; ok { - // it's not wildcard, so check base on our available macro types. - paramType = pType - } else { - return errors.New("invalid syntax for " + p.info.Name) - } - - a.paramKeys = append(a.paramKeys, paramKey) - a.paramTypes = append(a.paramTypes, paramType) - // /{paramfirst:path}, /{paramfirst:long}... - a.relPath += fmt.Sprintf("/{%s:%s}", paramKey, paramType) - a.dynamic = true - - if nextWord == "" && typ.NumIn() > funcArgPos+1 { - // By is the latest word but func is expected - // more path parameters values, i.e: - // GetBy(name string, age int) - // The caller (parse) doesn't need to know - // about the incremental funcArgPos because - // it will not need it. - return p.parsePathParam(a, nextWord, funcArgPos+1) - } - - return nil -} - -type ast struct { - paramKeys []string // paramfirst, paramsecond... [0] - paramTypes []string // string, int, long, path... [0] - relPath string - dynamic bool // when paramKeys (and paramTypes, are equal) > 0 -} - -// moved to func_caller#buildMethodcall, it's bigger and with repeated code -// than this, below function but it's faster. -// func (a *ast) MethodCall(ctx context.Context, f reflect.Value) { -// if a.dynamic { -// f.Call(a.paramValues(ctx)) -// return -// } -// -// f.Interface().(func())() -// } - -func (a *ast) paramValues(ctx context.Context) []reflect.Value { - if !a.dynamic { - return nil - } - - l := len(a.paramKeys) - values := make([]reflect.Value, l, l) - - for i := 0; i < l; i++ { - paramKey := a.paramKeys[i] - paramType := a.paramTypes[i] - values[i] = getParamValueFromType(ctx, paramType, paramKey) - } - - return values -} - -func getParamValueFromType(ctx context.Context, paramType string, paramKey string) reflect.Value { - if paramType == paramTypeInt { - v, _ := ctx.Params().GetInt(paramKey) - return reflect.ValueOf(v) - } - - if paramType == paramTypeLong { - v, _ := ctx.Params().GetInt64(paramKey) - return reflect.ValueOf(v) - } - - if paramType == paramTypeBoolean { - v, _ := ctx.Params().GetBool(paramKey) - return reflect.ValueOf(v) - } - - // string, path... - return reflect.ValueOf(ctx.Params().Get(paramKey)) -} diff --git a/mvc/activator/methodfunc/func_result_dispatcher.go b/mvc/activator/methodfunc/func_result_dispatcher.go deleted file mode 100644 index 480a9e362e..0000000000 --- a/mvc/activator/methodfunc/func_result_dispatcher.go +++ /dev/null @@ -1,230 +0,0 @@ -package methodfunc - -import ( - "reflect" - "strings" - - "github.com/kataras/iris/context" -) - -// Result is a response dispatcher. -// All types that complete this interface -// can be returned as values from the method functions. -type Result interface { - // Dispatch should sends the response to the context's response writer. - Dispatch(ctx context.Context) -} - -const slashB byte = '/' - -type compatibleErr interface { - Error() string -} - -// DefaultErrStatusCode is the default error status code (400) -// when the response contains an error which is not nil. -var DefaultErrStatusCode = 400 - -// DispatchErr writes the error to the response. -func DispatchErr(ctx context.Context, status int, err error) { - if status < 400 { - status = DefaultErrStatusCode - } - ctx.StatusCode(status) - if text := err.Error(); text != "" { - ctx.WriteString(text) - ctx.StopExecution() - } -} - -// DispatchCommon is being used internally to send -// commonly used data to the response writer with a smart way. -func DispatchCommon(ctx context.Context, - statusCode int, contentType string, content []byte, v interface{}, err error, found bool) { - - // if we have a false boolean as a return value - // then skip everything and fire a not found, - // we even don't care about the given status code or the object or the content. - if !found { - ctx.NotFound() - return - } - - status := statusCode - if status == 0 { - status = 200 - } - - if err != nil { - DispatchErr(ctx, status, err) - return - } - - // write the status code, the rest will need that before any write ofc. - ctx.StatusCode(status) - if contentType == "" { - // to respect any ctx.ContentType(...) call - // especially if v is not nil. - contentType = ctx.GetContentType() - } - - if v != nil { - if d, ok := v.(Result); ok { - // write the content type now (internal check for empty value) - ctx.ContentType(contentType) - d.Dispatch(ctx) - return - } - - if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) { - _, err = ctx.JSONP(v) - } else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) { - _, err = ctx.XML(v, context.XML{Indent: " "}) - } else { - // defaults to json if content type is missing or its application/json. - _, err = ctx.JSON(v, context.JSON{Indent: " "}) - } - - if err != nil { - DispatchErr(ctx, status, err) - } - - return - } - - ctx.ContentType(contentType) - // .Write even len(content) == 0 , this should be called in order to call the internal tryWriteHeader, - // it will not cost anything. - ctx.Write(content) -} - -// DispatchFuncResult is being used internally to resolve -// and send the method function's output values to the -// context's response writer using a smart way which -// respects status code, content type, content, custom struct -// and an error type. -// Supports for: -// func(c *ExampleController) Get() string | -// (string, string) | -// (string, int) | -// ... -// int | -// (int, string | -// (string, error) | -// ... -// error | -// (int, error) | -// (customStruct, error) | -// ... -// bool | -// (int, bool) | -// (string, bool) | -// (customStruct, bool) | -// ... -// customStruct | -// (customStruct, int) | -// (customStruct, string) | -// Result or (Result, error) and so on... -// -// where Get is an HTTP METHOD. -func DispatchFuncResult(ctx context.Context, values []reflect.Value) { - numOut := len(values) - if numOut == 0 { - return - } - - var ( - // if statusCode > 0 then send this status code. - // Except when err != nil then check if status code is < 400 and - // if it's set it as DefaultErrStatusCode. - // Except when found == false, then the status code is 404. - statusCode int - // if not empty then use that as content type, - // if empty and custom != nil then set it to application/json. - contentType string - // if len > 0 then write that to the response writer as raw bytes, - // except when found == false or err != nil or custom != nil. - content []byte - // if not nil then check - // for content type (or json default) and send the custom data object - // except when found == false or err != nil. - custom interface{} - // if not nil then check for its status code, - // if not status code or < 400 then set it as DefaultErrStatusCode - // and fire the error's text. - err error - // if false then skip everything and fire 404. - found = true // defaults to true of course, otherwise will break :) - ) - - for _, v := range values { - // order of these checks matters - // for example, first we need to check for status code, - // secondly the string (for content type and content)... - if !v.IsValid() { - continue - } - - f := v.Interface() - - if b, ok := f.(bool); ok { - found = b - if !found { - // skip everything, we don't care about other return values, - // this boolean is the higher in order. - break - } - continue - } - - if i, ok := f.(int); ok { - statusCode = i - continue - } - - if s, ok := f.(string); ok { - // a string is content type when it contains a slash and - // content or custom struct is being calculated already; - // (string -> content, string-> content type) - // (customStruct, string -> content type) - if (len(content) > 0 || custom != nil) && strings.IndexByte(s, slashB) > 0 { - contentType = s - } else { - // otherwise is content - content = []byte(s) - } - - continue - } - - if b, ok := f.([]byte); ok { - // it's raw content, get the latest - content = b - continue - } - - if e, ok := f.(compatibleErr); ok { - if e != nil { // it's always not nil but keep it here. - err = e - if statusCode < 400 { - statusCode = DefaultErrStatusCode - } - break // break on first error, error should be in the end but we - // need to know break the dispatcher if any error. - // at the end; we don't want to write anything to the response if error is not nil. - } - continue - } - - // else it's a custom struct or a dispatcher, we'll decide later - // because content type and status code matters - // do that check in order to be able to correctly dispatch: - // (customStruct, error) -> customStruct filled and error is nil - if custom == nil && f != nil { - custom = f - } - - } - - DispatchCommon(ctx, statusCode, contentType, content, custom, err, found) -} diff --git a/mvc/activator/methodfunc/methodfunc.go b/mvc/activator/methodfunc/methodfunc.go deleted file mode 100644 index 3a8f606552..0000000000 --- a/mvc/activator/methodfunc/methodfunc.go +++ /dev/null @@ -1,68 +0,0 @@ -package methodfunc - -import ( - "reflect" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/errors" -) - -// MethodFunc the handler function. -type MethodFunc struct { - FuncInfo - // MethodCall fires the actual handler. - // The "ctx" is the current context, helps us to get any path parameter's values. - // - // The "f" is the controller's function which is responsible - // for that request for this http method. - // That function can accept one parameter. - // - // The default callers (and the only one for now) - // are pre-calculated by the framework. - MethodCall func(ctx context.Context, f reflect.Value) - RelPath string -} - -// Resolve returns all the method funcs -// necessary information and actions to -// perform the request. -func Resolve(typ reflect.Type) ([]MethodFunc, error) { - r := errors.NewReporter() - var methodFuncs []MethodFunc - infos := fetchInfos(typ) - for _, info := range infos { - methodFunc, err := ResolveMethodFunc(info) - if r.AddErr(err) { - continue - } - methodFuncs = append(methodFuncs, methodFunc) - } - - return methodFuncs, r.Return() -} - -// ResolveMethodFunc resolves a single `MethodFunc` from a single `FuncInfo`. -func ResolveMethodFunc(info FuncInfo, paramKeys ...string) (MethodFunc, error) { - parser := newFuncParser(info) - a, err := parser.parse() - if err != nil { - return MethodFunc{}, err - } - - if len(paramKeys) > 0 { - a.paramKeys = paramKeys - } - - methodFunc := MethodFunc{ - RelPath: a.relPath, - FuncInfo: info, - MethodCall: buildMethodCall(a), - } - - /* TODO: split the method path and ast param keys, and all that - because now we want to use custom param keys but 'paramfirst' is set-ed. - - */ - - return methodFunc, nil -} diff --git a/mvc/activator/model/model.go b/mvc/activator/model/model.go deleted file mode 100644 index 078cc6d46c..0000000000 --- a/mvc/activator/model/model.go +++ /dev/null @@ -1,73 +0,0 @@ -package model - -import ( - "reflect" - - "github.com/kataras/iris/mvc/activator/field" - - "github.com/kataras/iris/context" -) - -// Controller is responsible -// to load and handle the `Model(s)` inside a controller struct -// via the `iris:"model"` tag field. -// It stores the optional models from -// the struct's fields values that -// are being setted by the method function -// and set them as ViewData. -type Controller struct { - fields []field.Field -} - -// Load tries to lookup and set for any valid model field. -// Returns nil if no models are being used. -func Load(typ reflect.Type) *Controller { - matcher := func(f reflect.StructField) bool { - if tag, ok := f.Tag.Lookup("iris"); ok { - if tag == "model" { - return true - } - } - return false - } - - fields := field.LookupFields(typ.Elem(), matcher, nil) - - if len(fields) == 0 { - return nil - } - - mc := &Controller{ - fields: fields, - } - return mc -} - -// Handle transfer the models to the view. -func (mc *Controller) Handle(ctx context.Context, c reflect.Value) { - elem := c.Elem() // controller should always be a pointer at this state - - for _, f := range mc.fields { - - index := f.GetIndex() - typ := f.GetType() - name := f.GetTagName() - - elemField := elem.FieldByIndex(index) - // check if current controller's element field - // is valid, is not nil and it's type is the same (should be but make that check to be sure). - if !elemField.IsValid() || - (elemField.Kind() == reflect.Ptr && elemField.IsNil()) || - elemField.Type() != typ { - continue - } - - fieldValue := elemField.Interface() - ctx.ViewData(name, fieldValue) - // /*maybe some time in the future*/ if resetable { - // // clean up - // elemField.Set(reflect.Zero(typ)) - // } - - } -} diff --git a/mvc/activator/persistence/persistence.go b/mvc/activator/persistence/persistence.go deleted file mode 100644 index 8b81ccbc93..0000000000 --- a/mvc/activator/persistence/persistence.go +++ /dev/null @@ -1,60 +0,0 @@ -package persistence - -import ( - "reflect" - - "github.com/kataras/iris/mvc/activator/field" -) - -// Controller is responsible to load from the original -// end-developer's main controller's value -// and re-store the persistence data by scanning the original. -// It stores and sets to each new controller -// the optional data that should be shared among all requests. -type Controller struct { - fields []field.Field -} - -// Load scans and load for persistence data based on the `iris:"persistence"` tag. -// -// The type is the controller's Type. -// the "val" is the original end-developer's controller's Value. -// Returns nil if no persistence data to store found. -func Load(typ reflect.Type, val reflect.Value) *Controller { - matcher := func(elemField reflect.StructField) bool { - if tag, ok := elemField.Tag.Lookup("iris"); ok { - if tag == "persistence" { - return true - } - } - return false - } - - handler := func(f *field.Field) { - valF := val.Field(f.Index) - if valF.IsValid() || (valF.Kind() == reflect.Ptr && !valF.IsNil()) { - val := reflect.ValueOf(valF.Interface()) - if val.IsValid() || (val.Kind() == reflect.Ptr && !val.IsNil()) { - f.Value = val - } - } - } - - fields := field.LookupFields(typ.Elem(), matcher, handler) - - if len(fields) == 0 { - return nil - } - - return &Controller{ - fields: fields, - } -} - -// Handle re-stores the persistence data at the current controller. -func (pc *Controller) Handle(c reflect.Value) { - elem := c.Elem() // controller should always be a pointer at this state - for _, f := range pc.fields { - f.SendTo(elem) - } -} diff --git a/mvc/controller.go b/mvc/controller.go deleted file mode 100644 index fe50663862..0000000000 --- a/mvc/controller.go +++ /dev/null @@ -1,369 +0,0 @@ -package mvc - -import ( - "reflect" - "strings" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/memstore" - "github.com/kataras/iris/mvc/activator" -) - -// C is the lightweight BaseController type as an alternative of the `Controller` struct type. -// It contains only the Name of the controller and the Context, it's the best option -// to balance the performance cost reflection uses -// if your controller uses the new func output values dispatcher feature; -// func(c *ExampleController) Get() string | -// (string, string) | -// (string, int) | -// int | -// (int, string | -// (string, error) | -// bool | -// (any, bool) | -// error | -// (int, error) | -// (customStruct, error) | -// customStruct | -// (customStruct, int) | -// (customStruct, string) | -// Result or (Result, error) -// where Get is an HTTP Method func. -// -// Look `core/router#APIBuilder#Controller` method too. -// -// It completes the `activator.BaseController` interface. -// -// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview/web/controllers. -// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go#L17. -type C struct { - // The Name of the `C` controller. - Name string - // The current context.Context. - // - // we have to name it for two reasons: - // 1: can't ignore these via reflection, it doesn't give an option to - // see if the functions is derived from another type. - // 2: end-developer may want to use some method functions - // or any fields that could be conflict with the context's. - Ctx context.Context -} - -var _ activator.BaseController = &C{} - -// SetName sets the controller's full name. -// It's called internally. -func (c *C) SetName(name string) { c.Name = name } - -// BeginRequest starts the request by initializing the `Context` field. -func (c *C) BeginRequest(ctx context.Context) { c.Ctx = ctx } - -// EndRequest does nothing, is here to complete the `BaseController` interface. -func (c *C) EndRequest(ctx context.Context) {} - -// Controller is the base controller for the high level controllers instances. -// -// This base controller is used as an alternative way of building -// APIs, the controller can register all type of http methods. -// -// Keep note that controllers are bit slow -// because of the reflection use however it's as fast as possible because -// it does preparation before the serve-time handler but still -// remains slower than the low-level handlers -// such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`. -// -// -// All fields that are tagged with iris:"persistence"` or binded -// are being persistence and kept the same between the different requests. -// -// An Example Controller can be: -// -// type IndexController struct { -// Controller -// } -// -// func (c *IndexController) Get() { -// c.Tmpl = "index.html" -// c.Data["title"] = "Index page" -// c.Data["message"] = "Hello world!" -// } -// -// Usage: app.Controller("/", new(IndexController)) -// -// -// Another example with bind: -// -// type UserController struct { -// mvc.Controller -// -// DB *DB -// CreatedAt time.Time -// } -// -// // Get serves using the User controller when HTTP Method is "GET". -// func (c *UserController) Get() { -// c.Tmpl = "user/index.html" -// c.Data["title"] = "User Page" -// c.Data["username"] = "kataras " + c.Params.Get("userid") -// c.Data["connstring"] = c.DB.Connstring -// c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds() -// } -// -// Usage: app.Controller("/user/{id:int}", new(UserController), db, time.Now()) -// Note: Binded values of context.Handler type are being recognised as middlewares by the router. -// -// Look `core/router/APIBuilder#Controller` method too. -// -// It completes the `activator.BaseController` interface. -type Controller struct { - // Name contains the current controller's full name. - // - // doesn't change on different paths. - Name string - - // contains the `Name` as different words, all lowercase, - // without the "Controller" suffix if exists. - // we need this as field because the activator - // we will not try to parse these if not needed - // it's up to the end-developer to call `RelPath()` or `RelTmpl()` - // which will result to fill them. - // - // doesn't change on different paths. - nameAsWords []string - - // relPath the "as assume" relative request path. - // - // If UserController and request path is "/user/messages" then it's "/messages" - // if UserPostController and request path is "/user/post" then it's "/" - // if UserProfile and request path is "/user/profile/likes" then it's "/likes" - // - // doesn't change on different paths. - relPath string - - // request path and its parameters, read-write. - // Path is the current request path, if changed then it redirects. - Path string - // Params are the request path's parameters, i.e - // for route like "/user/{id}" and request to "/user/42" - // it contains the "id" = 42. - Params *context.RequestParams - - // some info read and write, - // can be already set-ed by previous handlers as well. - Status int - Values *memstore.Store - - // relTmpl the "as assume" relative path to the view root folder. - // - // If UserController then it's "user/" - // if UserPostController then it's "user/post/" - // if UserProfile then it's "user/profile/". - // - // doesn't change on different paths. - relTmpl string - - // view read and write, - // can be already set-ed by previous handlers as well. - Layout string - Tmpl string - Data map[string]interface{} - - ContentType string - Text string // response as string - - // give access to the request context itself. - Ctx context.Context -} - -var _ activator.BaseController = &Controller{} - -var ctrlSuffix = reflect.TypeOf(Controller{}).Name() - -// SetName sets the controller's full name. -// It's called internally. -func (c *Controller) SetName(name string) { - c.Name = name -} - -func (c *Controller) getNameWords() []string { - if len(c.nameAsWords) == 0 { - c.nameAsWords = findCtrlWords(c.Name) - } - return c.nameAsWords -} - -// Route returns the current request controller's context read-only access route. -func (c *Controller) Route() context.RouteReadOnly { - return c.Ctx.GetCurrentRoute() -} - -const slashStr = "/" - -// RelPath tries to return the controller's name -// without the "Controller" prefix, all lowercase -// prefixed with slash and splited by slash appended -// with the rest of the request path. -// For example: -// If UserController and request path is "/user/messages" then it's "/messages" -// if UserPostController and request path is "/user/post" then it's "/" -// if UserProfile and request path is "/user/profile/likes" then it's "/likes" -// -// It's useful for things like path checking and redirect. -func (c *Controller) RelPath() string { - if c.relPath == "" { - w := c.getNameWords() - rel := strings.Join(w, slashStr) - - reqPath := c.Ctx.Path() - if len(reqPath) == 0 { - // it never come here - // but to protect ourselves just return an empty slash. - return slashStr - } - // [1:]to ellimuate the prefixes like "//" - // request path has always "/" - rel = strings.Replace(reqPath[1:], rel, "", 1) - if rel == "" { - rel = slashStr - } - c.relPath = rel - // this will return any dynamic path after the static one - // or a a slash "/": - // - // reqPath := c.Ctx.Path() - // if len(reqPath) == 0 { - // // it never come here - // // but to protect ourselves just return an empty slash. - // return slashStr - // } - // var routeVParams []string - // c.Params.Visit(func(key string, value string) { - // routeVParams = append(routeVParams, value) - // }) - - // rel := c.Route().StaticPath() - // println(rel) - // // [1:]to ellimuate the prefixes like "//" - // // request path has always "/" - // rel = strings.Replace(reqPath, rel[1:], "", 1) - // println(rel) - // if rel == "" { - // rel = slashStr - // } - // c.relPath = rel - } - - return c.relPath -} - -// RelTmpl tries to return the controller's name -// without the "Controller" prefix, all lowercase -// splited by slash and suffixed by slash. -// For example: -// If UserController then it's "user/" -// if UserPostController then it's "user/post/" -// if UserProfile then it's "user/profile/". -// -// It's useful to locate templates if the controller and views path have aligned names. -func (c *Controller) RelTmpl() string { - if c.relTmpl == "" { - c.relTmpl = strings.Join(c.getNameWords(), slashStr) + slashStr - } - return c.relTmpl -} - -// Write writes to the client via the context's ResponseWriter. -// Controller completes the `io.Writer` interface for the shake of ease. -func (c *Controller) Write(contents []byte) (int, error) { - c.tryWriteHeaders() - return c.Ctx.ResponseWriter().Write(contents) -} - -// Writef formats according to a format specifier and writes to the response. -func (c *Controller) Writef(format string, a ...interface{}) (int, error) { - c.tryWriteHeaders() - return c.Ctx.ResponseWriter().Writef(format, a...) -} - -// BeginRequest starts the main controller -// it initialize the Ctx and other fields. -// -// It's called internally. -// End-Developer can ovverride it but it still MUST be called. -func (c *Controller) BeginRequest(ctx context.Context) { - // path and path params - c.Path = ctx.Path() - c.Params = ctx.Params() - // response status code - c.Status = ctx.GetStatusCode() - - // share values - c.Values = ctx.Values() - // view data for templates, remember - // each controller is a new instance, so - // checking for nil and then init those type of fields - // have no meaning. - c.Data = make(map[string]interface{}, 0) - - // context itself - c.Ctx = ctx -} - -func (c *Controller) tryWriteHeaders() { - if c.Status > 0 && c.Status != c.Ctx.GetStatusCode() { - c.Ctx.StatusCode(c.Status) - } - - if c.ContentType != "" { - c.Ctx.ContentType(c.ContentType) - } -} - -// EndRequest is the final method which will be executed -// before response sent. -// -// It checks for the fields and calls the necessary context's -// methods to modify the response to the client. -// -// It's called internally. -// End-Developer can ovveride it but still should be called at the end. -func (c *Controller) EndRequest(ctx context.Context) { - if ctx.ResponseWriter().Written() >= 0 { // status code only (0) or actual body written(>0) - return - } - - if path := c.Path; path != "" && path != ctx.Path() { - // then redirect and exit. - ctx.Redirect(path, c.Status) - return - } - - c.tryWriteHeaders() - if response := c.Text; response != "" { - ctx.WriteString(response) - return // exit here - } - - if view := c.Tmpl; view != "" { - if layout := c.Layout; layout != "" { - ctx.ViewLayout(layout) - } - if len(c.Data) > 0 { - dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey() - // In order to respect any c.Ctx.ViewData that may called manually before; - if ctx.Values().Get(dataKey) == nil { - // if no c.Ctx.ViewData then it's empty do a - // pure set, it's faster. - ctx.Values().Set(dataKey, c.Data) - } else { - // else do a range loop and set the data one by one. - for k, v := range c.Data { - ctx.ViewData(k, v) - } - } - - } - - ctx.View(view) - } -} diff --git a/mvc/controller_test.go b/mvc/controller_test.go deleted file mode 100644 index 362249d5fa..0000000000 --- a/mvc/controller_test.go +++ /dev/null @@ -1,531 +0,0 @@ -// black-box testing -package mvc_test - -import ( - "testing" - - "github.com/kataras/iris" - "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc" - "github.com/kataras/iris/mvc/activator" - - "github.com/kataras/iris/core/router" - "github.com/kataras/iris/httptest" -) - -type testController struct { - mvc.Controller -} - -var writeMethod = func(c mvc.Controller) { - c.Ctx.Writef(c.Ctx.Method()) -} - -func (c *testController) Get() { - writeMethod(c.Controller) -} -func (c *testController) Post() { - writeMethod(c.Controller) -} -func (c *testController) Put() { - writeMethod(c.Controller) -} -func (c *testController) Delete() { - writeMethod(c.Controller) -} -func (c *testController) Connect() { - writeMethod(c.Controller) -} -func (c *testController) Head() { - writeMethod(c.Controller) -} -func (c *testController) Patch() { - writeMethod(c.Controller) -} -func (c *testController) Options() { - writeMethod(c.Controller) -} -func (c *testController) Trace() { - writeMethod(c.Controller) -} - -type ( - testControllerAll struct{ mvc.Controller } - testControllerAny struct{ mvc.Controller } // exactly the same as All -) - -func (c *testControllerAll) All() { - writeMethod(c.Controller) -} - -func (c *testControllerAny) Any() { - writeMethod(c.Controller) -} - -func TestControllerMethodFuncs(t *testing.T) { - app := iris.New() - app.Controller("/", new(testController)) - app.Controller("/all", new(testControllerAll)) - app.Controller("/any", new(testControllerAny)) - - e := httptest.New(t, app) - for _, method := range router.AllMethods { - - e.Request(method, "/").Expect().Status(iris.StatusOK). - Body().Equal(method) - - e.Request(method, "/all").Expect().Status(iris.StatusOK). - Body().Equal(method) - - e.Request(method, "/any").Expect().Status(iris.StatusOK). - Body().Equal(method) - } -} - -func TestControllerMethodAndPathHandleMany(t *testing.T) { - app := iris.New() - app.Controller("/ /path1 /path2 /path3", new(testController)) - - e := httptest.New(t, app) - for _, method := range router.AllMethods { - - e.Request(method, "/").Expect().Status(iris.StatusOK). - Body().Equal(method) - - e.Request(method, "/path1").Expect().Status(iris.StatusOK). - Body().Equal(method) - - e.Request(method, "/path2").Expect().Status(iris.StatusOK). - Body().Equal(method) - } -} - -type testControllerPersistence struct { - mvc.Controller - Data string `iris:"persistence"` -} - -func (c *testControllerPersistence) Get() { - c.Ctx.WriteString(c.Data) -} - -func TestControllerPersistenceFields(t *testing.T) { - data := "this remains the same for all requests" - app := iris.New() - app.Controller("/", &testControllerPersistence{Data: data}) - e := httptest.New(t, app) - e.GET("/").Expect().Status(iris.StatusOK). - Body().Equal(data) -} - -type testControllerBeginAndEndRequestFunc struct { - mvc.Controller - - Username string -} - -// called before of every method (Get() or Post()). -// -// useful when more than one methods using the -// same request values or context's function calls. -func (c *testControllerBeginAndEndRequestFunc) BeginRequest(ctx context.Context) { - c.Controller.BeginRequest(ctx) - c.Username = ctx.Params().Get("username") - // or t.Params.Get("username") because the - // t.Ctx == ctx and is being initialized at the t.Controller.BeginRequest. -} - -// called after every method (Get() or Post()). -func (c *testControllerBeginAndEndRequestFunc) EndRequest(ctx context.Context) { - ctx.Writef("done") // append "done" to the response - c.Controller.EndRequest(ctx) -} - -func (c *testControllerBeginAndEndRequestFunc) Get() { - c.Ctx.Writef(c.Username) -} - -func (c *testControllerBeginAndEndRequestFunc) Post() { - c.Ctx.Writef(c.Username) -} - -func TestControllerBeginAndEndRequestFunc(t *testing.T) { - app := iris.New() - app.Controller("/profile/{username}", new(testControllerBeginAndEndRequestFunc)) - - e := httptest.New(t, app) - usernames := []string{ - "kataras", - "makis", - "efi", - "rg", - "bill", - "whoisyourdaddy", - } - doneResponse := "done" - - for _, username := range usernames { - e.GET("/profile/" + username).Expect().Status(iris.StatusOK). - Body().Equal(username + doneResponse) - e.POST("/profile/" + username).Expect().Status(iris.StatusOK). - Body().Equal(username + doneResponse) - } -} - -func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) { - app := iris.New() - usernames := map[string]bool{ - "kataras": true, - "makis": false, - "efi": true, - "rg": false, - "bill": true, - "whoisyourdaddy": false, - } - middlewareCheck := func(ctx context.Context) { - for username, allow := range usernames { - if ctx.Params().Get("username") == username && allow { - ctx.Next() - return - } - } - - ctx.StatusCode(iris.StatusForbidden) - ctx.Writef("forbidden") - } - - app.Controller("/profile/{username}", new(testControllerBeginAndEndRequestFunc), middlewareCheck) - - e := httptest.New(t, app) - - doneResponse := "done" - - for username, allow := range usernames { - getEx := e.GET("/profile/" + username).Expect() - if allow { - getEx.Status(iris.StatusOK). - Body().Equal(username + doneResponse) - } else { - getEx.Status(iris.StatusForbidden).Body().Equal("forbidden") - } - - postEx := e.POST("/profile/" + username).Expect() - if allow { - postEx.Status(iris.StatusOK). - Body().Equal(username + doneResponse) - } else { - postEx.Status(iris.StatusForbidden).Body().Equal("forbidden") - } - } -} - -type Model struct { - Username string -} - -type testControllerModel struct { - mvc.Controller - - TestModel Model `iris:"model" name:"myModel"` - TestModel2 Model `iris:"model"` -} - -func (c *testControllerModel) Get() { - username := c.Ctx.Params().Get("username") - c.TestModel = Model{Username: username} - c.TestModel2 = Model{Username: username + "2"} -} - -func writeModels(ctx context.Context, names ...string) { - if expected, got := len(names), len(ctx.GetViewData()); expected != got { - ctx.Writef("expected view data length: %d but got: %d for names: %s", expected, got, names) - return - } - - for _, name := range names { - - m, ok := ctx.GetViewData()[name] - if !ok { - ctx.Writef("fail load and set the %s", name) - return - } - - model, ok := m.(Model) - if !ok { - ctx.Writef("fail to override the %s' name by the tag", name) - return - } - - ctx.Writef(model.Username) - } -} - -func (c *testControllerModel) EndRequest(ctx context.Context) { - writeModels(ctx, "myModel", "TestModel2") - c.Controller.EndRequest(ctx) -} -func TestControllerModel(t *testing.T) { - app := iris.New() - app.Controller("/model/{username}", new(testControllerModel)) - - e := httptest.New(t, app) - usernames := []string{ - "kataras", - "makis", - } - - for _, username := range usernames { - e.GET("/model/" + username).Expect().Status(iris.StatusOK). - Body().Equal(username + username + "2") - } -} - -type testBindType struct { - title string -} - -type testControllerBindStruct struct { - mvc.Controller - // should start with upper letter of course - TitlePointer *testBindType // should have the value of the "myTitlePtr" on test - TitleValue testBindType // should have the value of the "myTitleV" on test - Other string // just another type to check the field collection, should be empty -} - -func (t *testControllerBindStruct) Get() { - t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) -} - -type testControllerBindDeep struct { - testControllerBindStruct -} - -func (t *testControllerBindDeep) Get() { - // t.testControllerBindStruct.Get() - t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) -} -func TestControllerBind(t *testing.T) { - app := iris.New() - // app.Logger().SetLevel("debug") - - t1, t2 := "my pointer title", "val title" - // test bind pointer to pointer of the correct type - myTitlePtr := &testBindType{title: t1} - // test bind value to value of the correct type - myTitleV := testBindType{title: t2} - - app.Controller("/", new(testControllerBindStruct), myTitlePtr, myTitleV) - app.Controller("/deep", new(testControllerBindDeep), myTitlePtr, myTitleV) - - e := httptest.New(t, app) - expected := t1 + t2 - e.GET("/").Expect().Status(iris.StatusOK). - Body().Equal(expected) - e.GET("/deep").Expect().Status(iris.StatusOK). - Body().Equal(expected) -} - -type ( - UserController struct{ mvc.Controller } - Profile struct{ mvc.Controller } - UserProfilePostController struct{ mvc.Controller } -) - -func writeRelatives(c mvc.Controller) { - c.Ctx.JSON(context.Map{ - "RelPath": c.RelPath(), - "TmplPath": c.RelTmpl(), - }) -} -func (c *UserController) Get() { - writeRelatives(c.Controller) -} - -func (c *Profile) Get() { - writeRelatives(c.Controller) -} - -func (c *UserProfilePostController) Get() { - writeRelatives(c.Controller) -} - -func TestControllerRelPathAndRelTmpl(t *testing.T) { - app := iris.New() - var tests = map[string]context.Map{ - // UserController - "/user": {"RelPath": "/", "TmplPath": "user/"}, - "/user/42": {"RelPath": "/42", "TmplPath": "user/"}, - "/user/me": {"RelPath": "/me", "TmplPath": "user/"}, - // Profile (without Controller suffix, should work as expected) - "/profile": {"RelPath": "/", "TmplPath": "profile/"}, - "/profile/42": {"RelPath": "/42", "TmplPath": "profile/"}, - "/profile/me": {"RelPath": "/me", "TmplPath": "profile/"}, - // UserProfilePost - "/user/profile/post": {"RelPath": "/", "TmplPath": "user/profile/post/"}, - "/user/profile/post/42": {"RelPath": "/42", "TmplPath": "user/profile/post/"}, - "/user/profile/post/mine": {"RelPath": "/mine", "TmplPath": "user/profile/post/"}, - } - - app.Controller("/user /user/me /user/{id}", - new(UserController)) - - app.Controller("/profile /profile/me /profile/{id}", - new(Profile)) - - app.Controller("/user/profile/post /user/profile/post/mine /user/profile/post/{id}", - new(UserProfilePostController)) - - e := httptest.New(t, app) - for path, tt := range tests { - e.GET(path).Expect().Status(iris.StatusOK).JSON().Equal(tt) - } -} - -type testCtrl0 struct { - testCtrl00 -} - -func (c *testCtrl0) Get() { - username := c.Params.Get("username") - c.Model = Model{Username: username} -} - -func (c *testCtrl0) EndRequest(ctx context.Context) { - writeModels(ctx, "myModel") - - if c.TitlePointer == nil { - ctx.Writef("\nTitlePointer is nil!\n") - } else { - ctx.Writef(c.TitlePointer.title) - } - - //should be the same as `.testCtrl000.testCtrl0000.EndRequest(ctx)` - c.testCtrl00.EndRequest(ctx) -} - -type testCtrl00 struct { - testCtrl000 - - Model Model `iris:"model" name:"myModel"` -} - -type testCtrl000 struct { - testCtrl0000 - - TitlePointer *testBindType -} - -type testCtrl0000 struct { - mvc.Controller -} - -func (c *testCtrl0000) EndRequest(ctx context.Context) { - ctx.Writef("finish") -} - -func TestControllerInsideControllerRecursively(t *testing.T) { - var ( - username = "gerasimos" - title = "mytitle" - expected = username + title + "finish" - ) - - app := iris.New() - - app.Controller("/user/{username}", new(testCtrl0), - &testBindType{title: title}) - - e := httptest.New(t, app) - e.GET("/user/" + username).Expect(). - Status(iris.StatusOK).Body().Equal(expected) -} - -type testControllerRelPathFromFunc struct{ mvc.Controller } - -func (c *testControllerRelPathFromFunc) EndRequest(ctx context.Context) { - ctx.Writef("%s:%s", ctx.Method(), ctx.Path()) - c.Controller.EndRequest(ctx) -} - -func (c *testControllerRelPathFromFunc) Get() {} -func (c *testControllerRelPathFromFunc) GetBy(int64) {} -func (c *testControllerRelPathFromFunc) GetAnythingByWildcard(string) {} - -func (c *testControllerRelPathFromFunc) GetLogin() {} -func (c *testControllerRelPathFromFunc) PostLogin() {} - -func (c *testControllerRelPathFromFunc) GetAdminLogin() {} - -func (c *testControllerRelPathFromFunc) PutSomethingIntoThis() {} - -func (c *testControllerRelPathFromFunc) GetSomethingBy(bool) {} -func (c *testControllerRelPathFromFunc) GetSomethingByBy(string, int) {} -func (c *testControllerRelPathFromFunc) GetSomethingNewBy(string, int) {} // two input arguments, one By which is the latest word. -func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} // two input arguments - -func TestControllerRelPathFromFunc(t *testing.T) { - app := iris.New() - app.Controller("/", new(testControllerRelPathFromFunc)) - - e := httptest.New(t, app) - e.GET("/").Expect().Status(iris.StatusOK). - Body().Equal("GET:/") - - e.GET("/42").Expect().Status(iris.StatusOK). - Body().Equal("GET:/42") - e.GET("/something/true").Expect().Status(iris.StatusOK). - Body().Equal("GET:/something/true") - e.GET("/something/false").Expect().Status(iris.StatusOK). - Body().Equal("GET:/something/false") - e.GET("/something/truee").Expect().Status(iris.StatusNotFound) - e.GET("/something/falsee").Expect().Status(iris.StatusNotFound) - e.GET("/something/kataras/42").Expect().Status(iris.StatusOK). - Body().Equal("GET:/something/kataras/42") - e.GET("/something/new/kataras/42").Expect().Status(iris.StatusOK). - Body().Equal("GET:/something/new/kataras/42") - e.GET("/something/true/else/this/42").Expect().Status(iris.StatusOK). - Body().Equal("GET:/something/true/else/this/42") - - e.GET("/login").Expect().Status(iris.StatusOK). - Body().Equal("GET:/login") - e.POST("/login").Expect().Status(iris.StatusOK). - Body().Equal("POST:/login") - e.GET("/admin/login").Expect().Status(iris.StatusOK). - Body().Equal("GET:/admin/login") - e.PUT("/something/into/this").Expect().Status(iris.StatusOK). - Body().Equal("PUT:/something/into/this") - e.GET("/42").Expect().Status(iris.StatusOK). - Body().Equal("GET:/42") - e.GET("/anything/here").Expect().Status(iris.StatusOK). - Body().Equal("GET:/anything/here") -} - -type testControllerActivateListener struct { - mvc.Controller - - TitlePointer *testBindType -} - -func (c *testControllerActivateListener) OnActivate(t *activator.TController) { - t.BindValue(&testBindType{ - title: "default title", - }) -} - -func (c *testControllerActivateListener) Get() { - c.Text = c.TitlePointer.title -} - -func TestControllerActivateListener(t *testing.T) { - app := iris.New() - app.Controller("/", new(testControllerActivateListener)) - app.Controller("/manual", new(testControllerActivateListener), &testBindType{ - title: "my title", - }) - - e := httptest.New(t, app) - e.GET("/").Expect().Status(iris.StatusOK). - Body().Equal("default title") - e.GET("/manual").Expect().Status(iris.StatusOK). - Body().Equal("my title") -} diff --git a/mvc/go19.go b/mvc/go19.go deleted file mode 100644 index 7467090fae..0000000000 --- a/mvc/go19.go +++ /dev/null @@ -1,26 +0,0 @@ -// +build go1.9 - -package mvc - -import ( - "html/template" - - "github.com/kataras/iris/mvc/activator" -) - -type ( - // HTML wraps the "s" with the template.HTML - // in order to be marked as safe content, to be rendered as html and not escaped. - HTML = template.HTML - - // TController contains the necessary controller's pre-serve information. - // - // With `TController` the `Controller` can register custom routes - // or modify the provided values that will be binded to the - // controller later on. - // - // Look the `mvc/activator#TController` for its implementation. - // - // A shortcut for the `mvc/activator#TController`, useful when `OnActivate` is being used. - TController = activator.TController -) diff --git a/mvc/method_result.go b/mvc/method_result.go deleted file mode 100644 index 7512ff59be..0000000000 --- a/mvc/method_result.go +++ /dev/null @@ -1,58 +0,0 @@ -package mvc - -import ( - "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc/activator/methodfunc" -) - -// build go1.9 only(go19.go)--> -// // Result is a response dispatcher. -// // All types that complete this interface -// // can be returned as values from the method functions. -// Result = methodfunc.Result -// <-- -// No, let's just copy-paste in order to go 1.8 users have this type -// easy to be used from the root mvc package, -// sometimes duplication doesn't hurt. - -// Result is a response dispatcher. -// All types that complete this interface -// can be returned as values from the method functions. -// -// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview. -type Result interface { // NOTE: Should be always compatible with the methodfunc.Result. - // Dispatch should sends the response to the context's response writer. - Dispatch(ctx context.Context) -} - -var defaultFailureResponse = Response{Code: methodfunc.DefaultErrStatusCode} - -// Try will check if "fn" ran without any panics, -// using recovery, -// and return its result as the final response -// otherwise it returns the "failure" response if any, -// if not then a 400 bad request is being sent. -// -// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go. -func Try(fn func() Result, failure ...Result) Result { - var failed bool - var actionResponse Result - - func() { - defer func() { - if rec := recover(); rec != nil { - failed = true - } - }() - actionResponse = fn() - }() - - if failed { - if len(failure) > 0 { - return failure[0] - } - return defaultFailureResponse - } - - return actionResponse -} diff --git a/mvc/method_result_response.go b/mvc/method_result_response.go deleted file mode 100644 index 4c11cdaabc..0000000000 --- a/mvc/method_result_response.go +++ /dev/null @@ -1,69 +0,0 @@ -package mvc - -import ( - "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc/activator/methodfunc" -) - -// Response completes the `methodfunc.Result` interface. -// It's being used as an alternative return value which -// wraps the status code, the content type, a content as bytes or as string -// and an error, it's smart enough to complete the request and send the correct response to the client. -type Response struct { - Code int - ContentType string - Content []byte - - // if not empty then content type is the text/plain - // and content is the text as []byte. - Text string - // If not nil then it will fire that as "application/json" or the - // "ContentType" if not empty. - Object interface{} - - // If Path is not empty then it will redirect - // the client to this Path, if Code is >= 300 and < 400 - // then it will use that Code to do the redirection, otherwise - // StatusFound(302) or StatusSeeOther(303) for post methods will be used. - // Except when err != nil. - Path string - - // if not empty then fire a 400 bad request error - // unless the Status is > 200, then fire that error code - // with the Err.Error() string as its content. - // - // if Err.Error() is empty then it fires the custom error handler - // if any otherwise the framework sends the default http error text based on the status. - Err error - Try func() int - - // if true then it skips everything else and it throws a 404 not found error. - // Can be named as Failure but NotFound is more precise name in order - // to be visible that it's different than the `Err` - // because it throws a 404 not found instead of a 400 bad request. - // NotFound bool - // let's don't add this yet, it has its dangerous of missuse. -} - -var _ methodfunc.Result = Response{} - -// Dispatch writes the response result to the context's response writer. -func (r Response) Dispatch(ctx context.Context) { - if r.Path != "" && r.Err == nil { - // it's not a redirect valid status - if r.Code < 300 || r.Code >= 400 { - if ctx.Method() == "POST" { - r.Code = 303 // StatusSeeOther - } - r.Code = 302 // StatusFound - } - ctx.Redirect(r.Path, r.Code) - return - } - - if s := r.Text; s != "" { - r.Content = []byte(s) - } - - methodfunc.DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err, true) -} diff --git a/mvc/method_result_test.go b/mvc/method_result_test.go deleted file mode 100644 index b6e2c28eff..0000000000 --- a/mvc/method_result_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package mvc_test - -import ( - "errors" - "testing" - - "github.com/kataras/iris" - "github.com/kataras/iris/context" - "github.com/kataras/iris/httptest" - "github.com/kataras/iris/mvc" -) - -// activator/methodfunc/func_caller.go. -// and activator/methodfunc/func_result_dispatcher.go - -type testControllerMethodResult struct { - mvc.C -} - -func (c *testControllerMethodResult) Get() mvc.Result { - return mvc.Response{ - Text: "Hello World!", - } -} - -func (c *testControllerMethodResult) GetWithStatus() mvc.Response { // or mvc.Result again, no problem. - return mvc.Response{ - Text: "This page doesn't exist", - Code: iris.StatusNotFound, - } -} - -type testCustomStruct struct { - Name string `json:"name" xml:"name"` - Age int `json:"age" xml:"age"` -} - -func (c *testControllerMethodResult) GetJson() mvc.Result { - var err error - if c.Ctx.URLParamExists("err") { - err = errors.New("error here") - } - return mvc.Response{ - Err: err, // if err != nil then it will fire the error's text with a BadRequest. - Object: testCustomStruct{Name: "Iris", Age: 2}, - } -} - -var things = []string{"thing 0", "thing 1", "thing 2"} - -func (c *testControllerMethodResult) GetThingWithTryBy(index int) mvc.Result { - failure := mvc.Response{ - Text: "thing does not exist", - Code: iris.StatusNotFound, - } - - return mvc.Try(func() mvc.Result { - // if panic because of index exceed the slice - // then the "failure" response will be returned instead. - return mvc.Response{Text: things[index]} - }, failure) -} - -func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) mvc.Result { - return mvc.Try(func() mvc.Result { - // if panic because of index exceed the slice - // then the default failure response will be returned instead (400 bad request). - return mvc.Response{Text: things[index]} - }) -} - -func TestControllerMethodResult(t *testing.T) { - app := iris.New() - app.Controller("/", new(testControllerMethodResult)) - - e := httptest.New(t, app) - - e.GET("/").Expect().Status(iris.StatusOK). - Body().Equal("Hello World!") - - e.GET("/with/status").Expect().Status(iris.StatusNotFound). - Body().Equal("This page doesn't exist") - - e.GET("/json").Expect().Status(iris.StatusOK). - JSON().Equal(iris.Map{ - "name": "Iris", - "age": 2, - }) - - e.GET("/json").WithQuery("err", true).Expect(). - Status(iris.StatusBadRequest). - Body().Equal("error here") - - e.GET("/thing/with/try/1").Expect(). - Status(iris.StatusOK). - Body().Equal("thing 1") - // failure because of index exceed the slice - e.GET("/thing/with/try/3").Expect(). - Status(iris.StatusNotFound). - Body().Equal("thing does not exist") - - e.GET("/thing/with/try/default/3").Expect(). - Status(iris.StatusBadRequest). - Body().Equal("Bad Request") -} - -type testControllerMethodResultTypes struct { - mvc.Controller -} - -func (c *testControllerMethodResultTypes) GetText() string { - return "text" -} - -func (c *testControllerMethodResultTypes) GetStatus() int { - return iris.StatusBadGateway -} - -func (c *testControllerMethodResultTypes) GetTextWithStatusOk() (string, int) { - return "OK", iris.StatusOK -} - -// tests should have output arguments mixed -func (c *testControllerMethodResultTypes) GetStatusWithTextNotOkBy(first string, second string) (int, string) { - return iris.StatusForbidden, "NOT_OK_" + first + second -} - -func (c *testControllerMethodResultTypes) GetTextAndContentType() (string, string) { - return "text", "text/html" -} - -type testControllerMethodCustomResult struct { - HTML string -} - -// The only one required function to make that a custom Response dispatcher. -func (r testControllerMethodCustomResult) Dispatch(ctx context.Context) { - ctx.HTML(r.HTML) -} - -func (c *testControllerMethodResultTypes) GetCustomResponse() testControllerMethodCustomResult { - return testControllerMethodCustomResult{"text"} -} - -func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) { - return testControllerMethodCustomResult{"OK"}, iris.StatusOK -} - -func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) { - return testControllerMethodCustomResult{"internal server error"}, iris.StatusInternalServerError -} - -func (c *testControllerMethodResultTypes) GetCustomStruct() testCustomStruct { - return testCustomStruct{"Iris", 2} -} - -func (c *testControllerMethodResultTypes) GetCustomStructWithStatusNotOk() (testCustomStruct, int) { - return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError -} - -func (c *testControllerMethodResultTypes) GetCustomStructWithContentType() (testCustomStruct, string) { - return testCustomStruct{"Iris", 2}, "text/xml" -} - -func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCustomStruct, err error) { - s = testCustomStruct{"Iris", 2} - if c.Ctx.URLParamExists("err") { - err = errors.New("omit return of testCustomStruct and fire error") - } - - // it should send the testCustomStruct as JSON if error is nil - // otherwise it should fire the default error(BadRequest) with the error's text. - return -} - -func TestControllerMethodResultTypes(t *testing.T) { - app := iris.New() - app.Controller("/", new(testControllerMethodResultTypes)) - - e := httptest.New(t, app) - - e.GET("/text").Expect().Status(iris.StatusOK). - Body().Equal("text") - - e.GET("/status").Expect().Status(iris.StatusBadGateway) - - e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK). - Body().Equal("OK") - - e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden). - Body().Equal("NOT_OK_firstsecond") - - e.GET("/text/and/content/type").Expect().Status(iris.StatusOK). - ContentType("text/html", "utf-8"). - Body().Equal("text") - - e.GET("/custom/response").Expect().Status(iris.StatusOK). - ContentType("text/html", "utf-8"). - Body().Equal("text") - e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK). - ContentType("text/html", "utf-8"). - Body().Equal("OK") - e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). - ContentType("text/html", "utf-8"). - Body().Equal("internal server error") - - expectedResultFromCustomStruct := map[string]interface{}{ - "name": "Iris", - "age": 2, - } - e.GET("/custom/struct").Expect().Status(iris.StatusOK). - JSON().Equal(expectedResultFromCustomStruct) - e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). - JSON().Equal(expectedResultFromCustomStruct) - e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK). - ContentType("text/xml", "utf-8") - e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK). - JSON().Equal(expectedResultFromCustomStruct) - e.GET("/custom/struct/with/error").WithQuery("err", true).Expect(). - Status(iris.StatusBadRequest). // the default status code if error is not nil - // the content should be not JSON it should be the status code's text - // it will fire the error's text - Body().Equal("omit return of testCustomStruct and fire error") -} - -type testControllerViewResultRespectCtxViewData struct { - T *testing.T - mvc.C -} - -func (t *testControllerViewResultRespectCtxViewData) BeginRequest(ctx context.Context) { - t.C.BeginRequest(ctx) - ctx.ViewData("name_begin", "iris_begin") -} - -func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx context.Context) { - t.C.EndRequest(ctx) - // check if data is not overridden by return mvc.View {Data: context.Map...} - - dataWritten := ctx.GetViewData() - if dataWritten == nil { - t.T.Fatalf("view data is nil, both BeginRequest and Get failed to write the data") - return - } - - if dataWritten["name_begin"] == nil { - t.T.Fatalf(`view data[name_begin] is nil, - BeginRequest's ctx.ViewData call have been overridden by Get's return mvc.View {Data: }. - Total view data: %v`, dataWritten) - } - - if dataWritten["name"] == nil { - t.T.Fatalf("view data[name] is nil, Get's return mvc.View {Data: } didn't work. Total view data: %v", dataWritten) - } -} - -func (t *testControllerViewResultRespectCtxViewData) Get() mvc.Result { - return mvc.View{ - Name: "doesnt_exists.html", - Data: context.Map{"name": "iris"}, // we care about this only. - Code: iris.StatusInternalServerError, - } -} - -func TestControllerViewResultRespectCtxViewData(t *testing.T) { - app := iris.New() - app.Controller("/", new(testControllerViewResultRespectCtxViewData), t) - e := httptest.New(t, app) - - e.GET("/").Expect().Status(iris.StatusInternalServerError) -} diff --git a/mvc/method_result_view.go b/mvc/method_result_view.go deleted file mode 100644 index 59742fb267..0000000000 --- a/mvc/method_result_view.go +++ /dev/null @@ -1,104 +0,0 @@ -package mvc - -import ( - "strings" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc/activator/methodfunc" - - "github.com/fatih/structs" -) - -// View completes the `methodfunc.Result` interface. -// It's being used as an alternative return value which -// wraps the template file name, layout, (any) view data, status code and error. -// It's smart enough to complete the request and send the correct response to the client. -// -// Example at: https://github.com/kataras/iris/blob/master/_examples/mvc/overview/web/controllers/hello_controller.go. -type View struct { - Name string - Layout string - Data interface{} // map or a custom struct. - Code int - Err error -} - -var _ methodfunc.Result = View{} - -const dotB = byte('.') - -// DefaultViewExt is the default extension if `view.Name `is missing, -// but note that it doesn't care about -// the app.RegisterView(iris.$VIEW_ENGINE("./$dir", "$ext"))'s $ext. -// so if you don't use the ".html" as extension for your files -// you have to append the extension manually into the `view.Name` -// or change this global variable. -var DefaultViewExt = ".html" - -func ensureExt(s string) string { - if len(s) == 0 { - return "index" + DefaultViewExt - } - - if strings.IndexByte(s, dotB) < 1 { - s += DefaultViewExt - } - - return s -} - -// Dispatch writes the template filename, template layout and (any) data to the client. -// Completes the `Result` interface. -func (r View) Dispatch(ctx context.Context) { // r as Response view. - if r.Err != nil { - if r.Code < 400 { - r.Code = methodfunc.DefaultErrStatusCode - } - ctx.StatusCode(r.Code) - ctx.WriteString(r.Err.Error()) - ctx.StopExecution() - return - } - - if r.Code > 0 { - ctx.StatusCode(r.Code) - } - - if r.Name != "" { - r.Name = ensureExt(r.Name) - - if r.Layout != "" { - r.Layout = ensureExt(r.Layout) - ctx.ViewLayout(r.Layout) - } - - if r.Data != nil { - // In order to respect any c.Ctx.ViewData that may called manually before; - dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey() - if ctx.Values().Get(dataKey) == nil { - // if no c.Ctx.ViewData then it's empty do a - // pure set, it's faster. - ctx.Values().Set(dataKey, r.Data) - } else { - // else check if r.Data is map or struct, if struct convert it to map, - // do a range loop and set the data one by one. - // context.Map is actually a map[string]interface{} but we have to make that check; - if m, ok := r.Data.(map[string]interface{}); ok { - setViewData(ctx, m) - } else if m, ok := r.Data.(context.Map); ok { - setViewData(ctx, m) - } else if structs.IsStruct(r.Data) { - setViewData(ctx, structs.Map(r)) - } - } - } - - ctx.View(r.Name) - } -} - -func setViewData(ctx context.Context, data map[string]interface{}) { - for k, v := range data { - ctx.ViewData(k, v) - } -} diff --git a/mvc/session_controller.go b/mvc/session_controller.go deleted file mode 100644 index bc403311a3..0000000000 --- a/mvc/session_controller.go +++ /dev/null @@ -1,47 +0,0 @@ -package mvc - -import ( - "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc/activator" - "github.com/kataras/iris/sessions" - - "github.com/kataras/golog" -) - -var defaultManager = sessions.New(sessions.Config{}) - -// SessionController is a simple `Controller` implementation -// which requires a binded session manager in order to give -// direct access to the current client's session via its `Session` field. -type SessionController struct { - C - - Manager *sessions.Sessions - Session *sessions.Session -} - -// OnActivate called, once per application lifecycle NOT request, -// every single time the dev registers a specific SessionController-based controller. -// It makes sure that its "Manager" field is filled -// even if the caller didn't provide any sessions manager via the `app.Controller` function. -func (s *SessionController) OnActivate(t *activator.TController) { - if !t.BindValueTypeExists(defaultManager) { - t.BindValue(defaultManager) - golog.Warnf(`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, -therefore this controller is using the default sessions manager instead. -Please refer to the documentation to learn how you can provide the session manager`) - } -} - -// BeginRequest calls the Controller's BeginRequest -// and tries to initialize the current user's Session. -func (s *SessionController) BeginRequest(ctx context.Context) { - s.C.BeginRequest(ctx) - if s.Manager == nil { - ctx.Application().Logger().Errorf(`MVC SessionController: sessions manager is nil, report this as a bug -because the SessionController should predict this on its activation state and use a default one automatically`) - return - } - - s.Session = s.Manager.Start(ctx) -} diff --git a/mvc/strutil.go b/mvc/strutil.go deleted file mode 100644 index 686da607b0..0000000000 --- a/mvc/strutil.go +++ /dev/null @@ -1,38 +0,0 @@ -package mvc - -import ( - "strings" - "unicode" -) - -func findCtrlWords(ctrlName string) (w []string) { - end := len(ctrlName) - start := -1 - for i, n := 0, end; i < n; i++ { - c := rune(ctrlName[i]) - if unicode.IsUpper(c) { - // it doesn't count the last uppercase - if start != -1 { - end = i - w = append(w, strings.ToLower(ctrlName[start:end])) - } - start = i - continue - } - end = i + 1 - - } - - // We can't omit the last name, we have to take it. - // because of controller names like - // "UserProfile", we need to return "user", "profile" - // if "UserController", we need to return "user" - // if "User", we need to return "user". - last := ctrlName[start:end] - if last == ctrlSuffix { - return - } - - w = append(w, strings.ToLower(last)) - return -} diff --git a/mvc/strutil_test.go b/mvc/strutil_test.go deleted file mode 100644 index 34648ed4b3..0000000000 --- a/mvc/strutil_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package mvc - -import ( - "testing" -) - -func TestFindCtrlWords(t *testing.T) { - var tests = map[string][]string{ - "UserController": {"user"}, - "UserPostController": {"user", "post"}, - "ProfileController": {"profile"}, - "UserProfileController": {"user", "profile"}, - "UserProfilePostController": {"user", "profile", "post"}, - "UserProfile": {"user", "profile"}, - "Profile": {"profile"}, - "User": {"user"}, - } - - for ctrlName, expected := range tests { - words := findCtrlWords(ctrlName) - if len(expected) != len(words) { - t.Fatalf("expected words and return don't have the same length: [%d] != [%d] | '%s' != '%s'", - len(expected), len(words), expected, words) - } - for i, w := range words { - if expected[i] != w { - t.Fatalf("expected word is not equal with the return one: '%s' != '%s'", expected[i], w) - } - } - } -} diff --git a/mvc2/bind.go b/mvc2/bind.go index 5cfb859351..63f29cb195 100644 --- a/mvc2/bind.go +++ b/mvc2/bind.go @@ -1,7 +1,7 @@ package mvc2 import ( - "github.com/kataras/di" + "github.com/kataras/iris/mvc2/di" "reflect" ) diff --git a/mvc2/controller.go b/mvc2/controller.go index 4865644c19..af17b21ee8 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -3,8 +3,9 @@ package mvc2 import ( "fmt" "reflect" + "strings" - "github.com/kataras/di" + "github.com/kataras/iris/mvc2/di" "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" @@ -75,9 +76,9 @@ func (c *C) EndRequest(ctx context.Context) {} // ControllerActivator returns a new controller type info description. // Its functionality can be overriden by the end-dev. type ControllerActivator struct { - // the router is used on the `Activate` and can be used by end-dev on the `OnActivate` + // the router is used on the `Activate` and can be used by end-dev on the `BeforeActivate` // to register any custom controller's functions as handlers but we will need it here - // in order to not create a new type like `ActivationPayload` for the `OnActivate`. + // in order to not create a new type like `ActivationPayload` for the `BeforeActivate`. Router router.Party // initRef BaseController // the BaseController as it's passed from the end-dev. @@ -88,7 +89,7 @@ type ControllerActivator struct { FullName string // the methods names that is already binded to a handler, - // the BeginRequest, EndRequest and OnActivate are reserved by the internal implementation. + // the BeginRequest, EndRequest and BeforeActivate are reserved by the internal implementation. reservedMethods []string // the bindings that comes from the Engine and the controller's filled fields if any. @@ -100,6 +101,16 @@ type ControllerActivator struct { injector *di.StructInjector } +func getNameOf(typ reflect.Type) string { + elemTyp := di.IndirectType(typ) + + typName := elemTyp.Name() + pkgPath := elemTyp.PkgPath() + fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName + + return fullname +} + func newControllerActivator(router router.Party, controller interface{}, d *di.D) *ControllerActivator { var ( val = reflect.ValueOf(controller) @@ -116,7 +127,7 @@ func newControllerActivator(router router.Party, controller interface{}, d *di.D // the end-developer when declaring the controller, // activate listeners needs them in order to know if something set-ed already or not, // look `BindTypeExists`. - d.Values = append(lookupNonZeroFieldsValues(val), d.Values...) + d.Values = append(di.LookupNonZeroFieldsValues(val), d.Values...) c := &ControllerActivator{ // give access to the Router to the end-devs if they need it for some reason, @@ -142,7 +153,7 @@ func newControllerActivator(router router.Party, controller interface{}, d *di.D } func whatReservedMethods(typ reflect.Type) []string { - methods := []string{"OnActivate"} + methods := []string{"BeforeActivate"} if isBaseController(typ) { methods = append(methods, "BeginRequest", "EndRequest") } @@ -182,7 +193,6 @@ func (c *ControllerActivator) parseMethods() { } func (c *ControllerActivator) activate() { - c.injector = c.Dependencies.Struct(c.Value) c.parseMethods() } @@ -233,18 +243,40 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...) // get the function's input arguments' bindings. funcDependencies := c.Dependencies.Clone() - funcDependencies.Add(pathParams...) + funcDependencies.AddValue(pathParams...) funcInjector := funcDependencies.Func(m.Func) - // we will make use of 'n' to make a slice of reflect.Value - // to pass into if the function has input arguments that - // are will being filled by the funcDependencies. - n := len(funcIn) // the element value, not the pointer, wil lbe used to create a // new controller on each incoming request. - elemTyp := indirectTyp(c.Type) - implementsBase := isBaseController(c.Type) + // Remember: + // we cannot simply do that and expect to work: + // hasStructInjector = c.injector != nil && c.injector.Valid + // hasFuncInjector = funcInjector != nil && funcInjector.Valid + // because + // the `Handle` can be called from `BeforeActivate` callbacks + // and before activation, the c.injector is nil because + // we may not have the dependencies binded yet. But if `c.injector.Valid` + // inside the Handelr works because it's set on the `activate()` method. + // To solve this we can make check on the FIRST `Handle`, + // if c.injector is nil, then set it with the current bindings, + // so the user should bind the dependencies needed before the `Handle` + // this is a logical flow, so we will choose that one -> + if c.injector == nil { + c.injector = c.Dependencies.Struct(c.Value) + } + var ( + hasStructInjector = c.injector != nil && c.injector.Valid + hasFuncInjector = funcInjector != nil && funcInjector.Valid + + implementsBase = isBaseController(c.Type) + // we will make use of 'n' to make a slice of reflect.Value + // to pass into if the function has input arguments that + // are will being filled by the funcDependencies. + n = len(funcIn) + + elemTyp = di.IndirectType(c.Type) + ) handler := func(ctx context.Context) { ctrl := reflect.New(elemTyp) @@ -263,11 +295,11 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . defer b.EndRequest(ctx) } - if !c.injector.Valid && !funcInjector.Valid { + if !hasStructInjector && !hasFuncInjector { DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) } else { ctxValue := reflect.ValueOf(ctx) - if c.injector.Valid { + if hasStructInjector { elem := ctrl.Elem() c.injector.InjectElem(elem, ctxValue) if ctx.IsStopped() { @@ -276,13 +308,13 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // we do this in order to reduce in := make... // if not func input binders, we execute the handler with empty input args. - if !funcInjector.Valid { + if !hasFuncInjector { DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) } } // otherwise, it has one or more valid input binders, // make the input and call the func using those. - if funcInjector.Valid { + if hasFuncInjector { in := make([]reflect.Value, n, n) in[0] = ctrl funcInjector.Inject(&in, ctxValue) diff --git a/mvc2/controller_handle_test.go b/mvc2/controller_handle_test.go index 26b77f17d1..4857aeea7d 100644 --- a/mvc2/controller_handle_test.go +++ b/mvc2/controller_handle_test.go @@ -24,12 +24,11 @@ func (c *testControllerHandle) BeginRequest(ctx iris.Context) { c.reqField = ctx.URLParam("reqfield") } -func (c *testControllerHandle) OnActivate(t *ControllerActivator) { // OnActivate(t *mvc.TController) { - // t.Handle("GET", "/", "Get") - t.Handle("GET", "/histatic", "HiStatic") - t.Handle("GET", "/hiservice", "HiService") - t.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") - t.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") +func (c *testControllerHandle) BeforeActivate(ca *ControllerActivator) { // BeforeActivate(t *mvc.TController) { + ca.Handle("GET", "/histatic", "HiStatic") + ca.Handle("GET", "/hiservice", "HiService") + ca.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") + ca.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") } func (c *testControllerHandle) HiStatic() string { @@ -51,8 +50,10 @@ func (c *testControllerHandle) HiParamEmptyInputBy() string { func TestControllerHandle(t *testing.T) { app := iris.New() - m := New() - m.Bind(&TestServiceImpl{prefix: "service:"}).Controller(app, new(testControllerHandle)) + m := NewEngine() + m.Dependencies.Add(&TestServiceImpl{prefix: "service:"}) + m.Controller(app, new(testControllerHandle)) + e := httptest.New(t, app) // test the index, is not part of the current package's implementation but do it. diff --git a/mvc2/controller_test.go b/mvc2/controller_test.go index 785cf13342..9ab274767f 100644 --- a/mvc2/controller_test.go +++ b/mvc2/controller_test.go @@ -63,7 +63,7 @@ func (c *testControllerAny) Any() { func TestControllerMethodFuncs(t *testing.T) { app := iris.New() - m := New() + m := NewEngine() m.Controller(app, new(testController)) m.Controller(app.Party("/all"), new(testControllerAll)) m.Controller(app.Party("/any"), new(testControllerAny)) @@ -113,7 +113,7 @@ func (c *testControllerBeginAndEndRequestFunc) Post() { func TestControllerBeginAndEndRequestFunc(t *testing.T) { app := iris.New() - New().Controller(app.Party("/profile/{username}"), new(testControllerBeginAndEndRequestFunc)) + NewEngine().Controller(app.Party("/profile/{username}"), new(testControllerBeginAndEndRequestFunc)) e := httptest.New(t, app) usernames := []string{ @@ -156,7 +156,7 @@ func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) { ctx.Writef("forbidden") } - New().Controller(app.Party("/profile/{username}", middlewareCheck), + NewEngine().Controller(app.Party("/profile/{username}", middlewareCheck), new(testControllerBeginAndEndRequestFunc)) e := httptest.New(t, app) @@ -230,7 +230,7 @@ func (c *testControllerEndRequestAwareness) EndRequest(ctx context.Context) { func TestControllerEndRequestAwareness(t *testing.T) { app := iris.New() - New().Controller(app.Party("/era/{username}"), new(testControllerEndRequestAwareness)) + NewEngine().Controller(app.Party("/era/{username}"), new(testControllerEndRequestAwareness)) e := httptest.New(t, app) usernames := []string{ @@ -284,8 +284,8 @@ func TestControllerBind(t *testing.T) { myTitlePtr := &testBindType{title: t1} // test bind value to value of the correct type myTitleV := testBindType{title: t2} - m := New() - m.Bind(myTitlePtr, myTitleV) + m := NewEngine() + m.Dependencies.Add(myTitlePtr, myTitleV) // or just app m.Controller(app.Party("/"), new(testControllerBindStruct)) m.Controller(app.Party("/deep"), new(testControllerBindDeep)) @@ -345,8 +345,9 @@ func TestControllerInsideControllerRecursively(t *testing.T) { ) app := iris.New() - New().Bind(&testBindType{title: title}). - Controller(app.Party("/user/{username}"), new(testCtrl0)) + m := NewEngine() + m.Dependencies.Add(&testBindType{title: title}) + m.Controller(app.Party("/user/{username}"), new(testCtrl0)) e := httptest.New(t, app) e.GET("/user/" + username).Expect(). @@ -378,7 +379,7 @@ func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} / func TestControllerRelPathFromFunc(t *testing.T) { app := iris.New() - New().Controller(app, new(testControllerRelPathFromFunc)) + NewEngine().Controller(app, new(testControllerRelPathFromFunc)) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK). @@ -420,12 +421,8 @@ type testControllerActivateListener struct { TitlePointer *testBindType } -func (c *testControllerActivateListener) OnActivate(ca *ControllerActivator) { - if !ca.Dependencies.BindExists(&testBindType{}) { - ca.Dependencies.Bind(&testBindType{ - title: "default title", - }) - } +func (c *testControllerActivateListener) BeforeActivate(ca *ControllerActivator) { + ca.Dependencies.AddOnce(&testBindType{title: "default title"}) } func (c *testControllerActivateListener) Get() string { @@ -434,12 +431,14 @@ func (c *testControllerActivateListener) Get() string { func TestControllerActivateListener(t *testing.T) { app := iris.New() - New().Controller(app, new(testControllerActivateListener)) - New().Bind(&testBindType{ // will bind to all controllers under this .New() MVC Engine. + NewEngine().Controller(app, new(testControllerActivateListener)) + m := NewEngine() + m.Dependencies.Add(&testBindType{ // will bind to all controllers under this .New() MVC Engine. title: "my title", - }).Controller(app.Party("/manual"), new(testControllerActivateListener)) + }) + m.Controller(app.Party("/manual"), new(testControllerActivateListener)) // or - New().Controller(app.Party("/manual2"), &testControllerActivateListener{ + NewEngine().Controller(app.Party("/manual2"), &testControllerActivateListener{ TitlePointer: &testBindType{ title: "my title", }, diff --git a/mvc2/di/di.go b/mvc2/di/di.go new file mode 100644 index 0000000000..d56bed8604 --- /dev/null +++ b/mvc2/di/di.go @@ -0,0 +1,92 @@ +package di + +import "reflect" + +type ( + // Hijacker is a type which is used to catch fields or function's input argument + // to bind a custom object based on their type. + Hijacker func(reflect.Type) (*BindObject, bool) + // TypeChecker checks if a specific field's or function input argument's + // is valid to be binded. + TypeChecker func(reflect.Type) bool +) + +// D is the Dependency Injection container, +// it contains the Values that can be changed before the injectors. +// `Struct` and the `Func` methods returns an injector for specific +// struct instance-value or function. +type D struct { + Values + + hijacker Hijacker + goodFunc TypeChecker +} + +// New creates and returns a new Dependency Injection container. +// See `Values` field and `Func` and `Struct` methods for more. +func New() *D { + return &D{} +} + +// Hijack sets a hijacker function, read the `Hijacker` type for more explaination. +func (d *D) Hijack(fn Hijacker) *D { + d.hijacker = fn + return d +} + +// GoodFunc sets a type checker for a valid function that can be binded, +// read the `TypeChecker` type for more explaination. +func (d *D) GoodFunc(fn TypeChecker) *D { + d.goodFunc = fn + return d +} + +// Clone returns a new Dependency Injection container, it adopts the +// parent's (current "D") hijacker, good func type checker and all dependencies values. +func (d *D) Clone() *D { + clone := New() + clone.hijacker = d.hijacker + clone.goodFunc = d.goodFunc + + // copy the current dynamic bindings (func binders) + // and static struct bindings (services) to this new child. + if n := len(d.Values); n > 0 { + values := make(Values, n, n) + copy(values, d.Values) + clone.Values = values + } + + return clone +} + +// Struct is being used to return a new injector based on +// a struct value instance, if it contains fields that the types of those +// are matching with one or more of the `Values` then they are binded +// with the injector's `Inject` and `InjectElem` methods. +func (d *D) Struct(s interface{}) *StructInjector { + if s == nil { + return nil + } + v := ValueOf(s) + + return MakeStructInjector( + v, + d.hijacker, + d.goodFunc, + d.Values..., + ) +} + +// Func is being used to return a new injector based on +// a function, if it contains input arguments that the types of those +// are matching with one or more of the `Values` then they are binded +// to the function's input argument when called +// with the injector's `Fill` method. +func (d *D) Func(fn interface{}) *FuncInjector { + return MakeFuncInjector( + ValueOf(fn), + d.hijacker, + d.goodFunc, + d.Values..., + ) +} diff --git a/mvc2/vendor/github.com/kataras/di/func.go b/mvc2/di/func.go similarity index 90% rename from mvc2/vendor/github.com/kataras/di/func.go rename to mvc2/di/func.go index 78ae2b4e01..ef9d76ca20 100644 --- a/mvc2/vendor/github.com/kataras/di/func.go +++ b/mvc2/di/func.go @@ -16,18 +16,21 @@ type ( fn reflect.Value inputs []*targetFuncInput + // Length is the number of the valid, final binded input arguments. Length int - Valid bool // is True when contains func inputs and it's a valid target func. + // Valid is True when `Length` is > 0, it's statically set-ed for + // performance reasons. + Valid bool // } ) func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *FuncInjector { - typ := indirectTyp(fn.Type()) + typ := IndirectType(fn.Type()) s := &FuncInjector{ fn: fn, } - if !isFunc(typ) { + if !IsFunc(typ) { return s } diff --git a/mvc2/vendor/github.com/kataras/di/object.go b/mvc2/di/object.go similarity index 87% rename from mvc2/vendor/github.com/kataras/di/object.go rename to mvc2/di/object.go index eefb95910d..29de55f5b3 100644 --- a/mvc2/vendor/github.com/kataras/di/object.go +++ b/mvc2/di/object.go @@ -5,10 +5,10 @@ import ( "reflect" ) -type bindType uint32 +type BindType uint32 const ( - Static bindType = iota // simple assignable value, a static value. + Static BindType = iota // simple assignable value, a static value. Dynamic // dynamic value, depends on some input arguments from the caller. ) @@ -16,17 +16,12 @@ type BindObject struct { Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' . Value reflect.Value - BindType bindType + BindType BindType ReturnValue func([]reflect.Value) reflect.Value } -// ReturnValueBad can be customized to skip func binders that are not allowed, a custom logic. -var ReturnValueBad = func(reflect.Type) bool { - return false -} - func MakeBindObject(v reflect.Value, goodFunc TypeChecker) (b BindObject, err error) { - if isFunc(v) { + if IsFunc(v) { b.BindType = Dynamic b.ReturnValue, b.Type, err = MakeReturnValue(v, goodFunc) } else { @@ -52,7 +47,7 @@ var errBad = errors.New("bad") // The return type of the "fn" should be a value instance, not a pointer, for your own protection. // The binder function should return only one value. func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Value) reflect.Value, reflect.Type, error) { - typ := indirectTyp(fn.Type()) + typ := IndirectType(fn.Type()) // invalid if not a func. if typ.Kind() != reflect.Func { diff --git a/mvc2/vendor/github.com/kataras/di/reflect.go b/mvc2/di/reflect.go similarity index 83% rename from mvc2/vendor/github.com/kataras/di/reflect.go rename to mvc2/di/reflect.go index f56437f2c3..0c28bf029e 100644 --- a/mvc2/vendor/github.com/kataras/di/reflect.go +++ b/mvc2/di/reflect.go @@ -1,9 +1,6 @@ package di -import ( - "reflect" - "strings" -) +import "reflect" var emptyIn = []reflect.Value{} @@ -57,11 +54,11 @@ func IsZero(v reflect.Value) bool { return v.Interface() == zero.Interface() } -func indirectVal(v reflect.Value) reflect.Value { +func IndirectValue(v reflect.Value) reflect.Value { return reflect.Indirect(v) } -func valueOf(o interface{}) reflect.Value { +func ValueOf(o interface{}) reflect.Value { if v, ok := o.(reflect.Value); ok { return v } @@ -69,7 +66,7 @@ func valueOf(o interface{}) reflect.Value { return reflect.ValueOf(o) } -func indirectTyp(typ reflect.Type) reflect.Type { +func IndirectType(typ reflect.Type) reflect.Type { switch typ.Kind() { case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: return typ.Elem() @@ -88,7 +85,7 @@ func goodVal(v reflect.Value) bool { return v.IsValid() } -func isFunc(kindable interface { +func IsFunc(kindable interface { Kind() reflect.Kind }) bool { return kindable.Kind() == reflect.Func @@ -107,25 +104,6 @@ func equalTypes(got reflect.Type, expected reflect.Type) bool { return false } -func getNameOf(typ reflect.Type) string { - elemTyp := indirectTyp(typ) - - typName := elemTyp.Name() - pkgPath := elemTyp.PkgPath() - fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName - - return fullname -} - -func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { - n := funcTyp.NumIn() - funcIn := make([]reflect.Type, n, n) - for i := 0; i < n; i++ { - funcIn[i] = funcTyp.In(i) - } - return funcIn -} - // for controller's fields only. func structFieldIgnored(f reflect.StructField) bool { if !f.Anonymous { @@ -155,7 +133,7 @@ func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { for i, n := 0, elemTyp.NumField(); i < n; i++ { f := elemTyp.Field(i) - if indirectTyp(f.Type).Kind() == reflect.Struct && + if IndirectType(f.Type).Kind() == reflect.Struct && !structFieldIgnored(f) { fields = append(fields, lookupFields(f.Type, append(parentIndex, i))...) continue @@ -185,9 +163,12 @@ func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { return } -func lookupNonZeroFieldsValues(v reflect.Value) (bindValues []reflect.Value) { - elem := indirectVal(v) - fields := lookupFields(indirectTyp(v.Type()), nil) +// LookupNonZeroFieldsValues lookup for filled fields based on the "v" struct value instance. +// It returns a slice of reflect.Value (same type as `Values`) that can be binded, +// like the end-developer's custom values. +func LookupNonZeroFieldsValues(v reflect.Value) (bindValues []reflect.Value) { + elem := IndirectValue(v) + fields := lookupFields(IndirectType(v.Type()), nil) for _, f := range fields { if fieldVal := elem.FieldByIndex(f.Index); f.Type.Kind() == reflect.Ptr && !IsZero(fieldVal) { diff --git a/mvc2/vendor/github.com/kataras/di/struct.go b/mvc2/di/struct.go similarity index 96% rename from mvc2/vendor/github.com/kataras/di/struct.go rename to mvc2/di/struct.go index 42bb655512..76e177b092 100644 --- a/mvc2/vendor/github.com/kataras/di/struct.go +++ b/mvc2/di/struct.go @@ -18,7 +18,7 @@ type ( func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *StructInjector { s := &StructInjector{ - elemType: indirectTyp(v.Type()), + elemType: IndirectType(v.Type()), } fields := lookupFields(s.elemType, nil) @@ -64,7 +64,7 @@ func (s *StructInjector) Inject(dest interface{}, ctx ...reflect.Value) { return } - v := indirectVal(valueOf(dest)) + v := IndirectValue(ValueOf(dest)) s.InjectElem(v, ctx...) } diff --git a/mvc2/vendor/github.com/kataras/di/values.go b/mvc2/di/values.go similarity index 52% rename from mvc2/vendor/github.com/kataras/di/values.go rename to mvc2/di/values.go index a412e9b496..129fed0c2b 100644 --- a/mvc2/vendor/github.com/kataras/di/values.go +++ b/mvc2/di/values.go @@ -10,41 +10,36 @@ func NewValues() Values { return Values{} } -// Bind binds values to this controller, if you want to share +// Add binds values to this controller, if you want to share // binding values between controllers use the Engine's `Bind` function instead. -func (bv *Values) Bind(values ...interface{}) { +func (bv *Values) Add(values ...interface{}) { for _, val := range values { - bv.bind(reflect.ValueOf(val)) + bv.AddValue(reflect.ValueOf(val)) } } -// Add same as `Bind` but accepts reflect.Value +// AddValue same as `Add` but accepts reflect.Value // instead. -func (bv *Values) Add(values ...reflect.Value) { +func (bv *Values) AddValue(values ...reflect.Value) { for _, v := range values { - bv.bind(v) - } -} - -func (bv *Values) bind(v reflect.Value) { - if !goodVal(v) { - return + if !goodVal(v) { + continue + } + *bv = append(*bv, v) } - - *bv = append(*bv, v) } -// Unbind unbinds a binding value based on the type, +// Remove unbinds a binding value based on the type, // it returns true if at least one field is not binded anymore. // // The "n" indicates the number of elements to remove, if <=0 then it's 1, // this is useful because you may have bind more than one value to two or more fields // with the same type. -func (bv *Values) Unbind(value interface{}, n int) bool { - return bv.unbind(reflect.TypeOf(value), n) +func (bv *Values) Remove(value interface{}, n int) bool { + return bv.remove(reflect.TypeOf(value), n) } -func (bv *Values) unbind(typ reflect.Type, n int) (ok bool) { +func (bv *Values) remove(typ reflect.Type, n int) (ok bool) { input := *bv for i, in := range input { if equalTypes(in.Type(), typ) { @@ -62,13 +57,13 @@ func (bv *Values) unbind(typ reflect.Type, n int) (ok bool) { return } -// BindExists returns true if a binder responsible to +// Has returns true if a binder responsible to // bind and return a type of "typ" is already registered to this controller. -func (bv *Values) BindExists(value interface{}) bool { - return bv.bindTypeExists(reflect.TypeOf(value)) +func (bv *Values) Has(value interface{}) bool { + return bv.valueTypeExists(reflect.TypeOf(value)) } -func (bv *Values) bindTypeExists(typ reflect.Type) bool { +func (bv *Values) valueTypeExists(typ reflect.Type) bool { input := *bv for _, in := range input { if equalTypes(in.Type(), typ) { @@ -78,16 +73,16 @@ func (bv *Values) bindTypeExists(typ reflect.Type) bool { return false } -// BindIfNotExists bind a value to the controller's field with the same type, +// AddOnce binds a value to the controller's field with the same type, // if it's not binded already. // // Returns false if binded already or the value is not the proper one for binding, // otherwise true. -func (bv *Values) BindIfNotExists(value interface{}) bool { - return bv.bindIfNotExists(reflect.ValueOf(value)) +func (bv *Values) AddOnce(value interface{}) bool { + return bv.addIfNotExists(reflect.ValueOf(value)) } -func (bv *Values) bindIfNotExists(v reflect.Value) bool { +func (bv *Values) addIfNotExists(v reflect.Value) bool { var ( typ = v.Type() // no element, raw things here. ) @@ -96,10 +91,10 @@ func (bv *Values) bindIfNotExists(v reflect.Value) bool { return false } - if bv.bindTypeExists(typ) { + if bv.valueTypeExists(typ) { return false } - bv.bind(v) + bv.AddValue(v) return true } diff --git a/mvc2/engine.go b/mvc2/engine.go index 59454752f3..0dc900d6b8 100644 --- a/mvc2/engine.go +++ b/mvc2/engine.go @@ -1,65 +1,52 @@ package mvc2 import ( - "errors" - - "github.com/kataras/di" + "github.com/kataras/iris/mvc2/di" "github.com/kataras/golog" "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" ) -var ( - errNil = errors.New("nil") - errBad = errors.New("bad") - errAlreadyExists = errors.New("already exists") -) - type Engine struct { - dependencies *di.D + Dependencies *di.D } -func New() *Engine { +func NewEngine() *Engine { return &Engine{ - dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker), + Dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker), } } -func (e *Engine) Bind(values ...interface{}) *Engine { - e.dependencies.Bind(values...) - return e -} - -func (e *Engine) Child() *Engine { - child := New() - child.dependencies = e.dependencies.Clone() +func (e *Engine) Clone() *Engine { + child := NewEngine() + child.Dependencies = e.Dependencies.Clone() return child } func (e *Engine) Handler(handler interface{}) context.Handler { - h, err := MakeHandler(handler, e.dependencies.Values...) + h, err := MakeHandler(handler, e.Dependencies.Values...) if err != nil { golog.Errorf("mvc handler: %v", err) } return h } -func (e *Engine) Controller(router router.Party, controller interface{}, onActivate ...func(*ControllerActivator)) { - ca := newControllerActivator(router, controller, e.dependencies) +func (e *Engine) Controller(router router.Party, controller interface{}, beforeActivate ...func(*ControllerActivator)) { + ca := newControllerActivator(router, controller, e.Dependencies) - // give a priority to the "onActivate" + // give a priority to the "beforeActivate" // callbacks, if any. - for _, cb := range onActivate { + for _, cb := range beforeActivate { cb(ca) } - // check if controller has an "OnActivate" function + // check if controller has an "BeforeActivate" function // which accepts the controller activator and call it. if activateListener, ok := controller.(interface { - OnActivate(*ControllerActivator) + BeforeActivate(*ControllerActivator) }); ok { - activateListener.OnActivate(ca) + activateListener.BeforeActivate(ca) } ca.activate() diff --git a/mvc2/engine_handler_test.go b/mvc2/engine_handler_test.go index 547de0c914..9b4cb417b7 100644 --- a/mvc2/engine_handler_test.go +++ b/mvc2/engine_handler_test.go @@ -9,7 +9,8 @@ import ( ) func TestMvcEngineInAndHandler(t *testing.T) { - m := New().Bind(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) + m := NewEngine() + m.Dependencies.Add(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) var ( h1 = m.Handler(testConsumeUserHandler) diff --git a/mvc2/func_result.go b/mvc2/func_result.go index d03319495c..47a7030db3 100644 --- a/mvc2/func_result.go +++ b/mvc2/func_result.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/fatih/structs" + "github.com/kataras/iris/mvc2/di" + "github.com/kataras/iris/context" ) @@ -405,7 +407,7 @@ func (r View) Dispatch(ctx context.Context) { // r as Response view. setViewData(ctx, m) } else if m, ok := r.Data.(context.Map); ok { setViewData(ctx, m) - } else if indirectVal(reflect.ValueOf(r.Data)).Kind() == reflect.Struct { + } else if di.IndirectValue(reflect.ValueOf(r.Data)).Kind() == reflect.Struct { setViewData(ctx, structs.Map(r)) } } diff --git a/mvc2/func_result_test.go b/mvc2/func_result_test.go index 240a044ba6..92be4d55d3 100644 --- a/mvc2/func_result_test.go +++ b/mvc2/func_result_test.go @@ -71,7 +71,7 @@ func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) Result func TestControllerMethodResult(t *testing.T) { app := iris.New() - New().Controller(app, new(testControllerMethodResult)) + NewEngine().Controller(app, new(testControllerMethodResult)) e := httptest.New(t, app) @@ -175,7 +175,7 @@ func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCust func TestControllerMethodResultTypes(t *testing.T) { app := iris.New() - New().Controller(app, new(testControllerMethodResultTypes)) + NewEngine().Controller(app, new(testControllerMethodResultTypes)) e := httptest.New(t, app, httptest.LogLevel("debug")) @@ -266,8 +266,8 @@ func (t *testControllerViewResultRespectCtxViewData) Get() Result { func TestControllerViewResultRespectCtxViewData(t *testing.T) { app := iris.New() - New().Controller(app, new(testControllerViewResultRespectCtxViewData), func(ca *ControllerActivator) { - ca.Dependencies.Bind(t) + NewEngine().Controller(app, new(testControllerViewResultRespectCtxViewData), func(ca *ControllerActivator) { + ca.Dependencies.Add(t) }) e := httptest.New(t, app) diff --git a/mvc2/handler.go b/mvc2/handler.go index 994186779a..1213732bf2 100644 --- a/mvc2/handler.go +++ b/mvc2/handler.go @@ -2,7 +2,7 @@ package mvc2 import ( "fmt" - "github.com/kataras/di" + "github.com/kataras/iris/mvc2/di" "reflect" "runtime" @@ -23,7 +23,7 @@ func isContextHandler(handler interface{}) (context.Handler, bool) { } func validateHandler(handler interface{}) error { - if typ := reflect.TypeOf(handler); !isFunc(typ) { + if typ := reflect.TypeOf(handler); !di.IsFunc(typ) { return fmt.Errorf("handler expected to be a kind of func but got typeof(%s)", typ.String()) } return nil diff --git a/mvc2/ideas/1/main.go b/mvc2/ideas/1/main.go new file mode 100644 index 0000000000..a63aeda6de --- /dev/null +++ b/mvc2/ideas/1/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + + "github.com/kataras/iris" + "github.com/kataras/iris/sessions" + + mvc "github.com/kataras/iris/mvc2" +) + +func main() { + app := iris.New() + mvc.New(app.Party("/todo")).Configure(TodoApp) + // no let's have a clear "mvc" package without any conversions and type aliases, + // it's one extra import path for a whole new world, it worths it. + // + // app.UseMVC(app.Party("/todo")).Configure(func(app *iris.MVCApplication)) + + app.Run(iris.Addr(":8080")) +} + +func TodoApp(app *mvc.Application) { + // You can use normal middlewares at MVC apps of course. + app.Router.Use(func(ctx iris.Context) { + ctx.Application().Logger().Infof("Path: %s", ctx.Path()) + ctx.Next() + }) + + // Add dependencies which will be binding to the controller(s), + // can be either a function which accepts an iris.Context and returns a single value (dynamic binding) + // or a static struct value (service). + app.AddDependencies( + mvc.Session(sessions.New(sessions.Config{})), + &prefixedLogger{prefix: "DEV"}, + ) + + app.Register(new(TodoController)) + + // All dependencies of the parent *mvc.Application + // are cloned to that new child, thefore it has access to the same session as well. + app.NewChild(app.Router.Party("/sub")). + Register(new(TodoSubController)) +} + +// If controller's fields (or even its functions) expecting an interface +// but a struct value is binded then it will check if that struct value implements +// the interface and if true then it will bind it as expected. + +type LoggerService interface { + Log(string) +} + +type prefixedLogger struct { + prefix string +} + +func (s *prefixedLogger) Log(msg string) { + fmt.Printf("%s: %s\n", s.prefix, msg) +} + +type TodoController struct { + Logger LoggerService + + Session *sessions.Session +} + +func (c *TodoController) Get() string { + count := c.Session.Increment("count", 1) + + body := fmt.Sprintf("Hello from TodoController\nTotal visits from you: %d", count) + c.Logger.Log(body) + return body +} + +type TodoSubController struct { + Session *sessions.Session +} + +func (c *TodoSubController) Get() string { + count, _ := c.Session.GetIntDefault("count", 1) + return fmt.Sprintf("Hello from TodoSubController.\nRead-only visits count: %d", count) +} diff --git a/mvc2/mvc.go b/mvc2/mvc.go new file mode 100644 index 0000000000..c1a7e04bb8 --- /dev/null +++ b/mvc2/mvc.go @@ -0,0 +1,90 @@ +package mvc2 + +import "github.com/kataras/iris/core/router" + +// Application is the high-level compoment of the "mvc" package. +// It's the API that you will be using to register controllers among wih their +// dependencies that your controllers may expecting. +// It contains the Router(iris.Party) in order to be able to register +// template layout, middleware, done handlers as you used with the +// standard Iris APIBuilder. +// +// The Engine is created by the `New` method and it's the dependencies holder +// and controllers factory. +// +// See `mvc#New` for more. +type Application struct { + Engine *Engine + Router router.Party +} + +func newApp(engine *Engine, subRouter router.Party) *Application { + return &Application{ + Engine: engine, + Router: subRouter, + } +} + +// New returns a new mvc Application based on a "subRouter". +// Application creates a new engine which is responsible for binding the dependencies +// and creating and activating the app's controller(s). +// +// Example: `New(app.Party("/todo"))`. +func New(subRouter router.Party) *Application { + return newApp(NewEngine(), subRouter) +} + +// Configure can be used to pass one or more functions that accept this +// Application, use this to add dependencies and controller(s). +// +// Example: `New(app.Party("/todo")).Configure(func(mvcApp *mvc.Application){...})`. +func (app *Application) Configure(configurators ...func(*Application)) *Application { + for _, c := range configurators { + c(app) + } + return app +} + +// AddDependencies adds one or more values as dependencies. +// The value can be a single struct value-instance or a function +// which has one input and one output, the input should be +// an `iris.Context` and the output can be any type, that output type +// will be binded to the controller's field, if matching or to the +// controller's methods, if matching. +// +// The dependencies can be changed per-controller as well via a `beforeActivate` +// on the `Register` method or when the controller has the `BeforeActivate(c *ControllerActivator)` +// method defined. +// +// It returns this Application. +// +// Example: `.AddDependencies(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`. +func (app *Application) AddDependencies(values ...interface{}) *Application { + app.Engine.Dependencies.Add(values...) + return app +} + +// Register adds a controller for the current Router. +// It accept any custom struct which its functions will be transformed +// to routes. +// +// The second, optional and variadic argument is the "beforeActive", +// use that when you want to modify the controller before the activation +// and registration to the main Iris Application. +// +// It returns this Application. +// +// Example: `.Register(new(TodoController))`. +func (app *Application) Register(controller interface{}, beforeActivate ...func(*ControllerActivator)) *Application { + app.Engine.Controller(app.Router, controller, beforeActivate...) + return app +} + +// NewChild creates and returns a new Application which will be adapted +// to the "subRouter", it adopts +// the dependencies bindings from the parent(current) one. +// +// Example: `.NewChild(irisApp.Party("/sub")).Register(new(TodoSubController))`. +func (app *Application) NewChild(subRouter router.Party) *Application { + return newApp(app.Engine.Clone(), subRouter) +} diff --git a/mvc2/path_param_binder_test.go b/mvc2/path_param_binder_test.go index 582a1ee22c..e03555b2b3 100644 --- a/mvc2/path_param_binder_test.go +++ b/mvc2/path_param_binder_test.go @@ -7,7 +7,8 @@ import ( ) func TestPathParamsBinder(t *testing.T) { - m := New().Bind(PathParamsBinder) + m := NewEngine() + m.Dependencies.Add(PathParamsBinder) got := "" @@ -25,7 +26,8 @@ func TestPathParamsBinder(t *testing.T) { } } func TestPathParamBinder(t *testing.T) { - m := New().Bind(PathParamBinder("username")) + m := NewEngine() + m.Dependencies.Add(PathParamBinder("username")) got := "" executed := false diff --git a/mvc2/reflect.go b/mvc2/reflect.go index 09d4b47d58..e18ece737e 100644 --- a/mvc2/reflect.go +++ b/mvc2/reflect.go @@ -2,10 +2,8 @@ package mvc2 import ( "reflect" - "strings" "github.com/kataras/iris/context" - "github.com/kataras/pkg/zerocheck" ) var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem() @@ -20,58 +18,6 @@ func isContext(inTyp reflect.Type) bool { return inTyp.Implements(contextTyp) } -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(kindable interface { - Kind() reflect.Kind -}) bool { - return kindable.Kind() == reflect.Func -} - -func equalTypes(got reflect.Type, expected reflect.Type) bool { - if got == expected { - return true - } - // if accepts an interface, check if the given "got" type does - // implement this "expected" user handler's input argument. - if expected.Kind() == reflect.Interface { - // fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String()) - return got.Implements(expected) - } - return false -} - -func getNameOf(typ reflect.Type) string { - elemTyp := indirectTyp(typ) - - typName := elemTyp.Name() - pkgPath := elemTyp.PkgPath() - fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName - - return fullname -} - func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { n := funcTyp.NumIn() funcIn := make([]reflect.Type, n, n) @@ -80,72 +26,3 @@ func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { } return funcIn } - -// for controller's fields only. -func structFieldIgnored(f reflect.StructField) bool { - if !f.Anonymous { - return true // if not anonymous(embedded), ignore it. - } - - s := f.Tag.Get("ignore") - return s == "true" // if has an ignore tag then ignore it. -} - -type field struct { - Type reflect.Type - Index []int // the index of the field, slice if it's part of a embedded struct - Name string // the actual name - - // this could be empty, but in our cases it's not, - // it's filled with the bind object (as service which means as static value) - // and it's filled from the lookupFields' caller. - AnyValue reflect.Value -} - -func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { - if elemTyp.Kind() != reflect.Struct { - return - } - - for i, n := 0, elemTyp.NumField(); i < n; i++ { - f := elemTyp.Field(i) - - if f.PkgPath != "" { - continue // skip unexported. - } - - if indirectTyp(f.Type).Kind() == reflect.Struct && - !structFieldIgnored(f) { - fields = append(fields, lookupFields(f.Type, append(parentIndex, i))...) - continue - } - - index := []int{i} - if len(parentIndex) > 0 { - index = append(parentIndex, i) - } - - field := field{ - Type: f.Type, - Name: f.Name, - Index: index, - } - - fields = append(fields, field) - } - - return -} - -func lookupNonZeroFieldsValues(v reflect.Value) (bindValues []reflect.Value) { - elem := indirectVal(v) - fields := lookupFields(indirectTyp(v.Type()), nil) - for _, f := range fields { - - if fieldVal := elem.FieldByIndex(f.Index); f.Type.Kind() == reflect.Ptr && !zerocheck.IsZero(fieldVal) { - bindValues = append(bindValues, fieldVal) - } - } - - return -} diff --git a/mvc2/session_binder.go b/mvc2/session.go similarity index 100% rename from mvc2/session_binder.go rename to mvc2/session.go diff --git a/mvc2/session_controller.go b/mvc2/session_controller.go index 8b7e815af4..ab6dd39455 100644 --- a/mvc2/session_controller.go +++ b/mvc2/session_controller.go @@ -17,12 +17,12 @@ type SessionController struct { Session *sessions.Session } -// OnActivate called, once per application lifecycle NOT request, +// BeforeActivate called, once per application lifecycle NOT request, // every single time the dev registers a specific SessionController-based controller. // It makes sure that its "Manager" field is filled // even if the caller didn't provide any sessions manager via the `app.Controller` function. -func (s *SessionController) OnActivate(ca *ControllerActivator) { - if didntBindManually := ca.Dependencies.BindIfNotExists(defaultSessionManager); didntBindManually { +func (s *SessionController) BeforeActivate(ca *ControllerActivator) { + if didntBindManually := ca.Dependencies.AddOnce(defaultSessionManager); didntBindManually { ca.Router.GetReporter().Add( `MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, therefore this controller is using the default sessions manager instead. diff --git a/mvc2/vendor/github.com/kataras/di/README.md b/mvc2/vendor/github.com/kataras/di/README.md deleted file mode 100644 index cc27898703..0000000000 --- a/mvc2/vendor/github.com/kataras/di/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# UNFINISHED - -# BSD-3 License - Gerasimos Maropoulos. - -# INSIDE vendor folder - -Because it's not pushed to the https://github.com/kataras, yet. \ No newline at end of file diff --git a/mvc2/vendor/github.com/kataras/di/_examples/basic/main.go b/mvc2/vendor/github.com/kataras/di/_examples/basic/main.go deleted file mode 100644 index be75bb0acb..0000000000 --- a/mvc2/vendor/github.com/kataras/di/_examples/basic/main.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/kataras/di" -) - -type User struct { - Username string -} - -type Service interface { - save(user User) error -} - -type testService struct { - Name string -} - -func (s *testService) save(user User) error { - fmt.Printf("Saving user '%s' from %s Service\n", user.Username, s.Name) - return nil -} - -func myHandler(service Service) { - service.save(User{ - Username: "func: test username 2", - }) -} - -type userManager struct { - Service Service - - other string -} - -func (m *userManager) updateUsername(username string) { - m.Service.save(User{ - Username: "struct: " + username, - }) -} - -func main() { - // build state, when performance is not critical. - d := di.New() - d.Bind(&testService{"test service"}) - - structInjector := d.Struct(&userManager{}) - funcInjector := d.Func(myHandler) - - // - // at "serve-time", when performance is critical, this DI Binder works very fast. - // - // --- for struct's fields ---- - myManager := new(userManager) - structInjector.Inject(myManager) - myManager.updateUsername("test username 1") - - // --- for function's input ---- - funcInjector.Call() -} diff --git a/mvc2/vendor/github.com/kataras/di/di.go b/mvc2/vendor/github.com/kataras/di/di.go deleted file mode 100644 index 89752231eb..0000000000 --- a/mvc2/vendor/github.com/kataras/di/di.go +++ /dev/null @@ -1,68 +0,0 @@ -package di - -import "reflect" - -type ( - Hijacker func(reflect.Type) (*BindObject, bool) - TypeChecker func(reflect.Type) bool -) - -type D struct { - Values - - hijacker Hijacker - goodFunc TypeChecker -} - -func New() *D { - return &D{} -} - -func (d *D) Hijack(fn Hijacker) *D { - d.hijacker = fn - return d -} - -func (d *D) GoodFunc(fn TypeChecker) *D { - d.goodFunc = fn - return d -} - -func (d *D) Clone() *D { - clone := New() - clone.hijacker = d.hijacker - clone.goodFunc = d.goodFunc - - // copy the current dynamic bindings (func binders) - // and static struct bindings (services) to this new child. - if n := len(d.Values); n > 0 { - values := make(Values, n, n) - copy(values, d.Values) - clone.Values = values - } - - return clone -} - -func (d *D) Struct(s interface{}) *StructInjector { - if s == nil { - return nil - } - v := valueOf(s) - - return MakeStructInjector( - v, - d.hijacker, - d.goodFunc, - d.Values..., - ) -} - -func (d *D) Func(fn interface{}) *FuncInjector { - return MakeFuncInjector( - valueOf(fn), - d.hijacker, - d.goodFunc, - d.Values..., - ) -} diff --git a/mvc2/vendor/github.com/kataras/di/doc.go b/mvc2/vendor/github.com/kataras/di/doc.go deleted file mode 100644 index 4c56dd3ac5..0000000000 --- a/mvc2/vendor/github.com/kataras/di/doc.go +++ /dev/null @@ -1 +0,0 @@ -package di \ No newline at end of file diff --git a/sessions/session.go b/sessions/session.go index 3d9d46c87b..266f788139 100644 --- a/sessions/session.go +++ b/sessions/session.go @@ -167,6 +167,26 @@ func (s *Session) GetIntDefault(key string, defaultValue int) (int, error) { return defaultValue, errFindParse.Format("int", key, v) } +// Increment increments the stored int value saved as "key" by +"n". +// If value doesn't exist on that "key" then it creates one with the "n" as its value. +// It returns the new, incremented, value. +func (s *Session) Increment(key string, n int) (newValue int) { + newValue, _ = s.GetIntDefault(key, 0) + newValue += n + s.Set(key, newValue) + return +} + +// Decrement decrements the stored int value saved as "key" by -"n". +// If value doesn't exist on that "key" then it creates one with the "n" as its value. +// It returns the new, decremented, value even if it's less than zero. +func (s *Session) Decrement(key string, n int) (newValue int) { + newValue, _ = s.GetIntDefault(key, 0) + newValue -= n + s.Set(key, newValue) + return +} + // GetInt64 same as `Get` but returns its int64 representation, // if key doesn't exist then it returns -1. func (s *Session) GetInt64(key string) (int64, error) { From e28ed48086232fcc933d21adf8e02bf12bc5c57b Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 16 Dec 2017 06:38:28 +0200 Subject: [PATCH 21/79] OK, my dream-idea is implemented. TODO: Some examples and doc.go is not updated yet, comments on the mvc/di subpackage, the tutorial/vuejs-todo-mvc is running but not finished yet (it's using browser's localstorage and it should be replaced by the http requests that are registered via iris mvc --- .../iris-mvc/controllers/values_controller.go | 13 +-- _benchmarks/iris-mvc/main.go | 13 ++- .../controllers/values_controller.go | 25 ----- _benchmarks/iris-mvc2/main.go | 17 --- _benchmarks/iris/main.go | 7 +- _examples/mvc/hello-world/main.go | 59 ++++++---- _examples/mvc/overview/main.go | 44 +++++--- .../web/controllers/hello_controller.go | 6 +- .../web/controllers/movie_controller.go | 13 +-- _examples/tutorial/vuejs-todo-mvc/README.md | 1 + .../src/web/controllers/todo_controller.go | 7 +- .../tutorial/vuejs-todo-mvc/src/web/main.go | 15 ++- mvc/AUTHORS | 4 + mvc/LICENSE | 27 +++++ {mvc2 => mvc}/README.md | 0 {mvc2 => mvc}/controller.go | 66 ++--------- {mvc2 => mvc}/controller_handle_test.go | 25 +++-- {mvc2 => mvc}/controller_method_parser.go | 60 ++++++---- {mvc2 => mvc}/controller_test.go | 58 +++++----- {mvc2 => mvc}/di/di.go | 0 {mvc2 => mvc}/di/func.go | 11 +- {mvc2 => mvc}/di/object.go | 0 {mvc2 => mvc}/di/reflect.go | 0 {mvc2 => mvc}/di/struct.go | 0 {mvc2 => mvc}/di/values.go | 4 +- mvc/engine.go | 105 ++++++++++++++++++ {mvc2 => mvc}/engine_handler_test.go | 4 +- {mvc2 => mvc}/func_result.go | 4 +- {mvc2 => mvc}/func_result_test.go | 12 +- {mvc2 => mvc}/handler.go | 8 +- {mvc2 => mvc}/handler_test.go | 5 +- {mvc2 => mvc}/ideas/1/main.go | 2 +- {mvc2 => mvc}/mvc.go | 2 +- mvc2/path_param_binder.go => mvc/param.go | 42 ++++--- .../param_test.go | 2 +- mvc/reflect.go | 54 +++++++++ {mvc2 => mvc}/session.go | 2 +- {mvc2 => mvc}/session_controller.go | 11 +- mvc2/bind.go | 34 ------ mvc2/engine.go | 53 --------- mvc2/reflect.go | 28 ----- 41 files changed, 436 insertions(+), 407 deletions(-) delete mode 100644 _benchmarks/iris-mvc2/controllers/values_controller.go delete mode 100644 _benchmarks/iris-mvc2/main.go create mode 100644 _examples/tutorial/vuejs-todo-mvc/README.md create mode 100644 mvc/AUTHORS create mode 100644 mvc/LICENSE rename {mvc2 => mvc}/README.md (100%) rename {mvc2 => mvc}/controller.go (80%) rename {mvc2 => mvc}/controller_handle_test.go (93%) rename {mvc2 => mvc}/controller_method_parser.go (79%) rename {mvc2 => mvc}/controller_test.go (93%) rename {mvc2 => mvc}/di/di.go (100%) rename {mvc2 => mvc}/di/func.go (93%) rename {mvc2 => mvc}/di/object.go (100%) rename {mvc2 => mvc}/di/reflect.go (100%) rename {mvc2 => mvc}/di/struct.go (100%) rename {mvc2 => mvc}/di/values.go (99%) create mode 100644 mvc/engine.go rename {mvc2 => mvc}/engine_handler_test.go (88%) rename {mvc2 => mvc}/func_result.go (99%) rename {mvc2 => mvc}/func_result_test.go (98%) rename {mvc2 => mvc}/handler.go (92%) rename {mvc2 => mvc}/handler_test.go (98%) rename {mvc2 => mvc}/ideas/1/main.go (98%) rename {mvc2 => mvc}/mvc.go (99%) rename mvc2/path_param_binder.go => mvc/param.go (68%) rename mvc2/path_param_binder_test.go => mvc/param_test.go (99%) create mode 100644 mvc/reflect.go rename {mvc2 => mvc}/session.go (97%) rename {mvc2 => mvc}/session_controller.go (89%) delete mode 100644 mvc2/bind.go delete mode 100644 mvc2/engine.go delete mode 100644 mvc2/reflect.go diff --git a/_benchmarks/iris-mvc/controllers/values_controller.go b/_benchmarks/iris-mvc/controllers/values_controller.go index c560a6413a..3fe3a089ac 100644 --- a/_benchmarks/iris-mvc/controllers/values_controller.go +++ b/_benchmarks/iris-mvc/controllers/values_controller.go @@ -1,19 +1,8 @@ package controllers -import "github.com/kataras/iris/mvc" - // ValuesController is the equivalent // `ValuesController` of the .net core 2.0 mvc application. -type ValuesController struct { - mvc.C -} - -/* on windows tests(older) the Get was: -func (vc *ValuesController) Get() { - // id,_ := vc.Params.GetInt("id") - // vc.Ctx.WriteString("value") -} -but as Iris is always going better, now supports return values as well*/ +type ValuesController struct{} // Get handles "GET" requests to "api/values/{id}". func (vc *ValuesController) Get() string { diff --git a/_benchmarks/iris-mvc/main.go b/_benchmarks/iris-mvc/main.go index 92e03b593b..5b9ca83718 100644 --- a/_benchmarks/iris-mvc/main.go +++ b/_benchmarks/iris-mvc/main.go @@ -1,15 +1,18 @@ package main +/// TODO: remove this on the "master" branch, or even replace it +// with the "iris-mvc" (the new implementatioin is even faster, close to handlers version, +// with bindings or without). + import ( + "github.com/kataras/iris/_benchmarks/iris-mvc2/controllers" + "github.com/kataras/iris" - "github.com/kataras/iris/_benchmarks/iris-mvc/controllers" + "github.com/kataras/iris/mvc" ) func main() { app := iris.New() - app.Controller("/api/values/{id}", new(controllers.ValuesController)) - - // 24 August 2017: Iris has a built'n version updater but we don't need it - // when benchmarking... + mvc.New(app.Party("/api/values/{id}")).Register(new(controllers.ValuesController)) app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) } diff --git a/_benchmarks/iris-mvc2/controllers/values_controller.go b/_benchmarks/iris-mvc2/controllers/values_controller.go deleted file mode 100644 index 80ac5ba7b7..0000000000 --- a/_benchmarks/iris-mvc2/controllers/values_controller.go +++ /dev/null @@ -1,25 +0,0 @@ -package controllers - -// import "github.com/kataras/iris/mvc2" - -// ValuesController is the equivalent -// `ValuesController` of the .net core 2.0 mvc application. -type ValuesController struct{} //{ mvc2.C } - -/* on windows tests(older) the Get was: -func (vc *ValuesController) Get() { - // id,_ := vc.Params.GetInt("id") - // vc.Ctx.WriteString("value") -} -but as Iris is always going better, now supports return values as well*/ - -// Get handles "GET" requests to "api/values/{id}". -func (vc *ValuesController) Get() string { - return "value" -} - -// Put handles "PUT" requests to "api/values/{id}". -func (vc *ValuesController) Put() {} - -// Delete handles "DELETE" requests to "api/values/{id}". -func (vc *ValuesController) Delete() {} diff --git a/_benchmarks/iris-mvc2/main.go b/_benchmarks/iris-mvc2/main.go deleted file mode 100644 index 6366ac2c32..0000000000 --- a/_benchmarks/iris-mvc2/main.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -/// TODO: remove this on the "master" branch, or even replace it -// with the "iris-mvc" (the new implementatioin is even faster, close to handlers version, -// with bindings or without). - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/_benchmarks/iris-mvc2/controllers" - "github.com/kataras/iris/mvc2" -) - -func main() { - app := iris.New() - mvc2.New().Controller(app.Party("/api/values/{id}"), new(controllers.ValuesController)) - app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) -} diff --git a/_benchmarks/iris/main.go b/_benchmarks/iris/main.go index ef5d109007..7c1ee6e29d 100644 --- a/_benchmarks/iris/main.go +++ b/_benchmarks/iris/main.go @@ -1,13 +1,10 @@ package main -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/context" -) +import "github.com/kataras/iris" func main() { app := iris.New() - app.Get("/api/values/{id}", func(ctx context.Context) { + app.Get("/api/values/{id}", func(ctx iris.Context) { ctx.WriteString("value") }) diff --git a/_examples/mvc/hello-world/main.go b/_examples/mvc/hello-world/main.go index 5d2c759a51..006c818c08 100644 --- a/_examples/mvc/hello-world/main.go +++ b/_examples/mvc/hello-world/main.go @@ -3,13 +3,6 @@ package main import ( "github.com/kataras/iris" "github.com/kataras/iris/mvc" - // auto-completion does not working well with type aliases - // when embedded fields. - // We should complete a report on golang repo for that at some point. - // - // Therefore import the "mvc" package manually - // here at "hello-world" so users can see that - // import path somewhere else than the "FAQ" section. "github.com/kataras/iris/middleware/logger" "github.com/kataras/iris/middleware/recover" @@ -43,27 +36,18 @@ func main() { app.Use(recover.New()) app.Use(logger.New()) - app.Controller("/", new(ExampleController)) + // Register a controller based on the root Router, "/". + mvc.New(app).Register(new(ExampleController)) // http://localhost:8080 // http://localhost:8080/ping // http://localhost:8080/hello + // http://localhost:8080/custom_path app.Run(iris.Addr(":8080")) } // ExampleController serves the "/", "/ping" and "/hello". -type ExampleController struct { - // if you build with go1.8 you have to use the mvc package always, - // otherwise - // you can, optionally - // use the type alias `iris.C`, - // same for - // context.Context -> iris.Context, - // mvc.Result -> iris.Result, - // mvc.Response -> iris.Response, - // mvc.View -> iris.View - mvc.C -} +type ExampleController struct{} // Get serves // Method: GET @@ -89,6 +73,31 @@ func (c *ExampleController) GetHello() interface{} { return map[string]string{"message": "Hello Iris!"} } +// BeforeActivate called once, before the controller adapted to the main application +// and of course before the server ran. +// After version 9 you can also add custom routes for a specific controller's methods. +// Here you can register custom method's handlers +// use the standard router with `ca.Router` to do something that you can do without mvc as well, +// and add dependencies that will be binded to a controller's fields or method function's input arguments. +func (c *ExampleController) BeforeActivate(ca *mvc.ControllerActivator) { + anyMiddlewareHere := func(ctx iris.Context) { + ctx.Application().Logger().Warnf("Inside /custom_path") + ctx.Next() + } + ca.Handle("GET", "/custom_path", "CustomHandlerWithoutFollowingTheNamingGuide", anyMiddlewareHere) + + // or even add a global middleware based on this controller's router, + // which in this example is the root "/": + // ca.Router.Use(myMiddleware) +} + +// CustomHandlerWithoutFollowingTheNamingGuide serves +// Method: GET +// Resource: http://localhost:8080/custom_path +func (c *ExampleController) CustomHandlerWithoutFollowingTheNamingGuide() string { + return "hello from the custom handler without following the naming guide" +} + // GetUserBy serves // Method: GET // Resource: http://localhost:8080/user/{username:string} @@ -121,4 +130,14 @@ func (c *ExampleController) Trace() {} func (c *ExampleController) All() {} // OR func (c *ExampleController) Any() {} + + + +func (c *ExampleController) BeforeActivate(ca *mvc.ControllerActivator) { + // 1 -> the HTTP Method + // 2 -> the route's path + // 3 -> this controller's method name that should be handler for that route. + ca.Handle("GET", "/mypath/{param}", "DoIt", optionalMiddlewareHere...) +} + */ diff --git a/_examples/mvc/overview/main.go b/_examples/mvc/overview/main.go index 2d0befc568..c0a847132c 100644 --- a/_examples/mvc/overview/main.go +++ b/_examples/mvc/overview/main.go @@ -10,38 +10,52 @@ import ( "github.com/kataras/iris/_examples/mvc/overview/web/middleware" "github.com/kataras/iris" + "github.com/kataras/iris/mvc" ) func main() { app := iris.New() + app.Logger().SetLevel("debug") // Load the template files. app.RegisterView(iris.HTML("./web/views", ".html")) // Register our controllers. - app.Controller("/hello", new(controllers.HelloController)) + mvc.New(app.Party("/hello")).Register(new(controllers.HelloController)) + // You can also split the code you write to configure an mvc.Application + // using the `Configure` method, as shown below. + mvc.New(app.Party("/movies")).Configure(movies) - // Create our movie repository with some (memory) data from the datasource. - repo := repositories.NewMovieRepository(datasource.Movies) - // Create our movie service, we will bind it to the movie controller. - movieService := services.NewMovieService(repo) - - app.Controller("/movies", new(controllers.MovieController), - // Bind the "movieService" to the MovieController's Service (interface) field. - movieService, - // Add the basic authentication(admin:password) middleware - // for the /movies based requests. - middleware.BasicAuth) - - // Start the web server at localhost:8080 // http://localhost:8080/hello // http://localhost:8080/hello/iris // http://localhost:8080/movies // http://localhost:8080/movies/1 app.Run( + // Start the web server at localhost:8080 iris.Addr("localhost:8080"), + // disables updates: iris.WithoutVersionChecker, + // skip err server closed when CTRL/CMD+C pressed: iris.WithoutServerError(iris.ErrServerClosed), - iris.WithOptimizations, // enables faster json serialization and more + // enables faster json serialization and more: + iris.WithOptimizations, ) } + +// note the mvc.Application, it's not iris.Application. +func movies(app *mvc.Application) { + // Add the basic authentication(admin:password) middleware + // for the /movies based requests. + app.Router.Use(middleware.BasicAuth) + + // Create our movie repository with some (memory) data from the datasource. + repo := repositories.NewMovieRepository(datasource.Movies) + // Create our movie service, we will bind it to the movie app's dependencies. + movieService := services.NewMovieService(repo) + app.AddDependencies(movieService) + + // Register our movies controller. + // Note that you can register more than one controller + // you can alos create child mvc apps using the `movies.NewChild()` if you want. + app.Register(new(controllers.MovieController)) +} diff --git a/_examples/mvc/overview/web/controllers/hello_controller.go b/_examples/mvc/overview/web/controllers/hello_controller.go index 56518d5eb4..6f6eb264bf 100644 --- a/_examples/mvc/overview/web/controllers/hello_controller.go +++ b/_examples/mvc/overview/web/controllers/hello_controller.go @@ -10,9 +10,7 @@ import ( // HelloController is our sample controller // it handles GET: /hello and GET: /hello/{name} -type HelloController struct { - mvc.C -} +type HelloController struct{} var helloView = mvc.View{ Name: "hello/index.html", @@ -32,7 +30,7 @@ func (c *HelloController) Get() mvc.Result { return helloView } -// you can define a standard error in order to be re-usable anywhere in your app. +// you can define a standard error in order to re-use anywhere in your app. var errBadName = errors.New("bad name") // you can just return it as error or even better diff --git a/_examples/mvc/overview/web/controllers/movie_controller.go b/_examples/mvc/overview/web/controllers/movie_controller.go index 10542d1a3e..0555bf437e 100644 --- a/_examples/mvc/overview/web/controllers/movie_controller.go +++ b/_examples/mvc/overview/web/controllers/movie_controller.go @@ -9,17 +9,10 @@ import ( "github.com/kataras/iris/_examples/mvc/overview/services" "github.com/kataras/iris" - "github.com/kataras/iris/mvc" ) // MovieController is our /movies controller. type MovieController struct { - // mvc.C is just a lightweight lightweight alternative - // to the "mvc.Controller" controller type, - // use it when you don't need mvc.Controller's fields - // (you don't need those fields when you return values from the method functions). - mvc.C - // Our MovieService, it's an interface which // is binded from the main application. Service services.MovieService @@ -53,9 +46,9 @@ func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) { // PutBy updates a movie. // Demo: // curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MovieController) PutBy(id int64) (datamodels.Movie, error) { +func (c *MovieController) PutBy(ctx iris.Context, id int64) (datamodels.Movie, error) { // get the request data for poster and genre - file, info, err := c.Ctx.FormFile("poster") + file, info, err := ctx.FormFile("poster") if err != nil { return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") } @@ -64,7 +57,7 @@ func (c *MovieController) PutBy(id int64) (datamodels.Movie, error) { // imagine that is the url of the uploaded file... poster := info.Filename - genre := c.Ctx.FormValue("genre") + genre := ctx.FormValue("genre") return c.Service.UpdatePosterAndGenreByID(id, poster, genre) } diff --git a/_examples/tutorial/vuejs-todo-mvc/README.md b/_examples/tutorial/vuejs-todo-mvc/README.md new file mode 100644 index 0000000000..b1ab8cbdce --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/README.md @@ -0,0 +1 @@ +# Unfinished - wait until today :) \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go index ce2b8869d8..5536e0fdae 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -4,8 +4,9 @@ import ( "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo" "github.com/kataras/iris" - mvc "github.com/kataras/iris/mvc2" "github.com/kataras/iris/sessions" + + "github.com/kataras/iris/mvc" ) // TodoController is our TODO app's web controller. @@ -38,10 +39,6 @@ func (c *TodoController) BeforeActivate(ca *mvc.ControllerActivator) { }) // ca.Router.Use(...).Done(...).Layout(...) - // TODO:(?) - // m := ca.Method("PutCompleteBy") - // m.Route.Use(...).Done(...) <- we don't have the route here but I can find something to solve this. - // m.Dependencies.Add(...) } // Get handles the GET: /todo route. diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go index ee3c875e48..dbfaa446b8 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go @@ -5,8 +5,9 @@ import ( "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/web/controllers" "github.com/kataras/iris" - mvc "github.com/kataras/iris/mvc2" "github.com/kataras/iris/sessions" + + "github.com/kataras/iris/mvc" ) func main() { @@ -22,14 +23,16 @@ func main() { Cookie: "_iris_session", }) - m := mvc.New() + m := mvc.New(app.Party("/todo")) - // any bindings here... - m.Bind(mvc.Session(sess)) + // any dependencies bindings here... + m.AddDependencies( + mvc.Session(sess), + new(todo.MemoryService), + ) - m.Bind(new(todo.MemoryService)) // controllers registration here... - m.Controller(app.Party("/todo"), new(controllers.TodoController)) + m.Register(new(controllers.TodoController)) // start the web server at http://localhost:8080 app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker, iris.WithOptimizations) diff --git a/mvc/AUTHORS b/mvc/AUTHORS new file mode 100644 index 0000000000..669cf5d3af --- /dev/null +++ b/mvc/AUTHORS @@ -0,0 +1,4 @@ +# This is the official list of Iris MVC authors for copyright +# purposes. + +Gerasimos Maropoulos diff --git a/mvc/LICENSE b/mvc/LICENSE new file mode 100644 index 0000000000..469fb44d08 --- /dev/null +++ b/mvc/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2018 Gerasimos Maropoulos. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Iris nor the names of its +contributor, Gerasimos Maropoulos, may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/mvc2/README.md b/mvc/README.md similarity index 100% rename from mvc2/README.md rename to mvc/README.md diff --git a/mvc2/controller.go b/mvc/controller.go similarity index 80% rename from mvc2/controller.go rename to mvc/controller.go index af17b21ee8..13556dc81f 100644 --- a/mvc2/controller.go +++ b/mvc/controller.go @@ -1,78 +1,25 @@ -package mvc2 +package mvc import ( "fmt" "reflect" "strings" - "github.com/kataras/iris/mvc2/di" + "github.com/kataras/iris/mvc/di" "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" "github.com/kataras/iris/core/router/macro" ) -// BaseController is the controller interface, -// which the main request `C` will implement automatically. -// End-dev doesn't need to have any knowledge of this if she/he doesn't want to implement -// a new Controller type. -// Controller looks the whole flow as one handler, so `ctx.Next` -// inside `BeginRequest` is not be respected. -// Alternative way to check if a middleware was procceed successfully -// and called its `ctx.Next` is the `ctx.Proceed(handler) bool`. -// You have to navigate to the `context/context#Proceed` function's documentation. +// BaseController is the optional controller interface, if it's +// completed by the end controller then the BeginRequest and EndRequest +// are called between the controller's method responsible for the incoming request. type BaseController interface { BeginRequest(context.Context) EndRequest(context.Context) } -// C is the basic BaseController type that can be used as an embedded anonymous field -// to custom end-dev controllers. -// -// func(c *ExampleController) Get() string | -// (string, string) | -// (string, int) | -// int | -// (int, string | -// (string, error) | -// bool | -// (any, bool) | -// error | -// (int, error) | -// (customStruct, error) | -// customStruct | -// (customStruct, int) | -// (customStruct, string) | -// Result or (Result, error) -// where Get is an HTTP Method func. -// -// Look `core/router#APIBuilder#Controller` method too. -// -// It completes the `activator.BaseController` interface. -// -// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview/web/controllers. -// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go#L17. -type C struct { - // The current context.Context. - // - // we have to name it for two reasons: - // 1: can't ignore these via reflection, it doesn't give an option to - // see if the functions is derived from another type. - // 2: end-developer may want to use some method functions - // or any fields that could be conflict with the context's. - Ctx context.Context -} - -var _ BaseController = &C{} - -// BeginRequest does nothing anymore, is here to complet ethe `BaseController` interface. -// BaseController is not required anymore, `Ctx` is binded automatically by the engine's -// wrapped Handler. -func (c *C) BeginRequest(ctx context.Context) {} - -// EndRequest does nothing, is here to complete the `BaseController` interface. -func (c *C) EndRequest(ctx context.Context) {} - // ControllerActivator returns a new controller type info description. // Its functionality can be overriden by the end-dev. type ControllerActivator struct { @@ -244,7 +191,10 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // get the function's input arguments' bindings. funcDependencies := c.Dependencies.Clone() funcDependencies.AddValue(pathParams...) + + // fmt.Printf("for %s | values: %s\n", funcName, funcDependencies.Values) funcInjector := funcDependencies.Func(m.Func) + // fmt.Printf("actual injector's inputs length: %d\n", funcInjector.Length) // the element value, not the pointer, wil lbe used to create a // new controller on each incoming request. diff --git a/mvc2/controller_handle_test.go b/mvc/controller_handle_test.go similarity index 93% rename from mvc2/controller_handle_test.go rename to mvc/controller_handle_test.go index 4857aeea7d..9be5dd29f6 100644 --- a/mvc2/controller_handle_test.go +++ b/mvc/controller_handle_test.go @@ -1,34 +1,37 @@ -package mvc2_test +package mvc_test import ( "testing" "github.com/kataras/iris" + "github.com/kataras/iris/context" "github.com/kataras/iris/httptest" - . "github.com/kataras/iris/mvc2" + + . "github.com/kataras/iris/mvc" ) type testControllerHandle struct { - C + Ctx context.Context Service TestService reqField string } -func (c *testControllerHandle) Get() string { - return "index" +func (c *testControllerHandle) BeforeActivate(ca *ControllerActivator) { // BeforeActivate(t *mvc.TController) { + ca.Handle("GET", "/histatic", "HiStatic") + ca.Handle("GET", "/hiservice", "HiService") + ca.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") + ca.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") } func (c *testControllerHandle) BeginRequest(ctx iris.Context) { - c.C.BeginRequest(ctx) c.reqField = ctx.URLParam("reqfield") } -func (c *testControllerHandle) BeforeActivate(ca *ControllerActivator) { // BeforeActivate(t *mvc.TController) { - ca.Handle("GET", "/histatic", "HiStatic") - ca.Handle("GET", "/hiservice", "HiService") - ca.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") - ca.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") +func (c *testControllerHandle) EndRequest(ctx iris.Context) {} + +func (c *testControllerHandle) Get() string { + return "index" } func (c *testControllerHandle) HiStatic() string { diff --git a/mvc2/controller_method_parser.go b/mvc/controller_method_parser.go similarity index 79% rename from mvc2/controller_method_parser.go rename to mvc/controller_method_parser.go index 63d7a67174..c5d0ec508c 100644 --- a/mvc2/controller_method_parser.go +++ b/mvc/controller_method_parser.go @@ -1,4 +1,4 @@ -package mvc2 +package mvc import ( "errors" @@ -96,20 +96,31 @@ func (l *methodLexer) peekPrev() (w string) { } var posWords = map[int]string{ - 0: "", - 1: "first", - 2: "second", - 3: "third", - 4: "forth", - 5: "five", - 6: "sixth", - 7: "seventh", - 8: "eighth", - 9: "ninth", + 0: "", + 1: "first", + 2: "second", + 3: "third", + 4: "forth", + 5: "five", + 6: "sixth", + 7: "seventh", + 8: "eighth", + 9: "ninth", + 10: "tenth", + 11: "eleventh", + 12: "twelfth", + 13: "thirteenth", + 14: "fourteenth", + 15: "fifteenth", + 16: "sixteenth", + 17: "seventeenth", + 18: "eighteenth", + 19: "nineteenth", + 20: "twentieth", } func genParamKey(argIdx int) string { - return "param" + posWords[argIdx] // paramfirst, paramsecond... + return "arg" + posWords[argIdx] // argfirst, argsecond... } type methodParser struct { @@ -176,7 +187,7 @@ func (p *methodParser) parse() (method, path string, err error) { // continue // } - if path, err = p.parsePathParam(path, w, funcArgPos); err != nil { + if path, funcArgPos, err = p.parsePathParam(path, w, funcArgPos); err != nil { return "", "", err } @@ -184,24 +195,22 @@ func (p *methodParser) parse() (method, path string, err error) { } // static path. path += "/" + strings.ToLower(w) - } - return } -func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (string, error) { +func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (string, int, error) { typ := p.fn.Type if typ.NumIn() <= funcArgPos { // By found but input arguments are not there, so act like /by path without restricts. path += "/" + strings.ToLower(w) - return path, nil + return path, funcArgPos, nil } var ( - paramKey = genParamKey(funcArgPos) // paramfirst, paramsecond... + paramKey = genParamKey(funcArgPos) // argfirst, argsecond... paramType = ast.ParamTypeString // default string ) @@ -216,10 +225,19 @@ func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (st // it's not wildcard, so check base on our available macro types. paramType = pType } else { - return "", errors.New("invalid syntax for " + p.fn.Name) + if typ.NumIn() > funcArgPos { + // has more input arguments but we are not in the correct + // index now, maybe the first argument was an `iris/context.Context` + // so retry with the "funcArgPos" incremented. + // + // the "funcArgPos" will be updated to the caller as well + // because we return it as well. + return p.parsePathParam(path, w, funcArgPos+1) + } + return "", 0, errors.New("invalid syntax for " + p.fn.Name) } - // /{paramfirst:path}, /{paramfirst:long}... + // /{argfirst:path}, /{argfirst:long}... path += fmt.Sprintf("/{%s:%s}", paramKey, paramType.String()) if nextWord == "" && typ.NumIn() > funcArgPos+1 { @@ -232,5 +250,5 @@ func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (st return p.parsePathParam(path, nextWord, funcArgPos+1) } - return path, nil + return path, funcArgPos, nil } diff --git a/mvc2/controller_test.go b/mvc/controller_test.go similarity index 93% rename from mvc2/controller_test.go rename to mvc/controller_test.go index 9ab274767f..b7609690db 100644 --- a/mvc2/controller_test.go +++ b/mvc/controller_test.go @@ -1,5 +1,5 @@ // black-box testing -package mvc2_test +package mvc_test import ( "testing" @@ -8,56 +8,57 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" "github.com/kataras/iris/httptest" - . "github.com/kataras/iris/mvc2" + + . "github.com/kataras/iris/mvc" ) type testController struct { - C + Ctx context.Context } -var writeMethod = func(c C) { - c.Ctx.Writef(c.Ctx.Method()) +var writeMethod = func(ctx context.Context) { + ctx.Writef(ctx.Method()) } func (c *testController) Get() { - writeMethod(c.C) + writeMethod(c.Ctx) } func (c *testController) Post() { - writeMethod(c.C) + writeMethod(c.Ctx) } func (c *testController) Put() { - writeMethod(c.C) + writeMethod(c.Ctx) } func (c *testController) Delete() { - writeMethod(c.C) + writeMethod(c.Ctx) } func (c *testController) Connect() { - writeMethod(c.C) + writeMethod(c.Ctx) } func (c *testController) Head() { - writeMethod(c.C) + writeMethod(c.Ctx) } func (c *testController) Patch() { - writeMethod(c.C) + writeMethod(c.Ctx) } func (c *testController) Options() { - writeMethod(c.C) + writeMethod(c.Ctx) } func (c *testController) Trace() { - writeMethod(c.C) + writeMethod(c.Ctx) } type ( - testControllerAll struct{ C } - testControllerAny struct{ C } // exactly the same as All. + testControllerAll struct{ Ctx context.Context } + testControllerAny struct{ Ctx context.Context } // exactly the same as All. ) func (c *testControllerAll) All() { - writeMethod(c.C) + writeMethod(c.Ctx) } func (c *testControllerAny) Any() { - writeMethod(c.C) + writeMethod(c.Ctx) } func TestControllerMethodFuncs(t *testing.T) { @@ -83,7 +84,7 @@ func TestControllerMethodFuncs(t *testing.T) { } type testControllerBeginAndEndRequestFunc struct { - C + Ctx context.Context Username string } @@ -93,14 +94,12 @@ type testControllerBeginAndEndRequestFunc struct { // useful when more than one methods using the // same request values or context's function calls. func (c *testControllerBeginAndEndRequestFunc) BeginRequest(ctx context.Context) { - c.C.BeginRequest(ctx) c.Username = ctx.Params().Get("username") } // called after every method (Get() or Post()). func (c *testControllerBeginAndEndRequestFunc) EndRequest(ctx context.Context) { ctx.Writef("done") // append "done" to the response - c.C.EndRequest(ctx) } func (c *testControllerBeginAndEndRequestFunc) Get() { @@ -187,7 +186,7 @@ type Model struct { } type testControllerEndRequestAwareness struct { - C + Ctx context.Context } func (c *testControllerEndRequestAwareness) Get() { @@ -223,9 +222,9 @@ func writeModels(ctx context.Context, names ...string) { } } +func (c *testControllerEndRequestAwareness) BeginRequest(ctx context.Context) {} func (c *testControllerEndRequestAwareness) EndRequest(ctx context.Context) { writeModels(ctx, "TestModel", "myModel") - c.C.EndRequest(ctx) } func TestControllerEndRequestAwareness(t *testing.T) { @@ -249,7 +248,8 @@ type testBindType struct { } type testControllerBindStruct struct { - C + Ctx context.Context + // should start with upper letter of course TitlePointer *testBindType // should have the value of the "myTitlePtr" on test TitleValue testBindType // should have the value of the "myTitleV" on test @@ -320,6 +320,8 @@ func (c *testCtrl0) EndRequest(ctx context.Context) { } type testCtrl00 struct { + Ctx context.Context + testCtrl000 } @@ -330,9 +332,9 @@ type testCtrl000 struct { } type testCtrl0000 struct { - C } +func (c *testCtrl0000) BeginRequest(ctx context.Context) {} func (c *testCtrl0000) EndRequest(ctx context.Context) { ctx.Writef("finish") } @@ -354,11 +356,11 @@ func TestControllerInsideControllerRecursively(t *testing.T) { Status(iris.StatusOK).Body().Equal(expected) } -type testControllerRelPathFromFunc struct{ C } +type testControllerRelPathFromFunc struct{} +func (c *testControllerRelPathFromFunc) BeginRequest(ctx context.Context) {} func (c *testControllerRelPathFromFunc) EndRequest(ctx context.Context) { ctx.Writef("%s:%s", ctx.Method(), ctx.Path()) - c.C.EndRequest(ctx) } func (c *testControllerRelPathFromFunc) Get() {} @@ -416,8 +418,6 @@ func TestControllerRelPathFromFunc(t *testing.T) { } type testControllerActivateListener struct { - C - TitlePointer *testBindType } diff --git a/mvc2/di/di.go b/mvc/di/di.go similarity index 100% rename from mvc2/di/di.go rename to mvc/di/di.go diff --git a/mvc2/di/func.go b/mvc/di/func.go similarity index 93% rename from mvc2/di/func.go rename to mvc/di/func.go index ef9d76ca20..be2064323a 100644 --- a/mvc2/di/func.go +++ b/mvc/di/func.go @@ -1,8 +1,6 @@ package di -import ( - "reflect" -) +import "reflect" type ( targetFuncInput struct { @@ -50,7 +48,6 @@ func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, v InputIndex: i, Object: b, }) - continue } } @@ -69,6 +66,7 @@ func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, v } if b.IsAssignable(inTyp) { + // println(inTyp.String() + " is assignable to " + val.Type().String()) // fmt.Printf("binded input index: %d for type: %s and value: %v with pointer: %v\n", // i, b.Type.String(), val.String(), val.Pointer()) s.inputs = append(s.inputs, &targetFuncInput{ @@ -82,8 +80,9 @@ func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, v } } - s.Length = n - s.Valid = len(s.inputs) > 0 + // s.Length = n + s.Length = len(s.inputs) + s.Valid = s.Length > 0 return s } diff --git a/mvc2/di/object.go b/mvc/di/object.go similarity index 100% rename from mvc2/di/object.go rename to mvc/di/object.go diff --git a/mvc2/di/reflect.go b/mvc/di/reflect.go similarity index 100% rename from mvc2/di/reflect.go rename to mvc/di/reflect.go diff --git a/mvc2/di/struct.go b/mvc/di/struct.go similarity index 100% rename from mvc2/di/struct.go rename to mvc/di/struct.go diff --git a/mvc2/di/values.go b/mvc/di/values.go similarity index 99% rename from mvc2/di/values.go rename to mvc/di/values.go index 129fed0c2b..64301aee3f 100644 --- a/mvc2/di/values.go +++ b/mvc/di/values.go @@ -1,8 +1,6 @@ package di -import ( - "reflect" -) +import "reflect" type Values []reflect.Value diff --git a/mvc/engine.go b/mvc/engine.go new file mode 100644 index 0000000000..e835fa4a4c --- /dev/null +++ b/mvc/engine.go @@ -0,0 +1,105 @@ +package mvc + +import ( + "github.com/kataras/golog" + "github.com/kataras/iris/mvc/di" + + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router" +) + +// Engine contains the Dependencies which will be binded +// to the controller(s) or handler(s) that can be created +// using the Engine's `Handler` and `Controller` methods. +// +// This is not exported for being used by everyone, use it only when you want +// to share engines between multi mvc.go#Application +// or make custom mvc handlers that can be used on the standard +// iris' APIBuilder. The last one reason is the most useful here, +// although end-devs can use the `MakeHandler` as well. +// +// For a more high-level structure please take a look at the "mvc.go#Application". +type Engine struct { + Dependencies *di.D +} + +// NewEngine returns a new engine, a container for dependencies and a factory +// for handlers and controllers, this is used internally by the `mvc#Application` structure. +// Please take a look at the structure's documentation for more information. +func NewEngine() *Engine { + return &Engine{ + Dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker), + } +} + +// Clone creates and returns a new engine with the parent's(current) Dependencies. +// It copies the current "e" dependencies and returns a new engine. +func (e *Engine) Clone() *Engine { + child := NewEngine() + child.Dependencies = e.Dependencies.Clone() + return child +} + +// Handler accepts a "handler" function which can accept any input arguments that match +// with the Engine's `Dependencies` and any output result; like string, int (string,int), +// custom structs, Result(View | Response) and anything you already know that mvc implementation supports. +// It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application, +// as middleware or as simple route handler or subdomain's handler. +func (e *Engine) Handler(handler interface{}) context.Handler { + h, err := MakeHandler(handler, e.Dependencies.Values...) + if err != nil { + golog.Errorf("mvc handler: %v", err) + } + return h +} + +// Controller accepts a sub router and registers any custom struct +// as controller, if struct doesn't have any compatible methods +// neither are registered via `ControllerActivator`'s `Handle` method +// then the controller is not registered at all. +// +// A Controller may have one or more methods +// that are wrapped to a handler and registered as routes before the server ran. +// The controller's method can accept any input argument that are previously binded +// via the dependencies or route's path accepts dynamic path parameters. +// The controller's fields are also bindable via the dependencies, either a +// static value (service) or a function (dynamically) which accepts a context +// and returns a single value (this type is being used to find the relative field or method's input argument). +// +// func(c *ExampleController) Get() string | +// (string, string) | +// (string, int) | +// int | +// (int, string | +// (string, error) | +// bool | +// (any, bool) | +// error | +// (int, error) | +// (customStruct, error) | +// customStruct | +// (customStruct, int) | +// (customStruct, string) | +// Result or (Result, error) +// where Get is an HTTP Method func. +// +// Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc. +func (e *Engine) Controller(router router.Party, controller interface{}, beforeActivate ...func(*ControllerActivator)) { + ca := newControllerActivator(router, controller, e.Dependencies) + + // give a priority to the "beforeActivate" + // callbacks, if any. + for _, cb := range beforeActivate { + cb(ca) + } + + // check if controller has an "BeforeActivate" function + // which accepts the controller activator and call it. + if activateListener, ok := controller.(interface { + BeforeActivate(*ControllerActivator) + }); ok { + activateListener.BeforeActivate(ca) + } + + ca.activate() +} diff --git a/mvc2/engine_handler_test.go b/mvc/engine_handler_test.go similarity index 88% rename from mvc2/engine_handler_test.go rename to mvc/engine_handler_test.go index 9b4cb417b7..a2a01e30bc 100644 --- a/mvc2/engine_handler_test.go +++ b/mvc/engine_handler_test.go @@ -1,11 +1,11 @@ -package mvc2_test +package mvc_test // black-box in combination with the handler_test import ( "testing" - . "github.com/kataras/iris/mvc2" + . "github.com/kataras/iris/mvc" ) func TestMvcEngineInAndHandler(t *testing.T) { diff --git a/mvc2/func_result.go b/mvc/func_result.go similarity index 99% rename from mvc2/func_result.go rename to mvc/func_result.go index 47a7030db3..9b64c03333 100644 --- a/mvc2/func_result.go +++ b/mvc/func_result.go @@ -1,11 +1,11 @@ -package mvc2 +package mvc import ( "reflect" "strings" "github.com/fatih/structs" - "github.com/kataras/iris/mvc2/di" + "github.com/kataras/iris/mvc/di" "github.com/kataras/iris/context" ) diff --git a/mvc2/func_result_test.go b/mvc/func_result_test.go similarity index 98% rename from mvc2/func_result_test.go rename to mvc/func_result_test.go index 92be4d55d3..7b9b04a7c1 100644 --- a/mvc2/func_result_test.go +++ b/mvc/func_result_test.go @@ -1,4 +1,4 @@ -package mvc2_test +package mvc_test import ( "errors" @@ -7,14 +7,15 @@ import ( "github.com/kataras/iris" "github.com/kataras/iris/context" "github.com/kataras/iris/httptest" - . "github.com/kataras/iris/mvc2" + + . "github.com/kataras/iris/mvc" ) // activator/methodfunc/func_caller.go. // and activator/methodfunc/func_result_dispatcher.go type testControllerMethodResult struct { - C + Ctx context.Context } func (c *testControllerMethodResult) Get() Result { @@ -105,7 +106,7 @@ func TestControllerMethodResult(t *testing.T) { } type testControllerMethodResultTypes struct { - C + Ctx context.Context } func (c *testControllerMethodResultTypes) GetText() string { @@ -227,16 +228,13 @@ func TestControllerMethodResultTypes(t *testing.T) { type testControllerViewResultRespectCtxViewData struct { T *testing.T - C } func (t *testControllerViewResultRespectCtxViewData) BeginRequest(ctx context.Context) { - t.C.BeginRequest(ctx) ctx.ViewData("name_begin", "iris_begin") } func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx context.Context) { - t.C.EndRequest(ctx) // check if data is not overridden by return View {Data: context.Map...} dataWritten := ctx.GetViewData() diff --git a/mvc2/handler.go b/mvc/handler.go similarity index 92% rename from mvc2/handler.go rename to mvc/handler.go index 1213732bf2..0df5fb42c6 100644 --- a/mvc2/handler.go +++ b/mvc/handler.go @@ -1,8 +1,8 @@ -package mvc2 +package mvc import ( "fmt" - "github.com/kataras/iris/mvc2/di" + "github.com/kataras/iris/mvc/di" "reflect" "runtime" @@ -39,8 +39,8 @@ func MustMakeHandler(handler interface{}, bindValues ...reflect.Value) context.H return h } -// MakeHandler accepts a "handler" function which can accept any input that matches -// with the "binders" and any output, that matches the mvc types, like string, int (string,int), +// MakeHandler accepts a "handler" function which can accept any input arguments that match +// with the "bindValues" types and any output result, that matches the mvc types, like string, int (string,int), // custom structs, Result(View | Response) and anything that you already know that mvc implementation supports, // and returns a low-level `context/iris.Handler` which can be used anywhere in the Iris Application, // as middleware or as simple route handler or party handler or subdomain handler-router. diff --git a/mvc2/handler_test.go b/mvc/handler_test.go similarity index 98% rename from mvc2/handler_test.go rename to mvc/handler_test.go index 205b1602a5..fb22ea24f1 100644 --- a/mvc2/handler_test.go +++ b/mvc/handler_test.go @@ -1,4 +1,4 @@ -package mvc2_test +package mvc_test // black-box @@ -9,7 +9,8 @@ import ( "github.com/kataras/iris" "github.com/kataras/iris/httptest" - . "github.com/kataras/iris/mvc2" + + . "github.com/kataras/iris/mvc" ) // dynamic func diff --git a/mvc2/ideas/1/main.go b/mvc/ideas/1/main.go similarity index 98% rename from mvc2/ideas/1/main.go rename to mvc/ideas/1/main.go index a63aeda6de..1de61ec982 100644 --- a/mvc2/ideas/1/main.go +++ b/mvc/ideas/1/main.go @@ -6,7 +6,7 @@ import ( "github.com/kataras/iris" "github.com/kataras/iris/sessions" - mvc "github.com/kataras/iris/mvc2" + "github.com/kataras/iris/mvc" ) func main() { diff --git a/mvc2/mvc.go b/mvc/mvc.go similarity index 99% rename from mvc2/mvc.go rename to mvc/mvc.go index c1a7e04bb8..98241d571e 100644 --- a/mvc2/mvc.go +++ b/mvc/mvc.go @@ -1,4 +1,4 @@ -package mvc2 +package mvc import "github.com/kataras/iris/core/router" diff --git a/mvc2/path_param_binder.go b/mvc/param.go similarity index 68% rename from mvc2/path_param_binder.go rename to mvc/param.go index 1018cfb799..4596afca52 100644 --- a/mvc2/path_param_binder.go +++ b/mvc/param.go @@ -1,4 +1,4 @@ -package mvc2 +package mvc import ( "reflect" @@ -16,20 +16,36 @@ func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) return } - funcInIdx := 0 - // it's a valid param type. - for _, p := range params { - in := funcIn[funcInIdx] - paramType := p.Type - paramName := p.Name - // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) - if paramType.Assignable(in.Kind()) { - // fmt.Printf("path_param_binder.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) - values = append(values, makeFuncParamGetter(paramType, paramName)) + consumedParams := make(map[int]bool, 0) + for _, in := range funcIn { + for j, p := range params { + if _, consumed := consumedParams[j]; consumed { + continue + } + paramType := p.Type + paramName := p.Name + // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) + if paramType.Assignable(in.Kind()) { + consumedParams[j] = true + // fmt.Printf("path_param_binder.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) + values = append(values, makeFuncParamGetter(paramType, paramName)) + } } - - funcInIdx++ } + // funcInIdx := 0 + // // it's a valid param type. + // for _, p := range params { + // in := funcIn[funcInIdx] + // paramType := p.Type + // paramName := p.Name + // // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) + // if paramType.Assignable(in.Kind()) { + // // fmt.Printf("path_param_binder.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) + // values = append(values, makeFuncParamGetter(paramType, paramName)) + // } + + // funcInIdx++ + // } return } diff --git a/mvc2/path_param_binder_test.go b/mvc/param_test.go similarity index 99% rename from mvc2/path_param_binder_test.go rename to mvc/param_test.go index e03555b2b3..f9c63329bd 100644 --- a/mvc2/path_param_binder_test.go +++ b/mvc/param_test.go @@ -1,4 +1,4 @@ -package mvc2 +package mvc import ( "testing" diff --git a/mvc/reflect.go b/mvc/reflect.go new file mode 100644 index 0000000000..c28f87fcf7 --- /dev/null +++ b/mvc/reflect.go @@ -0,0 +1,54 @@ +package mvc + +import ( + "reflect" + + "github.com/kataras/iris/context" + "github.com/kataras/iris/mvc/di" +) + +var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem() + +func isBaseController(ctrlTyp reflect.Type) bool { + return ctrlTyp.Implements(baseControllerTyp) +} + +var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem() + +func isContext(inTyp reflect.Type) bool { + return inTyp.Implements(contextTyp) +} + +func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { + n := funcTyp.NumIn() + funcIn := make([]reflect.Type, n, n) + for i := 0; i < n; i++ { + funcIn[i] = funcTyp.In(i) + } + return funcIn +} + +var ( + typeChecker = func(fn reflect.Type) bool { + // valid if that single input arg is a typeof context.Context. + return fn.NumIn() == 1 && isContext(fn.In(0)) + } + + hijacker = func(fieldOrFuncInput reflect.Type) (*di.BindObject, bool) { + if !isContext(fieldOrFuncInput) { + return nil, false + } + + // this is being used on both func injector and struct injector. + // if the func's input argument or the struct's field is a type of Context + // then we can do a fast binding using the ctxValue + // which is used as slice of reflect.Value, because of the final method's `Call`. + return &di.BindObject{ + Type: contextTyp, + BindType: di.Dynamic, + ReturnValue: func(ctxValue []reflect.Value) reflect.Value { + return ctxValue[0] + }, + }, true + } +) diff --git a/mvc2/session.go b/mvc/session.go similarity index 97% rename from mvc2/session.go rename to mvc/session.go index 173a3c8ca1..b09c764fd2 100644 --- a/mvc2/session.go +++ b/mvc/session.go @@ -1,4 +1,4 @@ -package mvc2 +package mvc import ( "github.com/kataras/iris/context" diff --git a/mvc2/session_controller.go b/mvc/session_controller.go similarity index 89% rename from mvc2/session_controller.go rename to mvc/session_controller.go index ab6dd39455..39bc9b0aa0 100644 --- a/mvc2/session_controller.go +++ b/mvc/session_controller.go @@ -1,4 +1,4 @@ -package mvc2 +package mvc import ( "github.com/kataras/iris/context" @@ -11,8 +11,6 @@ var defaultSessionManager = sessions.New(sessions.Config{}) // which requires a binded session manager in order to give // direct access to the current client's session via its `Session` field. type SessionController struct { - C - Manager *sessions.Sessions Session *sessions.Session } @@ -30,10 +28,8 @@ func (s *SessionController) BeforeActivate(ca *ControllerActivator) { } } -// BeginRequest calls the Controller's BeginRequest -// and tries to initialize the current user's Session. +// BeginRequest initializes the current user's Session. func (s *SessionController) BeginRequest(ctx context.Context) { - s.C.BeginRequest(ctx) if s.Manager == nil { ctx.Application().Logger().Errorf(`MVC SessionController: sessions manager is nil, report this as a bug because the SessionController should predict this on its activation state and use a default one automatically`) @@ -42,3 +38,6 @@ because the SessionController should predict this on its activation state and us s.Session = s.Manager.Start(ctx) } + +// EndRequest is here to complete the `BaseController`. +func (s *SessionController) EndRequest(ctx context.Context) {} diff --git a/mvc2/bind.go b/mvc2/bind.go deleted file mode 100644 index 63f29cb195..0000000000 --- a/mvc2/bind.go +++ /dev/null @@ -1,34 +0,0 @@ -package mvc2 - -import ( - "github.com/kataras/iris/mvc2/di" - "reflect" -) - -var ( - typeChecker = func(fn reflect.Type) bool { - // invalid if that single input arg is not a typeof context.Context. - return isContext(fn.In(0)) - } - - hijacker = func(fieldOrFuncInput reflect.Type) (*di.BindObject, bool) { - if isContext(fieldOrFuncInput) { - return newContextBindObject(), true - } - return nil, false - } -) - -// newContextBindObject is being used on both targetFunc and targetStruct. -// if the func's input argument or the struct's field is a type of Context -// then we can do a fast binding using the ctxValue -// which is used as slice of reflect.Value, because of the final method's `Call`. -func newContextBindObject() *di.BindObject { - return &di.BindObject{ - Type: contextTyp, - BindType: di.Dynamic, - ReturnValue: func(ctxValue []reflect.Value) reflect.Value { - return ctxValue[0] - }, - } -} diff --git a/mvc2/engine.go b/mvc2/engine.go deleted file mode 100644 index 0dc900d6b8..0000000000 --- a/mvc2/engine.go +++ /dev/null @@ -1,53 +0,0 @@ -package mvc2 - -import ( - "github.com/kataras/iris/mvc2/di" - "github.com/kataras/golog" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/router" -) - -type Engine struct { - Dependencies *di.D -} - -func NewEngine() *Engine { - return &Engine{ - Dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker), - } -} - -func (e *Engine) Clone() *Engine { - child := NewEngine() - child.Dependencies = e.Dependencies.Clone() - return child -} - -func (e *Engine) Handler(handler interface{}) context.Handler { - h, err := MakeHandler(handler, e.Dependencies.Values...) - if err != nil { - golog.Errorf("mvc handler: %v", err) - } - return h -} - -func (e *Engine) Controller(router router.Party, controller interface{}, beforeActivate ...func(*ControllerActivator)) { - ca := newControllerActivator(router, controller, e.Dependencies) - - // give a priority to the "beforeActivate" - // callbacks, if any. - for _, cb := range beforeActivate { - cb(ca) - } - - // check if controller has an "BeforeActivate" function - // which accepts the controller activator and call it. - if activateListener, ok := controller.(interface { - BeforeActivate(*ControllerActivator) - }); ok { - activateListener.BeforeActivate(ca) - } - - ca.activate() -} diff --git a/mvc2/reflect.go b/mvc2/reflect.go deleted file mode 100644 index e18ece737e..0000000000 --- a/mvc2/reflect.go +++ /dev/null @@ -1,28 +0,0 @@ -package mvc2 - -import ( - "reflect" - - "github.com/kataras/iris/context" -) - -var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem() - -func isBaseController(ctrlTyp reflect.Type) bool { - return ctrlTyp.Implements(baseControllerTyp) -} - -var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem() - -func isContext(inTyp reflect.Type) bool { - return inTyp.Implements(contextTyp) -} - -func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { - n := funcTyp.NumIn() - funcIn := make([]reflect.Type, n, n) - for i := 0; i < n; i++ { - funcIn[i] = funcTyp.In(i) - } - return funcIn -} From de121296b234c0cac5ef2b706eb916d61f0ab84e Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 16 Dec 2017 17:57:20 +0200 Subject: [PATCH 22/79] don't create a new controller instance if it doesn't have struct dependencies and the fields length is 0 - 0.4MB/s difference from the raw handlers now. --- _benchmarks/iris-mvc/main.go | 4 +- mvc/controller.go | 127 ++++++++++++++++++++++++++++---- mvc/controller_method_parser.go | 2 +- mvc/param.go | 16 +--- 4 files changed, 117 insertions(+), 32 deletions(-) diff --git a/_benchmarks/iris-mvc/main.go b/_benchmarks/iris-mvc/main.go index 5b9ca83718..77743dff0a 100644 --- a/_benchmarks/iris-mvc/main.go +++ b/_benchmarks/iris-mvc/main.go @@ -5,7 +5,7 @@ package main // with bindings or without). import ( - "github.com/kataras/iris/_benchmarks/iris-mvc2/controllers" + "github.com/kataras/iris/_benchmarks/iris-mvc/controllers" "github.com/kataras/iris" "github.com/kataras/iris/mvc" @@ -16,3 +16,5 @@ func main() { mvc.New(app.Party("/api/values/{id}")).Register(new(controllers.ValuesController)) app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) } + +// +2MB/s faster than the previous implementation, 0.4MB/s difference from the raw handlers. diff --git a/mvc/controller.go b/mvc/controller.go index 13556dc81f..57d54dacc6 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -58,6 +58,8 @@ func getNameOf(typ reflect.Type) string { return fullname } +/// TODO: activate controllers with go routines so the startup time of iris +// can be improved on huge applications. func newControllerActivator(router router.Party, controller interface{}, d *di.D) *ControllerActivator { var ( val = reflect.ValueOf(controller) @@ -215,20 +217,125 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . if c.injector == nil { c.injector = c.Dependencies.Struct(c.Value) } + + handler := buildHandler(m, c.Type, c.Value, c.injector, funcInjector, funcIn) + + // register the handler now. + route := c.Router.Handle(method, path, append(middleware, handler)...) + if route != nil { + // change the main handler's name in order to respect the controller's and give + // a proper debug message. + route.MainHandlerName = fmt.Sprintf("%s.%s", c.FullName, funcName) + } + + return route +} + +// buildHandler has many many dublications but we do that to achieve the best +// performance possible, to use the information we know +// and calculate what is needed and what not in serve-time. +func buildHandler(m reflect.Method, typ reflect.Type, initRef reflect.Value, structInjector *di.StructInjector, funcInjector *di.FuncInjector, funcIn []reflect.Type) context.Handler { var ( - hasStructInjector = c.injector != nil && c.injector.Valid + hasStructInjector = structInjector != nil && structInjector.Valid hasFuncInjector = funcInjector != nil && funcInjector.Valid - implementsBase = isBaseController(c.Type) + implementsBase = isBaseController(typ) // we will make use of 'n' to make a slice of reflect.Value // to pass into if the function has input arguments that // are will being filled by the funcDependencies. n = len(funcIn) - elemTyp = di.IndirectType(c.Type) + elemTyp = di.IndirectType(typ) ) - handler := func(ctx context.Context) { + // if it doesn't implements the base controller, + // it may have struct injector and/or func injector. + if !implementsBase { + + if !hasStructInjector { + // if the controller doesn't have a struct injector + // and the controller's fields are empty + // then we don't need a new controller instance, we use the passed controller instance. + if elemTyp.NumField() == 0 { + if !hasFuncInjector { + return func(ctx context.Context) { + DispatchFuncResult(ctx, initRef.Method(m.Index).Call(emptyIn)) + } + } + + return func(ctx context.Context) { + in := make([]reflect.Value, n, n) + in[0] = initRef + funcInjector.Inject(&in, reflect.ValueOf(ctx)) + if ctx.IsStopped() { + return + } + + DispatchFuncResult(ctx, m.Func.Call(in)) + } + } + // it has fields, so it's request-scoped, even without struct injector + // it's safe to create a new controller on each request because the end-dev + // may use the controller's fields for request-scoping, so they should be + // zero on the next request. + if !hasFuncInjector { + return func(ctx context.Context) { + DispatchFuncResult(ctx, reflect.New(elemTyp).Method(m.Index).Call(emptyIn)) + } + } + return func(ctx context.Context) { + in := make([]reflect.Value, n, n) + in[0] = reflect.New(elemTyp) + funcInjector.Inject(&in, reflect.ValueOf(ctx)) + if ctx.IsStopped() { + return + } + + DispatchFuncResult(ctx, m.Func.Call(in)) + } + } + + // it has struct injector for sure and maybe a func injector. + if !hasFuncInjector { + return func(ctx context.Context) { + ctrl := reflect.New(elemTyp) + ctxValue := reflect.ValueOf(ctx) + elem := ctrl.Elem() + structInjector.InjectElem(elem, ctxValue) + if ctx.IsStopped() { + return + } + + DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) + } + } + + // has struct injector and func injector. + return func(ctx context.Context) { + ctrl := reflect.New(elemTyp) + ctxValue := reflect.ValueOf(ctx) + + elem := ctrl.Elem() + structInjector.InjectElem(elem, ctxValue) + if ctx.IsStopped() { + return + } + + in := make([]reflect.Value, n, n) + in[0] = ctrl + funcInjector.Inject(&in, ctxValue) + if ctx.IsStopped() { + return + } + + DispatchFuncResult(ctx, m.Func.Call(in)) + } + + } + + // if implements the base controller, + // it may have struct injector and func injector as well. + return func(ctx context.Context) { ctrl := reflect.New(elemTyp) if implementsBase { @@ -251,7 +358,7 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . ctxValue := reflect.ValueOf(ctx) if hasStructInjector { elem := ctrl.Elem() - c.injector.InjectElem(elem, ctxValue) + structInjector.InjectElem(elem, ctxValue) if ctx.IsStopped() { return } @@ -277,14 +384,4 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . } } - - // register the handler now. - route := c.Router.Handle(method, path, append(middleware, handler)...) - if route != nil { - // change the main handler's name in order to respect the controller's and give - // a proper debug message. - route.MainHandlerName = fmt.Sprintf("%s.%s", c.FullName, funcName) - } - - return route } diff --git a/mvc/controller_method_parser.go b/mvc/controller_method_parser.go index c5d0ec508c..97c1282edb 100644 --- a/mvc/controller_method_parser.go +++ b/mvc/controller_method_parser.go @@ -231,7 +231,7 @@ func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (st // so retry with the "funcArgPos" incremented. // // the "funcArgPos" will be updated to the caller as well - // because we return it as well. + // because we return it among the path and the error. return p.parsePathParam(path, w, funcArgPos+1) } return "", 0, errors.New("invalid syntax for " + p.fn.Name) diff --git a/mvc/param.go b/mvc/param.go index 4596afca52..b66ae846df 100644 --- a/mvc/param.go +++ b/mvc/param.go @@ -27,25 +27,11 @@ func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) if paramType.Assignable(in.Kind()) { consumedParams[j] = true - // fmt.Printf("path_param_binder.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) + // fmt.Printf("param.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) values = append(values, makeFuncParamGetter(paramType, paramName)) } } } - // funcInIdx := 0 - // // it's a valid param type. - // for _, p := range params { - // in := funcIn[funcInIdx] - // paramType := p.Type - // paramName := p.Name - // // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) - // if paramType.Assignable(in.Kind()) { - // // fmt.Printf("path_param_binder.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) - // values = append(values, makeFuncParamGetter(paramType, paramName)) - // } - - // funcInIdx++ - // } return } From d8d87757afb2b520241e723f60ee49b2b8864b46 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 16 Dec 2017 21:27:20 +0200 Subject: [PATCH 23/79] add simple tests for _examples/mvc/hello-world and session-controller --- _examples/mvc/hello-world/main.go | 9 ++- _examples/mvc/hello-world/main_test.go | 23 ++++++ _examples/mvc/session-controller/main.go | 72 +++++++++---------- _examples/mvc/session-controller/main_test.go | 21 ++++++ core/router/handler.go | 4 +- mvc/controller.go | 34 +++++---- mvc/ideas/1/main.go | 4 ++ 7 files changed, 109 insertions(+), 58 deletions(-) create mode 100644 _examples/mvc/hello-world/main_test.go create mode 100644 _examples/mvc/session-controller/main_test.go diff --git a/_examples/mvc/hello-world/main.go b/_examples/mvc/hello-world/main.go index 006c818c08..ca8550beb9 100644 --- a/_examples/mvc/hello-world/main.go +++ b/_examples/mvc/hello-world/main.go @@ -28,7 +28,9 @@ import ( // what suits you best with Iris, low-level handlers: performance // or high-level controllers: easier to maintain and smaller codebase on large applications. -func main() { +// Of course you can put all these to main func, it's just a separate function +// for the main_test.go. +func newApp() *iris.Application { app := iris.New() // Optionally, add two built'n handlers // that can recover from any http-relative panics @@ -38,6 +40,11 @@ func main() { // Register a controller based on the root Router, "/". mvc.New(app).Register(new(ExampleController)) + return app +} + +func main() { + app := newApp() // http://localhost:8080 // http://localhost:8080/ping diff --git a/_examples/mvc/hello-world/main_test.go b/_examples/mvc/hello-world/main_test.go new file mode 100644 index 0000000000..5c8e271923 --- /dev/null +++ b/_examples/mvc/hello-world/main_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "testing" + + "github.com/kataras/iris/httptest" +) + +func TestMVCHelloWorld(t *testing.T) { + e := httptest.New(t, newApp()) + + e.GET("/").Expect().Status(httptest.StatusOK). + ContentType("text/html", "utf-8").Body().Equal("

Welcome

") + + e.GET("/ping").Expect().Status(httptest.StatusOK). + Body().Equal("pong") + + e.GET("/hello").Expect().Status(httptest.StatusOK). + JSON().Object().Value("message").Equal("Hello Iris!") + + e.GET("/custom_path").Expect().Status(httptest.StatusOK). + Body().Equal("hello from the custom handler without following the naming guide") +} diff --git a/_examples/mvc/session-controller/main.go b/_examples/mvc/session-controller/main.go index b768da6819..19281993a9 100644 --- a/_examples/mvc/session-controller/main.go +++ b/_examples/mvc/session-controller/main.go @@ -7,18 +7,14 @@ import ( "time" "github.com/kataras/iris" + "github.com/kataras/iris/mvc" "github.com/kataras/iris/sessions" ) // VisitController handles the root route. type VisitController struct { - iris.C - - // the sessions manager, we need that to set `Session`. - // It's binded from `app.Controller`. - Manager *sessions.Sessions // the current request session, - // its initialization happens at the `BeginRequest`. + // its initialization happens by the dependency function that we've added to the `visitApp`. Session *sessions.Session // A time.time which is binded from the `app.Controller`, @@ -26,53 +22,49 @@ type VisitController struct { StartTime time.Time } -// BeginRequest is executed for each Get, Post, Put requests, -// can be used to share context, common data -// or to cancel the request via `ctx.StopExecution()`. -func (c *VisitController) BeginRequest(ctx iris.Context) { - // always call the embedded `BeginRequest` before everything else. - c.C.BeginRequest(ctx) - - if c.Manager == nil { - ctx.Application().Logger().Errorf(`VisitController: sessions manager is nil, you should bind it`) - // dont run the main method handler and any "done" handlers. - ctx.StopExecution() - return - } - - // set the `c.Session` we will use that in our Get method. - c.Session = c.Manager.Start(ctx) -} - // Get handles // Method: GET // Path: http://localhost:8080 func (c *VisitController) Get() string { - // get the visits, before calcuate this new one. - visits, _ := c.Session.GetIntDefault("visits", 0) - - // increment the visits and store to the session. - visits++ - c.Session.Set("visits", visits) - + // it increments a "visits" value of integer by one, + // if the entry with key 'visits' doesn't exist it will create it for you. + visits := c.Session.Increment("visits", 1) // write the current, updated visits. since := time.Now().Sub(c.StartTime).Seconds() return fmt.Sprintf("%d visit from my current session in %0.1f seconds of server's up-time", visits, since) } -var ( - manager = sessions.New(sessions.Config{Cookie: "mysession_cookie_name"}) -) - -func main() { +func newApp() *iris.Application { app := iris.New() + sess := sessions.New(sessions.Config{Cookie: "mysession_cookie_name"}) - // bind our session manager, which is required, to the `VisitController.Manager` + visitApp := mvc.New(app.Party("/")) + // bind the current *session.Session, which is required, to the `VisitController.Session` // and the time.Now() to the `VisitController.StartTime`. - app.Controller("/", new(VisitController), - manager, - time.Now()) + visitApp.AddDependencies( + // if dependency is a function which accepts + // a Context and returns a single value + // then the result type of this function is resolved by the controller + // and on each request it will call the function with its Context + // and set the result(the *sessions.Session here) to the controller's field. + // + // If dependencies are registered without field or function's input arguments as + // consumers then those dependencies are being ignored before the server ran, + // so you can bind many dependecies and use them in different controllers. + // func(ctx iris.Context) *sessions.Session { + // return sess.Start(ctx) + // }, -> same as mvc.Session(sess): + mvc.Session(sess), + time.Now(), + ) + visitApp.Register(new(VisitController)) + + return app +} + +func main() { + app := newApp() // 1. open the browser (no in private mode) // 2. navigate to http://localhost:8080 diff --git a/_examples/mvc/session-controller/main_test.go b/_examples/mvc/session-controller/main_test.go new file mode 100644 index 0000000000..f89005014b --- /dev/null +++ b/_examples/mvc/session-controller/main_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "testing" + + "github.com/kataras/iris/httptest" +) + +func TestMVCSession(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://example.com")) + + e1 := e.GET("/").Expect().Status(httptest.StatusOK) + e1.Cookies().NotEmpty() + e1.Body().Contains("1 visit") + + e.GET("/").Expect().Status(httptest.StatusOK). + Body().Contains("2 visit") + + e.GET("/").Expect().Status(httptest.StatusOK). + Body().Contains("3 visit") +} diff --git a/core/router/handler.go b/core/router/handler.go index 0114f9daa6..bb00eb3aa4 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -1,14 +1,14 @@ package router import ( - "github.com/kataras/golog" "html" "net/http" "sort" "strings" - "github.com/kataras/iris/context" + "github.com/kataras/golog" + "github.com/kataras/iris/context" "github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/netutil" "github.com/kataras/iris/core/router/node" diff --git a/mvc/controller.go b/mvc/controller.go index 57d54dacc6..33678313ce 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -58,8 +58,6 @@ func getNameOf(typ reflect.Type) string { return fullname } -/// TODO: activate controllers with go routines so the startup time of iris -// can be improved on huge applications. func newControllerActivator(router router.Party, controller interface{}, d *di.D) *ControllerActivator { var ( val = reflect.ValueOf(controller) @@ -121,24 +119,30 @@ func (c *ControllerActivator) isReservedMethod(name string) bool { return false } +func (c *ControllerActivator) parseMethod(m reflect.Method) { + httpMethod, httpPath, err := parseMethod(m, c.isReservedMethod) + if err != nil { + if err != errSkip { + err = fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.FullName, m.Name, err) + c.Router.GetReporter().AddErr(err) + + } + return + } + + c.Handle(httpMethod, httpPath, m.Name) +} + // register all available, exported methods to handlers if possible. func (c *ControllerActivator) parseMethods() { n := c.Type.NumMethod() + // wg := &sync.WaitGroup{} + // wg.Add(n) for i := 0; i < n; i++ { m := c.Type.Method(i) - - httpMethod, httpPath, err := parseMethod(m, c.isReservedMethod) - if err != nil { - if err != errSkip { - err = fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.FullName, m.Name, err) - c.Router.GetReporter().AddErr(err) - - } - continue - } - - c.Handle(httpMethod, httpPath, m.Name) + c.parseMethod(m) } + // wg.Wait() } func (c *ControllerActivator) activate() { @@ -248,7 +252,7 @@ func buildHandler(m reflect.Method, typ reflect.Type, initRef reflect.Value, str elemTyp = di.IndirectType(typ) ) - // if it doesn't implements the base controller, + // if it doesn't implement the base controller, // it may have struct injector and/or func injector. if !implementsBase { diff --git a/mvc/ideas/1/main.go b/mvc/ideas/1/main.go index 1de61ec982..88c120d1c8 100644 --- a/mvc/ideas/1/main.go +++ b/mvc/ideas/1/main.go @@ -9,6 +9,10 @@ import ( "github.com/kataras/iris/mvc" ) +// TODO: It's not here but this file is what I'll see before the commit in order to delete it: +// Think a way to simplify the router cycle, I did create it to support any type of router +// but as I see nobody wants to override the iris router's behavior(I'm not speaking about wrapper, this will stay of course because it's useful on security-critical middlewares) because it's the best by far. +// Therefore I should reduce some "freedom of change" for the shake of code maintanability in the core/router files: handler.go | router.go and single change on APIBuilder's field. func main() { app := iris.New() mvc.New(app.Party("/todo")).Configure(TodoApp) From a1b5cd56516f56ddfeaf5c6da898bcafb9b2b52a Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 16 Dec 2017 23:09:00 +0200 Subject: [PATCH 24/79] Add session.Destroy (before I've added: Increment & Decrement entry helpers as well) | Improve the debug messages for the controllers --- _examples/mvc/login/main.go | 43 ++++++++++++------- .../login/web/controllers/user_controller.go | 42 ++++++------------ .../login/web/controllers/users_controller.go | 10 ++++- mvc/controller.go | 15 +++++-- mvc/di/func.go | 29 ++++++++++++- mvc/di/struct.go | 23 +++++++++- sessions/session.go | 10 +++++ 7 files changed, 119 insertions(+), 53 deletions(-) diff --git a/_examples/mvc/login/main.go b/_examples/mvc/login/main.go index fffe66c60e..105f961015 100644 --- a/_examples/mvc/login/main.go +++ b/_examples/mvc/login/main.go @@ -5,12 +5,14 @@ package main import ( "time" - "github.com/kataras/iris" "github.com/kataras/iris/_examples/mvc/login/datasource" "github.com/kataras/iris/_examples/mvc/login/repositories" "github.com/kataras/iris/_examples/mvc/login/services" "github.com/kataras/iris/_examples/mvc/login/web/controllers" "github.com/kataras/iris/_examples/mvc/login/web/middleware" + + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" "github.com/kataras/iris/sessions" ) @@ -34,7 +36,9 @@ func main() { ctx.View("shared/error.html") }) - // Create our repositories and services. + // ---- Register our controllers. ---- + + // Prepare our repositories and services. db, err := datasource.LoadUsers(datasource.Memory) if err != nil { app.Logger().Fatalf("error while loading the users: %v", err) @@ -43,29 +47,38 @@ func main() { repo := repositories.NewUserRepository(db) userService := services.NewUserService(repo) - // Register our controllers. - app.Controller("/users", new(controllers.UsersController), - // Add the basic authentication(admin:password) middleware - // for the /users based requests. - middleware.BasicAuth, - // Bind the "userService" to the UserController's Service (interface) field. - userService, - ) + // "/users" based mvc application. + users := mvc.New(app.Party("/users")) + // Add the basic authentication(admin:password) middleware + // for the /users based requests. + users.Router.Use(middleware.BasicAuth) + // Bind the "userService" to the UserController's Service (interface) field. + users.AddDependencies(userService) + users.Register(new(controllers.UsersController)) + // "/user" based mvc application. sessManager := sessions.New(sessions.Config{ Cookie: "sessioncookiename", Expires: 24 * time.Hour, }) - app.Controller("/user", new(controllers.UserController), userService, sessManager) + user := mvc.New(app.Party("/user")) + user.AddDependencies( + userService, + mvc.Session(sessManager), + ) + user.Register(new(controllers.UserController)) - // Start the web server at localhost:8080 - // http://localhost:8080/hello - // http://localhost:8080/hello/iris + // http://localhost:8080/noexist + // and all controller's methods like // http://localhost:8080/users/1 app.Run( + // Starts the web server at localhost:8080 iris.Addr("localhost:8080"), + // Disables the updater. iris.WithoutVersionChecker, + // Ignores err server closed log when CTRL/CMD+C pressed. iris.WithoutServerError(iris.ErrServerClosed), - iris.WithOptimizations, // enables faster json serialization and more + // Enables faster json serialization and more. + iris.WithOptimizations, ) } diff --git a/_examples/mvc/login/web/controllers/user_controller.go b/_examples/mvc/login/web/controllers/user_controller.go index e4bda39d94..f246595bba 100644 --- a/_examples/mvc/login/web/controllers/user_controller.go +++ b/_examples/mvc/login/web/controllers/user_controller.go @@ -6,7 +6,7 @@ import ( "github.com/kataras/iris/_examples/mvc/login/datamodels" "github.com/kataras/iris/_examples/mvc/login/services" - "github.com/kataras/iris/context" + "github.com/kataras/iris" "github.com/kataras/iris/mvc" "github.com/kataras/iris/sessions" ) @@ -20,39 +20,21 @@ import ( // GET /user/me // All HTTP Methods /user/logout type UserController struct { - // mvc.C is just a lightweight lightweight alternative - // to the "mvc.Controller" controller type, - // use it when you don't need mvc.Controller's fields - // (you don't need those fields when you return values from the method functions). - mvc.C + // context is auto-binded by Iris on each request, + // remember that on each incoming request iris creates a new UserController each time, + // so all fields are request-scoped by-default, only dependency injection is able to set + // custom fields like the Service which is the same for all requests (static binding) + // and the Session which depends on the current context (dynamic binding). + Ctx iris.Context // Our UserService, it's an interface which // is binded from the main application. Service services.UserService - // Session-relative things. - Manager *sessions.Sessions + // Session, binded using dependency injection from the main.go. Session *sessions.Session } -// BeginRequest will set the current session to the controller. -// -// Remember: iris.Context and context.Context is exactly the same thing, -// iris.Context is just a type alias for go 1.9 users. -// We use context.Context here because we don't need all iris' root functions, -// when we see the import paths, we make it visible to ourselves that this file is using only the context. -func (c *UserController) BeginRequest(ctx context.Context) { - c.C.BeginRequest(ctx) - - if c.Manager == nil { - ctx.Application().Logger().Errorf(`UserController: sessions manager is nil, you should bind it`) - ctx.StopExecution() // dont run the main method handler and any "done" handlers. - return - } - - c.Session = c.Manager.Start(ctx) -} - const userIDKey = "UserID" func (c *UserController) getCurrentUserID() int64 { @@ -65,12 +47,12 @@ func (c *UserController) isLoggedIn() bool { } func (c *UserController) logout() { - c.Manager.DestroyByID(c.Session.ID()) + c.Session.Destroy() } var registerStaticView = mvc.View{ Name: "user/register.html", - Data: context.Map{"Title": "User Registration"}, + Data: iris.Map{"Title": "User Registration"}, } // GetRegister handles GET: http://localhost:8080/user/register. @@ -119,7 +101,7 @@ func (c *UserController) PostRegister() mvc.Result { var loginStaticView = mvc.View{ Name: "user/login.html", - Data: context.Map{"Title": "User Login"}, + Data: iris.Map{"Title": "User Login"}, } // GetLogin handles GET: http://localhost:8080/user/login. @@ -172,7 +154,7 @@ func (c *UserController) GetMe() mvc.Result { return mvc.View{ Name: "user/me.html", - Data: context.Map{ + Data: iris.Map{ "Title": "Profile of " + u.Username, "User": u, }, diff --git a/_examples/mvc/login/web/controllers/users_controller.go b/_examples/mvc/login/web/controllers/users_controller.go index f3f7b80f0b..38d42a1a02 100644 --- a/_examples/mvc/login/web/controllers/users_controller.go +++ b/_examples/mvc/login/web/controllers/users_controller.go @@ -4,7 +4,7 @@ import ( "github.com/kataras/iris/_examples/mvc/login/datamodels" "github.com/kataras/iris/_examples/mvc/login/services" - "github.com/kataras/iris/mvc" + "github.com/kataras/iris" ) // UsersController is our /users API controller. @@ -14,8 +14,14 @@ import ( // DELETE /users/{id:long} | delete by id // Requires basic authentication. type UsersController struct { - mvc.C + // context is auto-binded by Iris on each request, + // remember that on each incoming request iris creates a new UserController each time, + // so all fields are request-scoped by-default, only dependency injection is able to set + // custom fields like the Service which is the same for all requests (static binding). + Ctx iris.Context + // Our UserService, it's an interface which + // is binded from the main application. Service services.UserService } diff --git a/mvc/controller.go b/mvc/controller.go index 33678313ce..ec2d3211d2 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -2,6 +2,7 @@ package mvc import ( "fmt" + "github.com/kataras/golog" "reflect" "strings" @@ -220,9 +221,17 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // this is a logical flow, so we will choose that one -> if c.injector == nil { c.injector = c.Dependencies.Struct(c.Value) + if c.injector.Valid { + golog.Debugf("MVC dependencies of '%s':\n%s", c.FullName, c.injector.String()) + } + + } + + if funcInjector.Valid { + golog.Debugf("MVC dependencies of method '%s.%s':\n%s", c.FullName, funcName, funcInjector.String()) } - handler := buildHandler(m, c.Type, c.Value, c.injector, funcInjector, funcIn) + handler := buildControllerHandler(m, c.Type, c.Value, c.injector, funcInjector, funcIn) // register the handler now. route := c.Router.Handle(method, path, append(middleware, handler)...) @@ -235,10 +244,10 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . return route } -// buildHandler has many many dublications but we do that to achieve the best +// buildControllerHandler has many many dublications but we do that to achieve the best // performance possible, to use the information we know // and calculate what is needed and what not in serve-time. -func buildHandler(m reflect.Method, typ reflect.Type, initRef reflect.Value, structInjector *di.StructInjector, funcInjector *di.FuncInjector, funcIn []reflect.Type) context.Handler { +func buildControllerHandler(m reflect.Method, typ reflect.Type, initRef reflect.Value, structInjector *di.StructInjector, funcInjector *di.FuncInjector, funcIn []reflect.Type) context.Handler { var ( hasStructInjector = structInjector != nil && structInjector.Valid hasFuncInjector = funcInjector != nil && funcInjector.Valid diff --git a/mvc/di/func.go b/mvc/di/func.go index be2064323a..e80d59cf20 100644 --- a/mvc/di/func.go +++ b/mvc/di/func.go @@ -1,6 +1,9 @@ package di -import "reflect" +import ( + "fmt" + "reflect" +) type ( targetFuncInput struct { @@ -18,7 +21,9 @@ type ( Length int // Valid is True when `Length` is > 0, it's statically set-ed for // performance reasons. - Valid bool // + Valid bool + + trace string // for debug info. } ) @@ -83,9 +88,29 @@ func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, v // s.Length = n s.Length = len(s.inputs) s.Valid = s.Length > 0 + + for i, in := range s.inputs { + bindmethodTyp := "Static" + + if in.Object.BindType == Dynamic { + bindmethodTyp = "Dynamic" + } + + typIn := typ.In(in.InputIndex) + // remember: on methods that are part of a struct (i.e controller) + // the input index = 1 is the begggining instead of the 0, + // because the 0 is the controller receiver pointer of the method. + + s.trace += fmt.Sprintf("[%d] %s binding: '%s' for input position: %d and type: '%s'\n", i+1, bindmethodTyp, in.Object.Type.String(), in.InputIndex, typIn.String()) + } + return s } +func (s *FuncInjector) String() string { + return s.trace +} + func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) { args := *in for _, input := range s.inputs { diff --git a/mvc/di/struct.go b/mvc/di/struct.go index 76e177b092..6885562357 100644 --- a/mvc/di/struct.go +++ b/mvc/di/struct.go @@ -1,6 +1,9 @@ package di -import "reflect" +import ( + "fmt" + "reflect" +) type ( targetStructField struct { @@ -13,6 +16,8 @@ type ( // fields []*targetStructField Valid bool // is True when contains fields and it's a valid target struct. + + trace string // for debug info. } ) @@ -56,9 +61,25 @@ func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, } s.Valid = len(s.fields) > 0 + + if s.Valid { + for i, f := range s.fields { + bindmethodTyp := "Static" + if f.Object.BindType == Dynamic { + bindmethodTyp = "Dynamic" + } + elemField := s.elemType.FieldByIndex(f.FieldIndex) + s.trace += fmt.Sprintf("[%d] %s binding: '%s' for field '%s %s'\n", i+1, bindmethodTyp, f.Object.Type.String(), elemField.Name, elemField.Type.String()) + } + } + return s } +func (s *StructInjector) String() string { + return s.trace +} + func (s *StructInjector) Inject(dest interface{}, ctx ...reflect.Value) { if dest == nil { return diff --git a/sessions/session.go b/sessions/session.go index 266f788139..bb9b46f2ac 100644 --- a/sessions/session.go +++ b/sessions/session.go @@ -31,6 +31,16 @@ type ( } ) +// Destroy destroys this session, it removes all sessions and flash values, +// the session entry from the server and updates the registered session databases, +// note that this method does NOT removes the client's cookie, although +// it should be re-seted if new session is attached to that (client). +// +// Use the session's manager `Destroy(ctx)` in order to remove the cookie as well. +func (s *Session) Destroy() { + s.provider.deleteSession(s) +} + // ID returns the session's ID. func (s *Session) ID() string { return s.sid From a5f2cea0816e3d0d6cf5c4ef410bbee573cf4fa7 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 16 Dec 2017 23:12:49 +0200 Subject: [PATCH 25/79] grammar fixes(?) --- mvc/controller.go | 6 +++--- sessions/session.go | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/mvc/controller.go b/mvc/controller.go index ec2d3211d2..515e8982dd 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -2,15 +2,15 @@ package mvc import ( "fmt" - "github.com/kataras/golog" "reflect" "strings" - "github.com/kataras/iris/mvc/di" - "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" "github.com/kataras/iris/core/router/macro" + "github.com/kataras/iris/mvc/di" + + "github.com/kataras/golog" ) // BaseController is the optional controller interface, if it's diff --git a/sessions/session.go b/sessions/session.go index bb9b46f2ac..92b46e39fe 100644 --- a/sessions/session.go +++ b/sessions/session.go @@ -31,10 +31,12 @@ type ( } ) -// Destroy destroys this session, it removes all sessions and flash values, -// the session entry from the server and updates the registered session databases, -// note that this method does NOT removes the client's cookie, although -// it should be re-seted if new session is attached to that (client). +// Destroy destroys this session, it removes its session values and any flashes. +// This session entry will be removed from the server, +// the registered session databases will be notified for this deletion as well. +// +// Note that this method does NOT remove the client's cookie, although +// it should be reseted if new session is attached to that (client). // // Use the session's manager `Destroy(ctx)` in order to remove the cookie as well. func (s *Session) Destroy() { From 5fc26b301427ec52900c12558d7bf73ef4e78c5c Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 16 Dec 2017 23:27:04 +0200 Subject: [PATCH 26/79] update mvc/README.md --- mvc/README.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/mvc/README.md b/mvc/README.md index 715abce83f..0e3d4c4fe3 100644 --- a/mvc/README.md +++ b/mvc/README.md @@ -1,14 +1,14 @@ -# MVC Internals +# MVC Engine's Internals * `MakeHandler` - accepts a function which accepts any input and outputs any result, and any optional values that will be used as binders, if needed they will be converted in order to be faster at serve-time. Returns a `context/iris#Handler` and a non-nil error if passed function cannot be wrapped to a raw `context/iris#Handler` -* `Engine` - The "manager" of the controllers and handlers, can be grouped and an `Engine` can have any number of children. - * `Engine#Bind` Binds values to be used inside on one or more handlers and controllers - * `Engine#Handler` - Creates and returns a new mvc handler, which accept any input parameters (calculated by the binders) and output any result which will be sent as a response to the HTTP Client. Calls the `MakeHandler` with the Engine's `Input` values as the binders - * `Engine#Controller` - Creates and activates a controller based on a struct which has the `C` as an embedded , anonymous, field and defines methods to be used as routes. Can accept any optional activator listeners in order to bind any custom routes or change the bindings, called once at startup -* `C` * Struct fields with `Struct Binding` * Methods with `Dynamic Binding` - +* `Engine` - The "manager" of the controllers and handlers, can be grouped and an `Engine` can have any number of children. + * `Engine#Bind` Binds values to be used inside on one or more handlers and controllers + * `Engine#Handler` - Creates and returns a new mvc handler, which accept any input parameters (calculated by the binders) and output any result which will be sent as a response to the HTTP Client. Calls the `MakeHandler` with the Engine's `Dependencies.Values` as the binders + * `Engine#Controller` - Creates and activates a controller based on a struct which has the `C` as an embedded , anonymous, field and defines methods to be used as routes. Can accept any optional activator listeners in order to bind any custom routes or change the bindings, called once at startup. +* The highest level feature of this package is the `Application` which contains +an `iris.Party` as its Router and an `Engine`. A new `Application` is created with `New(iris.Party)` and registers a new `Engine` for itself, `Engines` can be shared via the `Application#NewChild` or by manually creating an `&Application{ Engine: engine, Router: subRouter }`. The `Application` is being used to build complete `mvc applications through controllers`, it doesn't contain any method to convert mvc handlers to raw handlers, although its `Engine` can do that but it's not a common scenario. Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/mvc. @@ -21,8 +21,6 @@ First of all, they can be binded to `func input arguments` (custom handlers) or func myHandler(user User) {} type myController struct { - C - // consume the user here, as struct field. user User } @@ -55,7 +53,7 @@ myHandler := func(user User) { ### Static Binding -`Value (Service)`, this is used to bind a value instance, like a service or a database connection. +`Static Value (Service)`, this is used to bind a value instance, like a service or a database connection. ```go // optional but we declare interface most of the times to @@ -80,7 +78,7 @@ myHandler := func(service Service) { } ``` -### Bind +### Add Dependencies #### For Handlers @@ -90,10 +88,11 @@ MakeHandler is used to create a handler based on a function which can accept any h, err := MakeHandler(myHandler, reflect.ValueOf(myBinder)) ``` -Values passed in `Bind` are binded to all handlers and controllers that are expected a type of the returned value, in this case the myBinder indicates a dynamic/serve-time function which returns a User, as shown above. +Values passed in `Dependencies` are binded to all handlers and controllers that are expected a type of the returned value, in this case the myBinder indicates a dynamic/serve-time function which returns a User, as shown above. ```go -m := New().Bind(myBinder) +m := NewEngine() +m.Dependencies.Add(myBinder) h := m.Handler(myHandler) ``` @@ -102,17 +101,20 @@ h := m.Handler(myHandler) ```go app := iris.New() -New().Bind(myBinder).Controller(app, new(myController)) +m := NewEngine() +m.Dependencies.Add(myBinder) +m.Controller(app, new(myController)) // ... ``` ```go sub := app.Party("/sub") -New().Controller(sub, &myController{service: myService}) +m := NewEngine() +m.Controller(sub, &myController{service: myService}) ``` ```go -New().Controller(sub.Party("/subsub"), new(myController), func(ca *ControllerActivator) { - ca.Bind(myService) +NewEngine().Controller(sub.Party("/subsub"), new(myController), func(ca *ControllerActivator) { + ca.Dependencies.Add(myService) }) ``` \ No newline at end of file From e89b4f14496f325b6b7624d59ec9359db69eb74f Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 17 Dec 2017 06:34:16 +0200 Subject: [PATCH 27/79] don't create new controller instance when all fields are set-ed by the end-dev - but we keep show those fields' values as Dependencies on the BeforeActivate in order to the future custom controllers authors to be able to check if something is added as dependecy or even manually set-ed before bind their own dependecies, otherwise they could override the manually set-ing --- mvc/README.md | 2 +- mvc/controller.go | 73 ++++++++++++++++++++++------------------- mvc/controller_test.go | 37 ++++++++++++++++++++- mvc/di/struct.go | 5 ++- mvc/func_result_test.go | 2 +- 5 files changed, 79 insertions(+), 40 deletions(-) diff --git a/mvc/README.md b/mvc/README.md index 0e3d4c4fe3..51de01be15 100644 --- a/mvc/README.md +++ b/mvc/README.md @@ -1,4 +1,4 @@ -# MVC Engine's Internals +# MVC Internals * `MakeHandler` - accepts a function which accepts any input and outputs any result, and any optional values that will be used as binders, if needed they will be converted in order to be faster at serve-time. Returns a `context/iris#Handler` and a non-nil error if passed function cannot be wrapped to a raw `context/iris#Handler` * Struct fields with `Struct Binding` diff --git a/mvc/controller.go b/mvc/controller.go index 515e8982dd..cba69d7879 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -68,15 +68,6 @@ func newControllerActivator(router router.Party, controller interface{}, d *di.D fullName = getNameOf(typ) ) - // the following will make sure that if - // the controller's has set-ed pointer struct fields by the end-dev - // we will include them to the bindings. - // set bindings to the non-zero pointer fields' values that may be set-ed by - // the end-developer when declaring the controller, - // activate listeners needs them in order to know if something set-ed already or not, - // look `BindTypeExists`. - d.Values = append(di.LookupNonZeroFieldsValues(val), d.Values...) - c := &ControllerActivator{ // give access to the Router to the end-devs if they need it for some reason, // i.e register done handlers. @@ -97,6 +88,19 @@ func newControllerActivator(router router.Party, controller interface{}, d *di.D Dependencies: d, } + filledFieldValues := di.LookupNonZeroFieldsValues(val) + c.Dependencies.AddValue(filledFieldValues...) + + if len(filledFieldValues) == di.IndirectType(typ).NumField() { + // all fields are filled by the end-developer, + // the controller doesn't contain any other field, not any dynamic binding as well. + // Therefore we don't need to create a new controller each time. + // Set the c.injector now instead on the first `Handle` and set it to invalid state + // in order to `buildControllerHandler` ignore + // creating new controller value on each incoming request. + c.injector = &di.StructInjector{Valid: false} + } + return c } @@ -109,6 +113,28 @@ func whatReservedMethods(typ reflect.Type) []string { return methods } +// IsRequestScoped returns new if each request has its own instance +// of the controller and it contains dependencies that are not manually +// filled by the struct initialization from the caller. +func (c *ControllerActivator) IsRequestScoped() bool { + // if the c.injector == nil means that is not seted to invalidate state, + // so it contains more fields that are filled by the end-dev. + // This "strange" check happens because the `IsRequestScoped` may + // called before the controller activation complete its task (see Handle: if c.injector == nil). + if c.injector == nil { // is nil so it contains more fields, maybe request-scoped or dependencies. + return true + } + if c.injector.Valid { + // if injector is not nil: + // if it is !Valid then all fields are manually filled by the end-dev (see `newControllerActivator`). + // if it is Valid then it's filled on the first `Handle` + // and it has more than one valid dependency from the registered values. + return true + } + // it's not nil and it's !Valid. + return false +} + // checks if a method is already registered. func (c *ControllerActivator) isReservedMethod(name string) bool { for _, s := range c.reservedMethods { @@ -224,7 +250,6 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . if c.injector.Valid { golog.Debugf("MVC dependencies of '%s':\n%s", c.FullName, c.injector.String()) } - } if funcInjector.Valid { @@ -267,38 +292,18 @@ func buildControllerHandler(m reflect.Method, typ reflect.Type, initRef reflect. if !hasStructInjector { // if the controller doesn't have a struct injector - // and the controller's fields are empty + // and the controller's fields are empty or all set-ed by the end-dev // then we don't need a new controller instance, we use the passed controller instance. - if elemTyp.NumField() == 0 { - if !hasFuncInjector { - return func(ctx context.Context) { - DispatchFuncResult(ctx, initRef.Method(m.Index).Call(emptyIn)) - } - } - return func(ctx context.Context) { - in := make([]reflect.Value, n, n) - in[0] = initRef - funcInjector.Inject(&in, reflect.ValueOf(ctx)) - if ctx.IsStopped() { - return - } - - DispatchFuncResult(ctx, m.Func.Call(in)) - } - } - // it has fields, so it's request-scoped, even without struct injector - // it's safe to create a new controller on each request because the end-dev - // may use the controller's fields for request-scoping, so they should be - // zero on the next request. if !hasFuncInjector { return func(ctx context.Context) { - DispatchFuncResult(ctx, reflect.New(elemTyp).Method(m.Index).Call(emptyIn)) + DispatchFuncResult(ctx, initRef.Method(m.Index).Call(emptyIn)) } } + return func(ctx context.Context) { in := make([]reflect.Value, n, n) - in[0] = reflect.New(elemTyp) + in[0] = initRef funcInjector.Inject(&in, reflect.ValueOf(ctx)) if ctx.IsStopped() { return diff --git a/mvc/controller_test.go b/mvc/controller_test.go index b7609690db..b2c7d75e71 100644 --- a/mvc/controller_test.go +++ b/mvc/controller_test.go @@ -433,7 +433,7 @@ func TestControllerActivateListener(t *testing.T) { app := iris.New() NewEngine().Controller(app, new(testControllerActivateListener)) m := NewEngine() - m.Dependencies.Add(&testBindType{ // will bind to all controllers under this .New() MVC Engine. + m.Dependencies.Add(&testBindType{ title: "my title", }) m.Controller(app.Party("/manual"), new(testControllerActivateListener)) @@ -452,3 +452,38 @@ func TestControllerActivateListener(t *testing.T) { e.GET("/manual2").Expect().Status(iris.StatusOK). Body().Equal("my title") } + +type testControllerNotCreateNewDueManuallySettingAllFields struct { + TitlePointer *testBindType +} + +func (c *testControllerNotCreateNewDueManuallySettingAllFields) Get() string { + return c.TitlePointer.title +} + +func TestControllerNotCreateNewDueManuallySettingAllFields(t *testing.T) { + app := iris.New() + NewEngine().Controller(app, &testControllerNotCreateNewDueManuallySettingAllFields{ + TitlePointer: &testBindType{ + title: "my title", + }, + }, func(ca *ControllerActivator) { + if n := len(ca.Dependencies.Values); n != 1 { + t.Fatalf(`expecting 1 dependency, the 'TitlePointer' which we manually insert + and the fields length is 1 so it will not create a new controller on each request + however the dependencies are available here + although the struct injector is being ignored when + creating the controller's handlers because we set it to invalidate state at "newControllerActivator" + -- got dependencies length: %d`, n) + } + + if ca.IsRequestScoped() { + t.Fatalf(`this controller shouldn't be tagged used as request scoped(create new instances on each request), + it doesn't contain any dynamic value or dependencies that should be binded via the iris mvc engine`) + } + }) + + e := httptest.New(t, app) + e.GET("/").Expect().Status(iris.StatusOK). + Body().Equal("my title") +} diff --git a/mvc/di/struct.go b/mvc/di/struct.go index 6885562357..2ee2c07ab3 100644 --- a/mvc/di/struct.go +++ b/mvc/di/struct.go @@ -15,9 +15,8 @@ type ( elemType reflect.Type // fields []*targetStructField - Valid bool // is True when contains fields and it's a valid target struct. - - trace string // for debug info. + Valid bool // is true when contains fields and it's a valid target struct. + trace string // for debug info. } ) diff --git a/mvc/func_result_test.go b/mvc/func_result_test.go index 7b9b04a7c1..7de6c6887d 100644 --- a/mvc/func_result_test.go +++ b/mvc/func_result_test.go @@ -178,7 +178,7 @@ func TestControllerMethodResultTypes(t *testing.T) { app := iris.New() NewEngine().Controller(app, new(testControllerMethodResultTypes)) - e := httptest.New(t, app, httptest.LogLevel("debug")) + e := httptest.New(t, app) e.GET("/text").Expect().Status(iris.StatusOK). Body().Equal("text") From 11c92cfe2225e0a6bb430230cc77cb8bc6598dce Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 18 Dec 2017 00:16:10 +0200 Subject: [PATCH 28/79] more checks about creating new instance of controller on each request - this time if all bindings are static then set them to the initial-devpassed controller and if the total number of lengths are equal with these static dependencies then we ignore the injector and use the initial controller on each request - maximize the performance when simple controller is used - need more cleanup before new release but I hope until Christmas iris developers will be amazed --- _examples/mvc/hello-world/main.go | 14 +- .../src/web/controllers/todo_controller.go | 4 +- context/context.go | 2 + context/gzip_response_writer.go | 2 +- context/response_recorder.go | 2 +- context/response_writer.go | 2 +- mvc/README.md | 4 +- mvc/controller.go | 134 ++++++++++++------ mvc/controller_handle_test.go | 10 +- mvc/controller_test.go | 39 ++--- mvc/di/di.go | 16 +-- mvc/di/reflect.go | 6 + mvc/di/struct.go | 11 ++ mvc/di/values.go | 30 +++- mvc/engine.go | 22 +-- mvc/func_result_test.go | 4 +- mvc/ideas/1/main.go | 14 ++ mvc/mvc.go | 25 ++-- mvc/session_controller.go | 8 +- 19 files changed, 232 insertions(+), 117 deletions(-) diff --git a/_examples/mvc/hello-world/main.go b/_examples/mvc/hello-world/main.go index ca8550beb9..fdac99cb62 100644 --- a/_examples/mvc/hello-world/main.go +++ b/_examples/mvc/hello-world/main.go @@ -80,22 +80,22 @@ func (c *ExampleController) GetHello() interface{} { return map[string]string{"message": "Hello Iris!"} } -// BeforeActivate called once, before the controller adapted to the main application +// BeforeActivation called once, before the controller adapted to the main application // and of course before the server ran. // After version 9 you can also add custom routes for a specific controller's methods. // Here you can register custom method's handlers // use the standard router with `ca.Router` to do something that you can do without mvc as well, // and add dependencies that will be binded to a controller's fields or method function's input arguments. -func (c *ExampleController) BeforeActivate(ca *mvc.ControllerActivator) { +func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { anyMiddlewareHere := func(ctx iris.Context) { ctx.Application().Logger().Warnf("Inside /custom_path") ctx.Next() } - ca.Handle("GET", "/custom_path", "CustomHandlerWithoutFollowingTheNamingGuide", anyMiddlewareHere) + b.Handle("GET", "/custom_path", "CustomHandlerWithoutFollowingTheNamingGuide", anyMiddlewareHere) // or even add a global middleware based on this controller's router, // which in this example is the root "/": - // ca.Router.Use(myMiddleware) + // b.Router().Use(myMiddleware) } // CustomHandlerWithoutFollowingTheNamingGuide serves @@ -140,11 +140,13 @@ func (c *ExampleController) Any() {} -func (c *ExampleController) BeforeActivate(ca *mvc.ControllerActivator) { +func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { // 1 -> the HTTP Method // 2 -> the route's path // 3 -> this controller's method name that should be handler for that route. - ca.Handle("GET", "/mypath/{param}", "DoIt", optionalMiddlewareHere...) + b.Handle("GET", "/mypath/{param}", "DoIt", optionalMiddlewareHere...) } +func (c *ExampleController) AfterActivation(a mvc.AfterActivation) + */ diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go index 5536e0fdae..744eb5224d 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -16,11 +16,11 @@ type TodoController struct { Session *sessions.Session } -// BeforeActivate called once before the server ran, and before +// BeforeActivation called once before the server ran, and before // the routes and dependency binder builded. // You can bind custom things to the controller, add new methods, add middleware, // add dependencies to the struct or the method(s) and more. -func (c *TodoController) BeforeActivate(ca *mvc.ControllerActivator) { +func (c *TodoController) BeforeActivation(ca *mvc.ControllerActivator) { // this could be binded to a controller's function input argument // if any, or struct field if any: ca.Dependencies.Add(func(ctx iris.Context) todo.Item { diff --git a/context/context.go b/context/context.go index c9794d09d5..498ae57c73 100644 --- a/context/context.go +++ b/context/context.go @@ -821,6 +821,8 @@ type Context interface { String() string } +var _ Context = (*context)(nil) + // Next calls all the next handler from the handlers chain, // it should be used inside a middleware. func Next(ctx Context) { diff --git a/context/gzip_response_writer.go b/context/gzip_response_writer.go index f9aa234118..05ee0c657c 100644 --- a/context/gzip_response_writer.go +++ b/context/gzip_response_writer.go @@ -83,7 +83,7 @@ type GzipResponseWriter struct { disabled bool } -var _ ResponseWriter = &GzipResponseWriter{} +var _ ResponseWriter = (*GzipResponseWriter)(nil) // BeginGzipResponse accepts a ResponseWriter // and prepares the new gzip response writer. diff --git a/context/response_recorder.go b/context/response_recorder.go index 76e7b777fd..c469dc255e 100644 --- a/context/response_recorder.go +++ b/context/response_recorder.go @@ -39,7 +39,7 @@ type ResponseRecorder struct { headers http.Header } -var _ ResponseWriter = &ResponseRecorder{} +var _ ResponseWriter = (*ResponseRecorder)(nil) // Naive returns the simple, underline and original http.ResponseWriter // that backends this response writer. diff --git a/context/response_writer.go b/context/response_writer.go index 04875723b5..baab5d226d 100644 --- a/context/response_writer.go +++ b/context/response_writer.go @@ -115,7 +115,7 @@ type responseWriter struct { beforeFlush func() } -var _ ResponseWriter = &responseWriter{} +var _ ResponseWriter = (*responseWriter)(nil) const ( defaultStatusCode = http.StatusOK diff --git a/mvc/README.md b/mvc/README.md index 51de01be15..06001befa9 100644 --- a/mvc/README.md +++ b/mvc/README.md @@ -114,7 +114,7 @@ m.Controller(sub, &myController{service: myService}) ``` ```go -NewEngine().Controller(sub.Party("/subsub"), new(myController), func(ca *ControllerActivator) { - ca.Dependencies.Add(myService) +NewEngine().Controller(sub.Party("/subsub"), new(myController), func(b mvc.BeforeActivation) { + b.Dependencies().Add(myService) }) ``` \ No newline at end of file diff --git a/mvc/controller.go b/mvc/controller.go index cba69d7879..d36b40320a 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -21,29 +21,50 @@ type BaseController interface { EndRequest(context.Context) } +type shared interface { + Name() string + Router() router.Party + Handle(method, path, funcName string, middleware ...context.Handler) *router.Route +} + +type BeforeActivation interface { + shared + Dependencies() *di.Values +} + +type AfterActivation interface { + shared + DependenciesReadOnly() di.ValuesReadOnly + IsRequestScoped() bool +} + +var ( + _ BeforeActivation = (*ControllerActivator)(nil) + _ AfterActivation = (*ControllerActivator)(nil) +) + // ControllerActivator returns a new controller type info description. // Its functionality can be overriden by the end-dev. type ControllerActivator struct { - // the router is used on the `Activate` and can be used by end-dev on the `BeforeActivate` - // to register any custom controller's functions as handlers but we will need it here - // in order to not create a new type like `ActivationPayload` for the `BeforeActivate`. - Router router.Party + // the router is used on the `Activate` and can be used by end-dev on the `BeforeActivation` + // to register any custom controller's methods as handlers. + router router.Party // initRef BaseController // the BaseController as it's passed from the end-dev. Value reflect.Value // the BaseController's Value. Type reflect.Type // raw type of the BaseController (initRef). // FullName it's the last package path segment + "." + the Name. // i.e: if login-example/user/controller.go, the FullName is "user.Controller". - FullName string + fullName string // the methods names that is already binded to a handler, - // the BeginRequest, EndRequest and BeforeActivate are reserved by the internal implementation. + // the BeginRequest, EndRequest and BeforeActivation are reserved by the internal implementation. reservedMethods []string // the bindings that comes from the Engine and the controller's filled fields if any. // Can be binded to the the new controller's fields and method that is fired // on incoming requests. - Dependencies *di.D + dependencies di.Values // on activate. injector *di.StructInjector @@ -59,7 +80,7 @@ func getNameOf(typ reflect.Type) string { return fullname } -func newControllerActivator(router router.Party, controller interface{}, d *di.D) *ControllerActivator { +func newControllerActivator(router router.Party, controller interface{}, dependencies di.Values) *ControllerActivator { var ( val = reflect.ValueOf(controller) typ = val.Type() @@ -68,13 +89,17 @@ func newControllerActivator(router router.Party, controller interface{}, d *di.D fullName = getNameOf(typ) ) + // add the manual filled fields to the dependencies. + filledFieldValues := di.LookupNonZeroFieldsValues(val) + dependencies.AddValue(filledFieldValues...) + c := &ControllerActivator{ // give access to the Router to the end-devs if they need it for some reason, // i.e register done handlers. - Router: router, + router: router, Value: val, Type: typ, - FullName: fullName, + fullName: fullName, // set some methods that end-dev cann't use accidentally // to register a route via the `Handle`, // all available exported and compatible methods @@ -85,27 +110,14 @@ func newControllerActivator(router router.Party, controller interface{}, d *di.D // TODO: now that BaseController is totally optionally // we have to check if BeginRequest and EndRequest should be here. reservedMethods: whatReservedMethods(typ), - Dependencies: d, - } - - filledFieldValues := di.LookupNonZeroFieldsValues(val) - c.Dependencies.AddValue(filledFieldValues...) - - if len(filledFieldValues) == di.IndirectType(typ).NumField() { - // all fields are filled by the end-developer, - // the controller doesn't contain any other field, not any dynamic binding as well. - // Therefore we don't need to create a new controller each time. - // Set the c.injector now instead on the first `Handle` and set it to invalid state - // in order to `buildControllerHandler` ignore - // creating new controller value on each incoming request. - c.injector = &di.StructInjector{Valid: false} + dependencies: dependencies, } return c } func whatReservedMethods(typ reflect.Type) []string { - methods := []string{"BeforeActivate"} + methods := []string{"BeforeActivation", "AfterActivation"} if isBaseController(typ) { methods = append(methods, "BeginRequest", "EndRequest") } @@ -113,6 +125,22 @@ func whatReservedMethods(typ reflect.Type) []string { return methods } +func (c *ControllerActivator) Dependencies() *di.Values { + return &c.dependencies +} + +func (c *ControllerActivator) DependenciesReadOnly() di.ValuesReadOnly { + return c.dependencies +} + +func (c *ControllerActivator) Name() string { + return c.fullName +} + +func (c *ControllerActivator) Router() router.Party { + return c.router +} + // IsRequestScoped returns new if each request has its own instance // of the controller and it contains dependencies that are not manually // filled by the struct initialization from the caller. @@ -150,8 +178,8 @@ func (c *ControllerActivator) parseMethod(m reflect.Method) { httpMethod, httpPath, err := parseMethod(m, c.isReservedMethod) if err != nil { if err != errSkip { - err = fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.FullName, m.Name, err) - c.Router.GetReporter().AddErr(err) + err = fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err) + c.router.GetReporter().AddErr(err) } return @@ -197,16 +225,16 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . m, ok := c.Type.MethodByName(funcName) if !ok { err := fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", - funcName, c.FullName) - c.Router.GetReporter().AddErr(err) + funcName, c.fullName) + c.router.GetReporter().AddErr(err) return nil } // parse a route template which contains the parameters organised. - tmpl, err := macro.Parse(path, c.Router.Macros()) + tmpl, err := macro.Parse(path, c.router.Macros()) if err != nil { - err = fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.FullName, funcName, err) - c.Router.GetReporter().AddErr(err) + err = fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.fullName, funcName, err) + c.router.GetReporter().AddErr(err) return nil } @@ -222,11 +250,11 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // end-dev's controller pointer. pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...) // get the function's input arguments' bindings. - funcDependencies := c.Dependencies.Clone() + funcDependencies := c.dependencies.Clone() funcDependencies.AddValue(pathParams...) - // fmt.Printf("for %s | values: %s\n", funcName, funcDependencies.Values) - funcInjector := funcDependencies.Func(m.Func) + // fmt.Printf("for %s | values: %s\n", funcName, funcDependencies) + funcInjector := di.MakeFuncInjector(m.Func, hijacker, typeChecker, funcDependencies...) // fmt.Printf("actual injector's inputs length: %d\n", funcInjector.Length) // the element value, not the pointer, wil lbe used to create a @@ -237,7 +265,7 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // hasStructInjector = c.injector != nil && c.injector.Valid // hasFuncInjector = funcInjector != nil && funcInjector.Valid // because - // the `Handle` can be called from `BeforeActivate` callbacks + // the `Handle` can be called from `BeforeActivation` callbacks // and before activation, the c.injector is nil because // we may not have the dependencies binded yet. But if `c.injector.Valid` // inside the Handelr works because it's set on the `activate()` method. @@ -246,24 +274,48 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // so the user should bind the dependencies needed before the `Handle` // this is a logical flow, so we will choose that one -> if c.injector == nil { - c.injector = c.Dependencies.Struct(c.Value) + // check if manually filled + any dependencies are only static, if so + // and the total struct's fields are equal these static dependencies length + // then we don't need to create a new struct on each request. + // + // We use our custom NumFields here because the std "reflect" package + // checks only for the current struct and not for embedded's exported fields. + totalFieldsLength := di.NumFields(di.IndirectType(c.Type)) + + // first, set these bindings to the passed controller, they will be useless + // if the struct contains any dynamic value because this controller will + // be never fired as it's but we make that in order to get the length of the static + // matched dependencies of the struct. + c.injector = di.MakeStructInjector(c.Value, hijacker, typeChecker, c.dependencies...) + matchedStaticDependenciesLength := c.injector.InjectElemStaticOnly(di.IndirectValue(c.Value)) + if c.injector.Valid { - golog.Debugf("MVC dependencies of '%s':\n%s", c.FullName, c.injector.String()) + golog.Debugf("MVC dependencies of '%s':\n%s", c.fullName, c.injector.String()) + } + + if matchedStaticDependenciesLength == totalFieldsLength { + // all fields are filled by the end-developer or via static dependencies (if context is there then it will be filled by the MakeStructInjector so we don't worry about it), + // the controller doesn't contain any other field neither any dynamic binding as well. + // Therefore we don't need to create a new controller each time. + // Set the c.injector now instead on the first `Handle` and set it to invalid state + // in order to `buildControllerHandler` ignore the + // creation of a new controller value on each incoming request. + c.injector = &di.StructInjector{Valid: false} } } if funcInjector.Valid { - golog.Debugf("MVC dependencies of method '%s.%s':\n%s", c.FullName, funcName, funcInjector.String()) + golog.Debugf("MVC dependencies of method '%s.%s':\n%s", c.fullName, funcName, funcInjector.String()) } handler := buildControllerHandler(m, c.Type, c.Value, c.injector, funcInjector, funcIn) // register the handler now. - route := c.Router.Handle(method, path, append(middleware, handler)...) + route := c.router.Handle(method, path, append(middleware, handler)...) if route != nil { // change the main handler's name in order to respect the controller's and give // a proper debug message. - route.MainHandlerName = fmt.Sprintf("%s.%s", c.FullName, funcName) + route.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName) } return route diff --git a/mvc/controller_handle_test.go b/mvc/controller_handle_test.go index 9be5dd29f6..a373f18c1b 100644 --- a/mvc/controller_handle_test.go +++ b/mvc/controller_handle_test.go @@ -17,11 +17,11 @@ type testControllerHandle struct { reqField string } -func (c *testControllerHandle) BeforeActivate(ca *ControllerActivator) { // BeforeActivate(t *mvc.TController) { - ca.Handle("GET", "/histatic", "HiStatic") - ca.Handle("GET", "/hiservice", "HiService") - ca.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") - ca.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") +func (c *testControllerHandle) BeforeActivation(b BeforeActivation) { // BeforeActivation(t *mvc.TController) { + b.Handle("GET", "/histatic", "HiStatic") + b.Handle("GET", "/hiservice", "HiService") + b.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") + b.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") } func (c *testControllerHandle) BeginRequest(ctx iris.Context) { diff --git a/mvc/controller_test.go b/mvc/controller_test.go index b2c7d75e71..24ed310bd9 100644 --- a/mvc/controller_test.go +++ b/mvc/controller_test.go @@ -275,7 +275,7 @@ func (t *testControllerBindDeep) Get() { t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) } -func TestControllerBind(t *testing.T) { +func TestControllerDependencies(t *testing.T) { app := iris.New() // app.Logger().SetLevel("debug") @@ -421,8 +421,8 @@ type testControllerActivateListener struct { TitlePointer *testBindType } -func (c *testControllerActivateListener) BeforeActivate(ca *ControllerActivator) { - ca.Dependencies.AddOnce(&testBindType{title: "default title"}) +func (c *testControllerActivateListener) BeforeActivation(b BeforeActivation) { + b.Dependencies().AddOnce(&testBindType{title: "default title"}) } func (c *testControllerActivateListener) Get() string { @@ -454,9 +454,27 @@ func TestControllerActivateListener(t *testing.T) { } type testControllerNotCreateNewDueManuallySettingAllFields struct { + T *testing.T + TitlePointer *testBindType } +func (c *testControllerNotCreateNewDueManuallySettingAllFields) AfterActivation(a AfterActivation) { + if n := a.DependenciesReadOnly().Len(); n != 2 { + c.T.Fatalf(`expecting 2 dependency, the 'T' and the 'TitlePointer' that we manually insert + and the fields total length is 2 so it will not create a new controller on each request + however the dependencies are available here + although the struct injector is being ignored when + creating the controller's handlers because we set it to invalidate state at "newControllerActivator" + -- got dependencies length: %d`, n) + } + + if a.IsRequestScoped() { + c.T.Fatalf(`this controller shouldn't be tagged used as request scoped(create new instances on each request), + it doesn't contain any dynamic value or dependencies that should be binded via the iris mvc engine`) + } +} + func (c *testControllerNotCreateNewDueManuallySettingAllFields) Get() string { return c.TitlePointer.title } @@ -464,23 +482,12 @@ func (c *testControllerNotCreateNewDueManuallySettingAllFields) Get() string { func TestControllerNotCreateNewDueManuallySettingAllFields(t *testing.T) { app := iris.New() NewEngine().Controller(app, &testControllerNotCreateNewDueManuallySettingAllFields{ + T: t, TitlePointer: &testBindType{ title: "my title", }, - }, func(ca *ControllerActivator) { - if n := len(ca.Dependencies.Values); n != 1 { - t.Fatalf(`expecting 1 dependency, the 'TitlePointer' which we manually insert - and the fields length is 1 so it will not create a new controller on each request - however the dependencies are available here - although the struct injector is being ignored when - creating the controller's handlers because we set it to invalidate state at "newControllerActivator" - -- got dependencies length: %d`, n) - } + }, func(b BeforeActivation) { - if ca.IsRequestScoped() { - t.Fatalf(`this controller shouldn't be tagged used as request scoped(create new instances on each request), - it doesn't contain any dynamic value or dependencies that should be binded via the iris mvc engine`) - } }) e := httptest.New(t, app) diff --git a/mvc/di/di.go b/mvc/di/di.go index d56bed8604..adc069ac78 100644 --- a/mvc/di/di.go +++ b/mvc/di/di.go @@ -44,19 +44,11 @@ func (d *D) GoodFunc(fn TypeChecker) *D { // Clone returns a new Dependency Injection container, it adopts the // parent's (current "D") hijacker, good func type checker and all dependencies values. func (d *D) Clone() *D { - clone := New() - clone.hijacker = d.hijacker - clone.goodFunc = d.goodFunc - - // copy the current dynamic bindings (func binders) - // and static struct bindings (services) to this new child. - if n := len(d.Values); n > 0 { - values := make(Values, n, n) - copy(values, d.Values) - clone.Values = values + return &D{ + Values: d.Values.Clone(), + hijacker: d.hijacker, + goodFunc: d.goodFunc, } - - return clone } // Struct is being used to return a new injector based on diff --git a/mvc/di/reflect.go b/mvc/di/reflect.go index 0c28bf029e..3b03e6c0f6 100644 --- a/mvc/di/reflect.go +++ b/mvc/di/reflect.go @@ -125,6 +125,12 @@ type field struct { AnyValue reflect.Value } +// NumFields returns the total number of fields, and the embedded, even if the embedded struct is not exported, +// it will check for its exported fields. +func NumFields(elemTyp reflect.Type) int { + return len(lookupFields(elemTyp, nil)) +} + func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { if elemTyp.Kind() != reflect.Struct { return diff --git a/mvc/di/struct.go b/mvc/di/struct.go index 2ee2c07ab3..aaa88f8909 100644 --- a/mvc/di/struct.go +++ b/mvc/di/struct.go @@ -97,6 +97,17 @@ func (s *StructInjector) InjectElem(destElem reflect.Value, ctx ...reflect.Value } } +func (s *StructInjector) InjectElemStaticOnly(destElem reflect.Value) (n int) { + for _, f := range s.fields { + if f.Object.BindType != Static { + continue + } + destElem.FieldByIndex(f.FieldIndex).Set(f.Object.Value) + n++ + } + return +} + func (s *StructInjector) New(ctx ...reflect.Value) reflect.Value { dest := reflect.New(s.elemType) s.InjectElem(dest, ctx...) diff --git a/mvc/di/values.go b/mvc/di/values.go index 64301aee3f..da9342bb5d 100644 --- a/mvc/di/values.go +++ b/mvc/di/values.go @@ -2,12 +2,35 @@ package di import "reflect" +type ValuesReadOnly interface { + // Has returns true if a binder responsible to + // bind and return a type of "typ" is already registered to this controller. + Has(value interface{}) bool + // Len returns the length of the values. + Len() int +} + type Values []reflect.Value func NewValues() Values { return Values{} } +// Clone returns a copy of the current values. +func (bv Values) Clone() Values { + if n := len(bv); n > 0 { + values := make(Values, n, n) + copy(values, bv) + return values + } + + return NewValues() +} + +func (bv Values) Len() int { + return len(bv) +} + // Add binds values to this controller, if you want to share // binding values between controllers use the Engine's `Bind` function instead. func (bv *Values) Add(values ...interface{}) { @@ -57,13 +80,12 @@ func (bv *Values) remove(typ reflect.Type, n int) (ok bool) { // Has returns true if a binder responsible to // bind and return a type of "typ" is already registered to this controller. -func (bv *Values) Has(value interface{}) bool { +func (bv Values) Has(value interface{}) bool { return bv.valueTypeExists(reflect.TypeOf(value)) } -func (bv *Values) valueTypeExists(typ reflect.Type) bool { - input := *bv - for _, in := range input { +func (bv Values) valueTypeExists(typ reflect.Type) bool { + for _, in := range bv { if equalTypes(in.Type(), typ) { return true } diff --git a/mvc/engine.go b/mvc/engine.go index e835fa4a4c..269e203222 100644 --- a/mvc/engine.go +++ b/mvc/engine.go @@ -20,7 +20,7 @@ import ( // // For a more high-level structure please take a look at the "mvc.go#Application". type Engine struct { - Dependencies *di.D + Dependencies di.Values } // NewEngine returns a new engine, a container for dependencies and a factory @@ -28,7 +28,7 @@ type Engine struct { // Please take a look at the structure's documentation for more information. func NewEngine() *Engine { return &Engine{ - Dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker), + Dependencies: di.NewValues(), } } @@ -46,7 +46,7 @@ func (e *Engine) Clone() *Engine { // It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application, // as middleware or as simple route handler or subdomain's handler. func (e *Engine) Handler(handler interface{}) context.Handler { - h, err := MakeHandler(handler, e.Dependencies.Values...) + h, err := MakeHandler(handler, e.Dependencies.Clone()...) if err != nil { golog.Errorf("mvc handler: %v", err) } @@ -84,8 +84,8 @@ func (e *Engine) Handler(handler interface{}) context.Handler { // where Get is an HTTP Method func. // // Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc. -func (e *Engine) Controller(router router.Party, controller interface{}, beforeActivate ...func(*ControllerActivator)) { - ca := newControllerActivator(router, controller, e.Dependencies) +func (e *Engine) Controller(router router.Party, controller interface{}, beforeActivate ...func(BeforeActivation)) { + ca := newControllerActivator(router, controller, e.Dependencies.Clone()) // give a priority to the "beforeActivate" // callbacks, if any. @@ -93,13 +93,19 @@ func (e *Engine) Controller(router router.Party, controller interface{}, beforeA cb(ca) } - // check if controller has an "BeforeActivate" function + // check if controller has an "BeforeActivation" function // which accepts the controller activator and call it. if activateListener, ok := controller.(interface { - BeforeActivate(*ControllerActivator) + BeforeActivation(BeforeActivation) }); ok { - activateListener.BeforeActivate(ca) + activateListener.BeforeActivation(ca) } ca.activate() + + if afterActivateListener, ok := controller.(interface { + AfterActivation(AfterActivation) + }); ok { + afterActivateListener.AfterActivation(ca) + } } diff --git a/mvc/func_result_test.go b/mvc/func_result_test.go index 7de6c6887d..424f6734eb 100644 --- a/mvc/func_result_test.go +++ b/mvc/func_result_test.go @@ -264,8 +264,8 @@ func (t *testControllerViewResultRespectCtxViewData) Get() Result { func TestControllerViewResultRespectCtxViewData(t *testing.T) { app := iris.New() - NewEngine().Controller(app, new(testControllerViewResultRespectCtxViewData), func(ca *ControllerActivator) { - ca.Dependencies.Add(t) + NewEngine().Controller(app, new(testControllerViewResultRespectCtxViewData), func(b BeforeActivation) { + b.Dependencies().Add(t) }) e := httptest.New(t, app) diff --git a/mvc/ideas/1/main.go b/mvc/ideas/1/main.go index 88c120d1c8..93e31a1e8e 100644 --- a/mvc/ideas/1/main.go +++ b/mvc/ideas/1/main.go @@ -69,6 +69,16 @@ type TodoController struct { Session *sessions.Session } +func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { + b.Handle("GET", "/custom", "Custom") +} + +func (c *TodoController) AfterActivation(b mvc.BeforeActivation) { + if !b.IsRequestScoped() { + panic("TodoController should be request scoped, we have a 'Session' which depends on the context.") + } +} + func (c *TodoController) Get() string { count := c.Session.Increment("count", 1) @@ -77,6 +87,10 @@ func (c *TodoController) Get() string { return body } +func (c *TodoController) Custom() string { + return "custom" +} + type TodoSubController struct { Session *sessions.Session } diff --git a/mvc/mvc.go b/mvc/mvc.go index 98241d571e..08744f15d4 100644 --- a/mvc/mvc.go +++ b/mvc/mvc.go @@ -25,13 +25,13 @@ func newApp(engine *Engine, subRouter router.Party) *Application { } } -// New returns a new mvc Application based on a "subRouter". +// New returns a new mvc Application based on a "party". // Application creates a new engine which is responsible for binding the dependencies // and creating and activating the app's controller(s). // -// Example: `New(app.Party("/todo"))`. -func New(subRouter router.Party) *Application { - return newApp(NewEngine(), subRouter) +// Example: `New(app.Party("/todo"))` or `New(app)` as it's the same as `New(app.Party("/"))`. +func New(party router.Party) *Application { + return newApp(NewEngine(), party) } // Configure can be used to pass one or more functions that accept this @@ -53,7 +53,7 @@ func (app *Application) Configure(configurators ...func(*Application)) *Applicat // controller's methods, if matching. // // The dependencies can be changed per-controller as well via a `beforeActivate` -// on the `Register` method or when the controller has the `BeforeActivate(c *ControllerActivator)` +// on the `Register` method or when the controller has the `BeforeActivation(c *ControllerActivator)` // method defined. // // It returns this Application. @@ -75,16 +75,17 @@ func (app *Application) AddDependencies(values ...interface{}) *Application { // It returns this Application. // // Example: `.Register(new(TodoController))`. -func (app *Application) Register(controller interface{}, beforeActivate ...func(*ControllerActivator)) *Application { +func (app *Application) Register(controller interface{}, beforeActivate ...func(BeforeActivation)) *Application { app.Engine.Controller(app.Router, controller, beforeActivate...) return app } -// NewChild creates and returns a new Application which will be adapted -// to the "subRouter", it adopts -// the dependencies bindings from the parent(current) one. +// NewChild creates and returns a new MVC Application which will be adapted +// to the "party", it adopts +// the parent's (current) dependencies, the "party" may be +// a totally new router or a child path one via the parent's `.Router.Party`. // -// Example: `.NewChild(irisApp.Party("/sub")).Register(new(TodoSubController))`. -func (app *Application) NewChild(subRouter router.Party) *Application { - return newApp(app.Engine.Clone(), subRouter) +// Example: `.NewChild(irisApp.Party("/path")).Register(new(TodoSubController))`. +func (app *Application) NewChild(party router.Party) *Application { + return newApp(app.Engine.Clone(), party) } diff --git a/mvc/session_controller.go b/mvc/session_controller.go index 39bc9b0aa0..e5670a001e 100644 --- a/mvc/session_controller.go +++ b/mvc/session_controller.go @@ -15,13 +15,13 @@ type SessionController struct { Session *sessions.Session } -// BeforeActivate called, once per application lifecycle NOT request, +// BeforeActivation called, once per application lifecycle NOT request, // every single time the dev registers a specific SessionController-based controller. // It makes sure that its "Manager" field is filled // even if the caller didn't provide any sessions manager via the `app.Controller` function. -func (s *SessionController) BeforeActivate(ca *ControllerActivator) { - if didntBindManually := ca.Dependencies.AddOnce(defaultSessionManager); didntBindManually { - ca.Router.GetReporter().Add( +func (s *SessionController) BeforeActivation(b BeforeActivation) { + if didntBindManually := b.Dependencies().AddOnce(defaultSessionManager); didntBindManually { + b.Router().GetReporter().Add( `MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, therefore this controller is using the default sessions manager instead. Please refer to the documentation to learn how you can provide the session manager`) From 5b26781f87bdf397ae8feadba3f2255388105501 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 18 Dec 2017 02:24:53 +0200 Subject: [PATCH 29/79] fix mvc/ideas/1/main.go->AfterActivation --- _examples/mvc/hello-world/main.go | 4 +++- mvc/ideas/1/main.go | 4 ++-- mvc/mvc.go | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/_examples/mvc/hello-world/main.go b/_examples/mvc/hello-world/main.go index fdac99cb62..e74003673d 100644 --- a/_examples/mvc/hello-world/main.go +++ b/_examples/mvc/hello-world/main.go @@ -147,6 +147,8 @@ func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { b.Handle("GET", "/mypath/{param}", "DoIt", optionalMiddlewareHere...) } -func (c *ExampleController) AfterActivation(a mvc.AfterActivation) +// After activation, all dependencies are set-ed - so read only access on them +// but still possible to add custom controller or simple standard handlers. +func (c *ExampleController) AfterActivation(a mvc.AfterActivation) {} */ diff --git a/mvc/ideas/1/main.go b/mvc/ideas/1/main.go index 93e31a1e8e..329f5c66ff 100644 --- a/mvc/ideas/1/main.go +++ b/mvc/ideas/1/main.go @@ -73,8 +73,8 @@ func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { b.Handle("GET", "/custom", "Custom") } -func (c *TodoController) AfterActivation(b mvc.BeforeActivation) { - if !b.IsRequestScoped() { +func (c *TodoController) AfterActivation(a mvc.AfterActivation) { + if !a.IsRequestScoped() { panic("TodoController should be request scoped, we have a 'Session' which depends on the context.") } } diff --git a/mvc/mvc.go b/mvc/mvc.go index 08744f15d4..44abd8645f 100644 --- a/mvc/mvc.go +++ b/mvc/mvc.go @@ -53,7 +53,7 @@ func (app *Application) Configure(configurators ...func(*Application)) *Applicat // controller's methods, if matching. // // The dependencies can be changed per-controller as well via a `beforeActivate` -// on the `Register` method or when the controller has the `BeforeActivation(c *ControllerActivator)` +// on the `Register` method or when the controller has the `BeforeActivation(b BeforeActivation)` // method defined. // // It returns this Application. From 9ae0343db40225246b65e00d211d85d40b6e0703 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 18 Dec 2017 06:47:05 +0200 Subject: [PATCH 30/79] fix typo --- _examples/tutorial/vuejs-todo-mvc/src/todo/service.go | 2 +- context/context.go | 4 ++-- core/router/router.go | 2 +- mvc/controller.go | 2 +- mvc/di/di.go | 4 ++-- mvc/di/func.go | 2 +- mvc/mvc.go | 2 +- mvc/session.go | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go index 3a8f75a6a9..32e53a2c4a 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go @@ -47,7 +47,7 @@ func (s *MemoryService) Save(newItem Item) error { newItem.ID = s.getLatestID() + 1 } - // full replace here for the shake of simplicy) + // full replace here for the shake of simplicity) s.items[newItem.ID] = newItem return nil } diff --git a/context/context.go b/context/context.go index 498ae57c73..8abf8e7f59 100644 --- a/context/context.go +++ b/context/context.go @@ -813,7 +813,7 @@ type Context interface { // String returns the string representation of this request. // Each context has a unique string representation. - // It can be used for simple debugging scenarions, i.e print context as string. + // It can be used for simple debugging scenarios, i.e print context as string. // // What it returns? A number which declares the length of the // total `String` calls per executable application, followed @@ -2748,7 +2748,7 @@ func LastCapturedContextID() uint64 { // String returns the string representation of this request. // Each context has a unique string representation. -// It can be used for simple debugging scenarions, i.e print context as string. +// It can be used for simple debugging scenarios, i.e print context as string. // // What it returns? A number which declares the length of the // total `String` calls per executable application, followed diff --git a/core/router/router.go b/core/router/router.go index 05c5af1136..805c0df1b0 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -83,7 +83,7 @@ func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHan // be aware to change the global variables of 'ParamStart' and 'ParamWildcardStart'. // can be used to implement a custom proxy or // a custom router which should work with raw ResponseWriter, *Request -// instead of the Context(which agaiin, can be retrieved by the Cramework's context pool). +// instead of the Context(which again, can be retrieved by the Framework's context pool). // // Note: Downgrade will by-pass the Wrapper, the caller is responsible for everything. // Downgrade is thread-safe. diff --git a/mvc/controller.go b/mvc/controller.go index d36b40320a..9f95fed969 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -44,7 +44,7 @@ var ( ) // ControllerActivator returns a new controller type info description. -// Its functionality can be overriden by the end-dev. +// Its functionality can be overridden by the end-dev. type ControllerActivator struct { // the router is used on the `Activate` and can be used by end-dev on the `BeforeActivation` // to register any custom controller's methods as handlers. diff --git a/mvc/di/di.go b/mvc/di/di.go index adc069ac78..f0a7ff1bb6 100644 --- a/mvc/di/di.go +++ b/mvc/di/di.go @@ -28,14 +28,14 @@ func New() *D { return &D{} } -// Hijack sets a hijacker function, read the `Hijacker` type for more explaination. +// Hijack sets a hijacker function, read the `Hijacker` type for more explanation. func (d *D) Hijack(fn Hijacker) *D { d.hijacker = fn return d } // GoodFunc sets a type checker for a valid function that can be binded, -// read the `TypeChecker` type for more explaination. +// read the `TypeChecker` type for more explanation. func (d *D) GoodFunc(fn TypeChecker) *D { d.goodFunc = fn return d diff --git a/mvc/di/func.go b/mvc/di/func.go index e80d59cf20..629c8b0159 100644 --- a/mvc/di/func.go +++ b/mvc/di/func.go @@ -13,7 +13,7 @@ type ( FuncInjector struct { // the original function, is being used - // only the .Call, which is refering to the same function, always. + // only the .Call, which is referring to the same function, always. fn reflect.Value inputs []*targetFuncInput diff --git a/mvc/mvc.go b/mvc/mvc.go index 44abd8645f..4f127df349 100644 --- a/mvc/mvc.go +++ b/mvc/mvc.go @@ -3,7 +3,7 @@ package mvc import "github.com/kataras/iris/core/router" // Application is the high-level compoment of the "mvc" package. -// It's the API that you will be using to register controllers among wih their +// It's the API that you will be using to register controllers among with their // dependencies that your controllers may expecting. // It contains the Router(iris.Party) in order to be able to register // template layout, middleware, done handlers as you used with the diff --git a/mvc/session.go b/mvc/session.go index b09c764fd2..c1e009649a 100644 --- a/mvc/session.go +++ b/mvc/session.go @@ -9,7 +9,7 @@ import ( // a different folder like "bindings" // so it will be used as .Bind(bindings.Session(manager)) // or let it here but change the rest of the binding names as well -// because they are not "binders", their result are binders to be percise. +// because they are not "binders", their result are binders to be precise. func Session(sess *sessions.Sessions) func(context.Context) *sessions.Session { return func(ctx context.Context) *sessions.Session { return sess.Start(ctx) From b5a87acff61afe0d5d64af5b0dfd19a5b8b8c158 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 19 Dec 2017 23:40:42 +0200 Subject: [PATCH 31/79] ok make it cleaner, it's working well and blazing fast but I have to do a lot cleaning and commenting and docs as well before push it to master --- hope at christmas day, also thinking some internal ideas - the whole code is not ready to be readen by a third person yet. --- _benchmarks/iris-mvc/main.go | 4 +- mvc/controller.go | 280 +++++++++++------------------------ mvc/controller_test.go | 4 +- mvc/di/func.go | 9 +- mvc/di/object.go | 9 ++ mvc/di/reflect.go | 41 +++-- mvc/di/struct.go | 132 ++++++++++++----- mvc/di/values.go | 28 ++-- mvc/engine.go | 4 +- mvc/func_result.go | 126 +++++++++++----- mvc/ideas/1/main.go | 5 +- 11 files changed, 336 insertions(+), 306 deletions(-) diff --git a/_benchmarks/iris-mvc/main.go b/_benchmarks/iris-mvc/main.go index 77743dff0a..71719eb2fc 100644 --- a/_benchmarks/iris-mvc/main.go +++ b/_benchmarks/iris-mvc/main.go @@ -13,7 +13,9 @@ import ( func main() { app := iris.New() - mvc.New(app.Party("/api/values/{id}")).Register(new(controllers.ValuesController)) + mvc.New(app.Party("/api/values/{id}")). + Register(new(controllers.ValuesController)) + app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) } diff --git a/mvc/controller.go b/mvc/controller.go index 9f95fed969..e578c28bb2 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -35,7 +35,7 @@ type BeforeActivation interface { type AfterActivation interface { shared DependenciesReadOnly() di.ValuesReadOnly - IsRequestScoped() bool + Singleton() bool } var ( @@ -85,14 +85,10 @@ func newControllerActivator(router router.Party, controller interface{}, depende val = reflect.ValueOf(controller) typ = val.Type() - // the full name of the controller, it's its type including the package path. + // the full name of the controller: its type including the package path. fullName = getNameOf(typ) ) - // add the manual filled fields to the dependencies. - filledFieldValues := di.LookupNonZeroFieldsValues(val) - dependencies.AddValue(filledFieldValues...) - c := &ControllerActivator{ // give access to the Router to the end-devs if they need it for some reason, // i.e register done handlers. @@ -141,26 +137,16 @@ func (c *ControllerActivator) Router() router.Party { return c.router } -// IsRequestScoped returns new if each request has its own instance -// of the controller and it contains dependencies that are not manually -// filled by the struct initialization from the caller. -func (c *ControllerActivator) IsRequestScoped() bool { - // if the c.injector == nil means that is not seted to invalidate state, - // so it contains more fields that are filled by the end-dev. - // This "strange" check happens because the `IsRequestScoped` may - // called before the controller activation complete its task (see Handle: if c.injector == nil). - if c.injector == nil { // is nil so it contains more fields, maybe request-scoped or dependencies. - return true - } - if c.injector.Valid { - // if injector is not nil: - // if it is !Valid then all fields are manually filled by the end-dev (see `newControllerActivator`). - // if it is Valid then it's filled on the first `Handle` - // and it has more than one valid dependency from the registered values. - return true +// Singleton returns new if all incoming clients' requests +// have the same controller instance. +// This is done automatically by iris to reduce the creation +// of a new controller on each request, if the controller doesn't contain +// any unexported fields and all fields are services-like, static. +func (c *ControllerActivator) Singleton() bool { + if c.injector == nil { + panic("MVC: IsRequestScoped used on an invalid state the API gives access to it only `AfterActivation`, report this as bug") } - // it's not nil and it's !Valid. - return false + return c.injector.State == di.Singleton } // checks if a method is already registered. @@ -191,21 +177,16 @@ func (c *ControllerActivator) parseMethod(m reflect.Method) { // register all available, exported methods to handlers if possible. func (c *ControllerActivator) parseMethods() { n := c.Type.NumMethod() - // wg := &sync.WaitGroup{} - // wg.Add(n) for i := 0; i < n; i++ { m := c.Type.Method(i) c.parseMethod(m) } - // wg.Wait() } func (c *ControllerActivator) activate() { c.parseMethods() } -var emptyIn = []reflect.Value{} - // Handle registers a route based on a http method, the route's path // and a function name that belongs to the controller, it accepts // a forth, optionally, variadic parameter which is the before handlers. @@ -221,45 +202,6 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . return nil } - // get the method from the controller type. - m, ok := c.Type.MethodByName(funcName) - if !ok { - err := fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", - funcName, c.fullName) - c.router.GetReporter().AddErr(err) - return nil - } - - // parse a route template which contains the parameters organised. - tmpl, err := macro.Parse(path, c.router.Macros()) - if err != nil { - err = fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.fullName, funcName, err) - c.router.GetReporter().AddErr(err) - return nil - } - - // add this as a reserved method name in order to - // be sure that the same func will not be registered again, even if a custom .Handle later on. - c.reservedMethods = append(c.reservedMethods, funcName) - - // get the function's input. - funcIn := getInputArgsFromFunc(m.Type) - - // get the path parameters bindings from the template, - // use the function's input except the receiver which is the - // end-dev's controller pointer. - pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...) - // get the function's input arguments' bindings. - funcDependencies := c.dependencies.Clone() - funcDependencies.AddValue(pathParams...) - - // fmt.Printf("for %s | values: %s\n", funcName, funcDependencies) - funcInjector := di.MakeFuncInjector(m.Func, hijacker, typeChecker, funcDependencies...) - // fmt.Printf("actual injector's inputs length: %d\n", funcInjector.Length) - - // the element value, not the pointer, wil lbe used to create a - // new controller on each incoming request. - // Remember: // we cannot simply do that and expect to work: // hasStructInjector = c.injector != nil && c.injector.Valid @@ -274,41 +216,44 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // so the user should bind the dependencies needed before the `Handle` // this is a logical flow, so we will choose that one -> if c.injector == nil { - // check if manually filled + any dependencies are only static, if so - // and the total struct's fields are equal these static dependencies length - // then we don't need to create a new struct on each request. - // - // We use our custom NumFields here because the std "reflect" package - // checks only for the current struct and not for embedded's exported fields. - totalFieldsLength := di.NumFields(di.IndirectType(c.Type)) - // first, set these bindings to the passed controller, they will be useless // if the struct contains any dynamic value because this controller will // be never fired as it's but we make that in order to get the length of the static // matched dependencies of the struct. c.injector = di.MakeStructInjector(c.Value, hijacker, typeChecker, c.dependencies...) - matchedStaticDependenciesLength := c.injector.InjectElemStaticOnly(di.IndirectValue(c.Value)) - - if c.injector.Valid { + if c.injector.HasFields { golog.Debugf("MVC dependencies of '%s':\n%s", c.fullName, c.injector.String()) } + } - if matchedStaticDependenciesLength == totalFieldsLength { - // all fields are filled by the end-developer or via static dependencies (if context is there then it will be filled by the MakeStructInjector so we don't worry about it), - // the controller doesn't contain any other field neither any dynamic binding as well. - // Therefore we don't need to create a new controller each time. - // Set the c.injector now instead on the first `Handle` and set it to invalid state - // in order to `buildControllerHandler` ignore the - // creation of a new controller value on each incoming request. - c.injector = &di.StructInjector{Valid: false} - } + // get the method from the controller type. + m, ok := c.Type.MethodByName(funcName) + if !ok { + err := fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", + funcName, c.fullName) + c.router.GetReporter().AddErr(err) + return nil } - if funcInjector.Valid { - golog.Debugf("MVC dependencies of method '%s.%s':\n%s", c.fullName, funcName, funcInjector.String()) + // parse a route template which contains the parameters organised. + tmpl, err := macro.Parse(path, c.router.Macros()) + if err != nil { + err = fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.fullName, funcName, err) + c.router.GetReporter().AddErr(err) + return nil } - handler := buildControllerHandler(m, c.Type, c.Value, c.injector, funcInjector, funcIn) + // get the function's input. + funcIn := getInputArgsFromFunc(m.Type) + // get the path parameters bindings from the template, + // use the function's input except the receiver which is the + // end-dev's controller pointer. + pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...) + // get the function's input arguments' bindings. + funcDependencies := c.dependencies.Clone() + funcDependencies.AddValues(pathParams...) + + handler := c.handlerOf(m, funcDependencies) // register the handler now. route := c.router.Handle(method, path, append(middleware, handler)...) @@ -316,98 +261,55 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // change the main handler's name in order to respect the controller's and give // a proper debug message. route.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName) + + // add this as a reserved method name in order to + // be sure that the same func will not be registered again, + // even if a custom .Handle later on. + c.reservedMethods = append(c.reservedMethods, funcName) } return route } -// buildControllerHandler has many many dublications but we do that to achieve the best -// performance possible, to use the information we know -// and calculate what is needed and what not in serve-time. -func buildControllerHandler(m reflect.Method, typ reflect.Type, initRef reflect.Value, structInjector *di.StructInjector, funcInjector *di.FuncInjector, funcIn []reflect.Type) context.Handler { - var ( - hasStructInjector = structInjector != nil && structInjector.Valid - hasFuncInjector = funcInjector != nil && funcInjector.Valid - - implementsBase = isBaseController(typ) - // we will make use of 'n' to make a slice of reflect.Value - // to pass into if the function has input arguments that - // are will being filled by the funcDependencies. - n = len(funcIn) - - elemTyp = di.IndirectType(typ) - ) - - // if it doesn't implement the base controller, - // it may have struct injector and/or func injector. - if !implementsBase { - - if !hasStructInjector { - // if the controller doesn't have a struct injector - // and the controller's fields are empty or all set-ed by the end-dev - // then we don't need a new controller instance, we use the passed controller instance. - - if !hasFuncInjector { - return func(ctx context.Context) { - DispatchFuncResult(ctx, initRef.Method(m.Index).Call(emptyIn)) - } - } +var emptyIn = []reflect.Value{} - return func(ctx context.Context) { - in := make([]reflect.Value, n, n) - in[0] = initRef - funcInjector.Inject(&in, reflect.ValueOf(ctx)) - if ctx.IsStopped() { - return - } +func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []reflect.Value) context.Handler { + // fmt.Printf("for %s | values: %s\n", funcName, funcDependencies) + funcInjector := di.MakeFuncInjector(m.Func, hijacker, typeChecker, funcDependencies...) + // fmt.Printf("actual injector's inputs length: %d\n", funcInjector.Length) + if funcInjector.Valid { + golog.Debugf("MVC dependencies of method '%s.%s':\n%s", c.fullName, m.Name, funcInjector.String()) + } - DispatchFuncResult(ctx, m.Func.Call(in)) - } - } + var ( + implementsBase = isBaseController(c.Type) + hasBindableFields = c.injector.CanInject + hasBindableFuncInputs = funcInjector.Valid - // it has struct injector for sure and maybe a func injector. - if !hasFuncInjector { - return func(ctx context.Context) { - ctrl := reflect.New(elemTyp) - ctxValue := reflect.ValueOf(ctx) - elem := ctrl.Elem() - structInjector.InjectElem(elem, ctxValue) - if ctx.IsStopped() { - return - } - - DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) - } - } + call = m.Func.Call + ) - // has struct injector and func injector. + if !implementsBase && !hasBindableFields && !hasBindableFuncInputs { return func(ctx context.Context) { - ctrl := reflect.New(elemTyp) - ctxValue := reflect.ValueOf(ctx) - - elem := ctrl.Elem() - structInjector.InjectElem(elem, ctxValue) - if ctx.IsStopped() { - return - } - - in := make([]reflect.Value, n, n) - in[0] = ctrl - funcInjector.Inject(&in, ctxValue) - if ctx.IsStopped() { - return - } - - DispatchFuncResult(ctx, m.Func.Call(in)) + DispatchFuncResult(ctx, call(c.injector.NewAsSlice())) } - } - // if implements the base controller, - // it may have struct injector and func injector as well. + n := m.Type.NumIn() return func(ctx context.Context) { - ctrl := reflect.New(elemTyp) + var ( + ctrl = c.injector.New() + ctxValue reflect.Value + ) + + // inject struct fields first before the BeginRequest and EndRequest, if any, + // in order to be able to have access there. + if hasBindableFields { + ctxValue = reflect.ValueOf(ctx) + c.injector.InjectElem(ctrl.Elem(), ctxValue) + } + // check if has BeginRequest & EndRequest, before try to bind the method's inputs. if implementsBase { // the Interface(). is faster than MethodByName or pre-selected methods. b := ctrl.Interface().(BaseController) @@ -422,36 +324,20 @@ func buildControllerHandler(m reflect.Method, typ reflect.Type, initRef reflect. defer b.EndRequest(ctx) } - if !hasStructInjector && !hasFuncInjector { - DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) - } else { - ctxValue := reflect.ValueOf(ctx) - if hasStructInjector { - elem := ctrl.Elem() - structInjector.InjectElem(elem, ctxValue) - if ctx.IsStopped() { - return - } - - // we do this in order to reduce in := make... - // if not func input binders, we execute the handler with empty input args. - if !hasFuncInjector { - DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) - } - } - // otherwise, it has one or more valid input binders, - // make the input and call the func using those. - if hasFuncInjector { - in := make([]reflect.Value, n, n) - in[0] = ctrl - funcInjector.Inject(&in, ctxValue) - if ctx.IsStopped() { - return - } - - DispatchFuncResult(ctx, m.Func.Call(in)) + if hasBindableFuncInputs { + // means that ctxValue is not initialized before by the controller's struct injector. + if !hasBindableFields { + ctxValue = reflect.ValueOf(ctx) } + in := make([]reflect.Value, n, n) + in[0] = ctrl + funcInjector.Inject(&in, ctxValue) + DispatchFuncResult(ctx, call(in)) + return } + + DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) } + } diff --git a/mvc/controller_test.go b/mvc/controller_test.go index 24ed310bd9..8238924aaa 100644 --- a/mvc/controller_test.go +++ b/mvc/controller_test.go @@ -469,8 +469,8 @@ func (c *testControllerNotCreateNewDueManuallySettingAllFields) AfterActivation( -- got dependencies length: %d`, n) } - if a.IsRequestScoped() { - c.T.Fatalf(`this controller shouldn't be tagged used as request scoped(create new instances on each request), + if !a.Singleton() { + c.T.Fatalf(`this controller should be tagged as Singleton. It shouldn't be tagged used as request scoped(create new instances on each request), it doesn't contain any dynamic value or dependencies that should be binded via the iris mvc engine`) } } diff --git a/mvc/di/func.go b/mvc/di/func.go index 629c8b0159..36424b0666 100644 --- a/mvc/di/func.go +++ b/mvc/di/func.go @@ -85,22 +85,15 @@ func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, v } } - // s.Length = n s.Length = len(s.inputs) s.Valid = s.Length > 0 for i, in := range s.inputs { - bindmethodTyp := "Static" - - if in.Object.BindType == Dynamic { - bindmethodTyp = "Dynamic" - } - + bindmethodTyp := bindTypeString(in.Object.BindType) typIn := typ.In(in.InputIndex) // remember: on methods that are part of a struct (i.e controller) // the input index = 1 is the begggining instead of the 0, // because the 0 is the controller receiver pointer of the method. - s.trace += fmt.Sprintf("[%d] %s binding: '%s' for input position: %d and type: '%s'\n", i+1, bindmethodTyp, in.Object.Type.String(), in.InputIndex, typIn.String()) } diff --git a/mvc/di/object.go b/mvc/di/object.go index 29de55f5b3..f33e7036c6 100644 --- a/mvc/di/object.go +++ b/mvc/di/object.go @@ -12,6 +12,15 @@ const ( Dynamic // dynamic value, depends on some input arguments from the caller. ) +func bindTypeString(typ BindType) string { + switch typ { + case Dynamic: + return "Dynamic" + default: + return "Static" + } +} + type BindObject struct { Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' . Value reflect.Value diff --git a/mvc/di/reflect.go b/mvc/di/reflect.go index 3b03e6c0f6..a0674a2a12 100644 --- a/mvc/di/reflect.go +++ b/mvc/di/reflect.go @@ -66,6 +66,13 @@ func ValueOf(o interface{}) reflect.Value { return reflect.ValueOf(o) } +func ValuesOf(valuesAsInterface []interface{}) (values []reflect.Value) { + for _, v := range valuesAsInterface { + values = append(values, ValueOf(v)) + } + return +} + func IndirectType(typ reflect.Type) reflect.Type { switch typ.Kind() { case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: @@ -115,9 +122,10 @@ func structFieldIgnored(f reflect.StructField) bool { } type field struct { - Type reflect.Type - Index []int // the index of the field, slice if it's part of a embedded struct - Name string // the actual name + Type reflect.Type + Name string // the actual name. + Index []int // the index of the field, slice if it's part of a embedded struct + CanSet bool // is true if it's exported. // this could be empty, but in our cases it's not, // it's filled with the bind object (as service which means as static value) @@ -127,11 +135,11 @@ type field struct { // NumFields returns the total number of fields, and the embedded, even if the embedded struct is not exported, // it will check for its exported fields. -func NumFields(elemTyp reflect.Type) int { - return len(lookupFields(elemTyp, nil)) +func NumFields(elemTyp reflect.Type, skipUnexported bool) int { + return len(lookupFields(elemTyp, skipUnexported, nil)) } -func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { +func lookupFields(elemTyp reflect.Type, skipUnexported bool, parentIndex []int) (fields []field) { if elemTyp.Kind() != reflect.Struct { return } @@ -141,14 +149,15 @@ func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { if IndirectType(f.Type).Kind() == reflect.Struct && !structFieldIgnored(f) { - fields = append(fields, lookupFields(f.Type, append(parentIndex, i))...) + fields = append(fields, lookupFields(f.Type, skipUnexported, append(parentIndex, i))...) continue } // skip unexported fields here, // after the check for embedded structs, these can be binded if their // fields are exported. - if f.PkgPath != "" { + isExported := f.PkgPath == "" + if skipUnexported && !isExported { continue } @@ -158,9 +167,10 @@ func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { } field := field{ - Type: f.Type, - Name: f.Name, - Index: index, + Type: f.Type, + Name: f.Name, + Index: index, + CanSet: isExported, } fields = append(fields, field) @@ -172,12 +182,13 @@ func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) { // LookupNonZeroFieldsValues lookup for filled fields based on the "v" struct value instance. // It returns a slice of reflect.Value (same type as `Values`) that can be binded, // like the end-developer's custom values. -func LookupNonZeroFieldsValues(v reflect.Value) (bindValues []reflect.Value) { +func LookupNonZeroFieldsValues(v reflect.Value, skipUnexported bool) (bindValues []reflect.Value) { elem := IndirectValue(v) - fields := lookupFields(IndirectType(v.Type()), nil) - for _, f := range fields { + fields := lookupFields(IndirectType(v.Type()), skipUnexported, nil) - if fieldVal := elem.FieldByIndex(f.Index); f.Type.Kind() == reflect.Ptr && !IsZero(fieldVal) { + for _, f := range fields { + if fieldVal := elem.FieldByIndex(f.Index); /*f.Type.Kind() == reflect.Ptr &&*/ + !IsZero(fieldVal) { bindValues = append(bindValues, fieldVal) } } diff --git a/mvc/di/struct.go b/mvc/di/struct.go index aaa88f8909..a0ac9f1983 100644 --- a/mvc/di/struct.go +++ b/mvc/di/struct.go @@ -5,6 +5,13 @@ import ( "reflect" ) +type State uint8 + +const ( + Stateless State = iota + Singleton +) + type ( targetStructField struct { Object *BindObject @@ -12,22 +19,38 @@ type ( } StructInjector struct { - elemType reflect.Type + initRef reflect.Value + initRefAsSlice []reflect.Value // useful when the struct is passed on a func as input args via reflection. + elemType reflect.Type // fields []*targetStructField - Valid bool // is true when contains fields and it's a valid target struct. - trace string // for debug info. + // is true when contains bindable fields and it's a valid target struct, + // it maybe 0 but struct may contain unexported fields or exported but no bindable (Stateless) + // see `setState`. + HasFields bool + CanInject bool // if any bindable fields when the state is NOT singleton. + State State } ) +func (s *StructInjector) countBindType(typ BindType) (n int) { + for _, f := range s.fields { + if f.Object.BindType == typ { + n++ + } + } + return +} + func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *StructInjector { s := &StructInjector{ - elemType: IndirectType(v.Type()), + initRef: v, + initRefAsSlice: []reflect.Value{v}, + elemType: IndirectType(v.Type()), } - fields := lookupFields(s.elemType, nil) + fields := lookupFields(s.elemType, true, nil) for _, f := range fields { - if hijack != nil { if b, ok := hijack(f.Type); ok && b != nil { s.fields = append(s.fields, &targetStructField{ @@ -55,28 +78,75 @@ func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, }) break } - } } - s.Valid = len(s.fields) > 0 + s.HasFields = len(s.fields) > 0 + // set the overall state of this injector. + s.setState() + s.fillStruct() - if s.Valid { - for i, f := range s.fields { - bindmethodTyp := "Static" - if f.Object.BindType == Dynamic { - bindmethodTyp = "Dynamic" - } - elemField := s.elemType.FieldByIndex(f.FieldIndex) - s.trace += fmt.Sprintf("[%d] %s binding: '%s' for field '%s %s'\n", i+1, bindmethodTyp, f.Object.Type.String(), elemField.Name, elemField.Type.String()) - } + return s +} + +// set the state, once. +func (s *StructInjector) setState() { + // note for zero length of struct's fields: + // if struct doesn't contain any field + // so both of the below variables will be 0, + // so it's a singleton. + // At the other hand the `s.HasFields` maybe false + // but the struct may contain UNEXPORTED fields or non-bindable fields (request-scoped on both cases) + // so a new controller/struct at the caller side should be initialized on each request, + // we should not depend on the `HasFields` for singleton or no, this is the reason I + // added the `.State` now. + + staticBindingsFieldsLength := s.countBindType(Static) + structFieldsLength := NumFields(s.elemType, false) + + // println("staticBindingsFieldsLength: ", staticBindingsFieldsLength) + // println("structFieldsLength: ", structFieldsLength) + + // if the number of static values binded is equal to the + // total struct's fields(including unexported fields this time) then set as singleton. + if staticBindingsFieldsLength == structFieldsLength { + s.State = Singleton + return } - return s + s.CanInject = s.State == Stateless && s.HasFields + // the default is `Stateless`, which means that a new instance should be created + // on each inject action by the caller. +} + +// fill the static bindings values once. +func (s *StructInjector) fillStruct() { + if !s.HasFields { + return + } + // if field is Static then set it to the value that passed by the caller, + // so will have the static bindings already and we can just use that value instead + // of creating new instance. + destElem := IndirectValue(s.initRef) + for _, f := range s.fields { + // if field is Static then set it to the value that passed by the caller, + // so will have the static bindings already and we can just use that value instead + // of creating new instance. + if s.State == Singleton && f.Object.BindType == Static { + destElem.FieldByIndex(f.FieldIndex).Set(f.Object.Value) + } + } } -func (s *StructInjector) String() string { - return s.trace +func (s *StructInjector) String() (trace string) { + for i, f := range s.fields { + elemField := s.elemType.FieldByIndex(f.FieldIndex) + trace += fmt.Sprintf("[%d] %s binding: '%s' for field '%s %s'\n", + i+1, bindTypeString(f.Object.BindType), f.Object.Type.String(), + elemField.Name, elemField.Type.String()) + } + + return } func (s *StructInjector) Inject(dest interface{}, ctx ...reflect.Value) { @@ -91,25 +161,21 @@ func (s *StructInjector) Inject(dest interface{}, ctx ...reflect.Value) { func (s *StructInjector) InjectElem(destElem reflect.Value, ctx ...reflect.Value) { for _, f := range s.fields { f.Object.Assign(ctx, func(v reflect.Value) { - // fmt.Printf("%s for %s at index: %d\n", destElem.Type().String(), f.Object.Type.String(), f.FieldIndex) destElem.FieldByIndex(f.FieldIndex).Set(v) }) } } -func (s *StructInjector) InjectElemStaticOnly(destElem reflect.Value) (n int) { - for _, f := range s.fields { - if f.Object.BindType != Static { - continue - } - destElem.FieldByIndex(f.FieldIndex).Set(f.Object.Value) - n++ +func (s *StructInjector) New() reflect.Value { + if s.State == Singleton { + return s.initRef } - return + return reflect.New(s.elemType) } -func (s *StructInjector) New(ctx ...reflect.Value) reflect.Value { - dest := reflect.New(s.elemType) - s.InjectElem(dest, ctx...) - return dest +func (s *StructInjector) NewAsSlice() []reflect.Value { + if s.State == Singleton { + return s.initRefAsSlice + } + return []reflect.Value{reflect.New(s.elemType)} } diff --git a/mvc/di/values.go b/mvc/di/values.go index da9342bb5d..f0801b84b3 100644 --- a/mvc/di/values.go +++ b/mvc/di/values.go @@ -8,6 +8,8 @@ type ValuesReadOnly interface { Has(value interface{}) bool // Len returns the length of the values. Len() int + // Clone returns a copy of the current values. + Clone() Values } type Values []reflect.Value @@ -27,21 +29,29 @@ func (bv Values) Clone() Values { return NewValues() } +// CloneWithFieldsOf will return a copy of the current values +// plus the "v" struct's fields that are filled(non-zero) by the caller. +func (bv Values) CloneWithFieldsOf(s interface{}) Values { + values := bv.Clone() + + // add the manual filled fields to the dependencies. + filledFieldValues := LookupNonZeroFieldsValues(ValueOf(s), true) + values = append(values, filledFieldValues...) + return values +} + func (bv Values) Len() int { return len(bv) } -// Add binds values to this controller, if you want to share -// binding values between controllers use the Engine's `Bind` function instead. +// Add adds values as dependencies, if the struct's fields +// or the function's input arguments needs them, they will be defined as +// bindings (at build-time) and they will be used (at serve-time). func (bv *Values) Add(values ...interface{}) { - for _, val := range values { - bv.AddValue(reflect.ValueOf(val)) - } + bv.AddValues(ValuesOf(values)...) } -// AddValue same as `Add` but accepts reflect.Value -// instead. -func (bv *Values) AddValue(values ...reflect.Value) { +func (bv *Values) AddValues(values ...reflect.Value) { for _, v := range values { if !goodVal(v) { continue @@ -115,6 +125,6 @@ func (bv *Values) addIfNotExists(v reflect.Value) bool { return false } - bv.AddValue(v) + bv.Add(v) return true } diff --git a/mvc/engine.go b/mvc/engine.go index 269e203222..83e88cd0bb 100644 --- a/mvc/engine.go +++ b/mvc/engine.go @@ -85,7 +85,9 @@ func (e *Engine) Handler(handler interface{}) context.Handler { // // Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc. func (e *Engine) Controller(router router.Party, controller interface{}, beforeActivate ...func(BeforeActivation)) { - ca := newControllerActivator(router, controller, e.Dependencies.Clone()) + // add the manual filled fields to the dependencies. + dependencies := e.Dependencies.CloneWithFieldsOf(controller) + ca := newControllerActivator(router, controller, dependencies) // give a priority to the "beforeActivate" // callbacks, if any. diff --git a/mvc/func_result.go b/mvc/func_result.go index 9b64c03333..8875f07882 100644 --- a/mvc/func_result.go +++ b/mvc/func_result.go @@ -165,8 +165,7 @@ func DispatchCommon(ctx context.Context, // // where Get is an HTTP METHOD. func DispatchFuncResult(ctx context.Context, values []reflect.Value) { - numOut := len(values) - if numOut == 0 { + if len(values) == 0 { return } @@ -195,54 +194,107 @@ func DispatchFuncResult(ctx context.Context, values []reflect.Value) { ) for _, v := range values { + // order of these checks matters // for example, first we need to check for status code, // secondly the string (for content type and content)... + // if !v.IsValid() || !v.CanInterface() { + // continue + // } if !v.IsValid() { continue } f := v.Interface() + /* + if b, ok := f.(bool); ok { + found = b + if !found { + // skip everything, we don't care about other return values, + // this boolean is the higher in order. + break + } + continue + } + + if i, ok := f.(int); ok { + statusCode = i + continue + } + + if s, ok := f.(string); ok { + // a string is content type when it contains a slash and + // content or custom struct is being calculated already; + // (string -> content, string-> content type) + // (customStruct, string -> content type) + if (len(content) > 0 || custom != nil) && strings.IndexByte(s, slashB) > 0 { + contentType = s + } else { + // otherwise is content + content = []byte(s) + } + + continue + } + + if b, ok := f.([]byte); ok { + // it's raw content, get the latest + content = b + continue + } - if b, ok := f.(bool); ok { - found = b + if e, ok := f.(compatibleErr); ok { + if e != nil { // it's always not nil but keep it here. + err = e + if statusCode < 400 { + statusCode = DefaultErrStatusCode + } + break // break on first error, error should be in the end but we + // need to know break the dispatcher if any error. + // at the end; we don't want to write anything to the response if error is not nil. + } + continue + } + + // else it's a custom struct or a dispatcher, we'll decide later + // because content type and status code matters + // do that check in order to be able to correctly dispatch: + // (customStruct, error) -> customStruct filled and error is nil + if custom == nil && f != nil { + custom = f + } + + } + + */ + switch value := f.(type) { + case bool: + found = value if !found { - // skip everything, we don't care about other return values, + // skip everything, skip other values, we don't care about other return values, // this boolean is the higher in order. break } - continue - } - - if i, ok := f.(int); ok { - statusCode = i - continue - } - - if s, ok := f.(string); ok { + case int: + statusCode = value + case string: // a string is content type when it contains a slash and // content or custom struct is being calculated already; // (string -> content, string-> content type) // (customStruct, string -> content type) - if (len(content) > 0 || custom != nil) && strings.IndexByte(s, slashB) > 0 { - contentType = s + if (len(content) > 0 || custom != nil) && strings.IndexByte(value, slashB) > 0 { + contentType = value } else { // otherwise is content - content = []byte(s) + content = []byte(value) } - continue - } - - if b, ok := f.([]byte); ok { + case []byte: // it's raw content, get the latest - content = b - continue - } - - if e, ok := f.(compatibleErr); ok { - if e != nil { // it's always not nil but keep it here. - err = e + content = value + case compatibleErr: + if value != nil { // it's always not nil but keep it here. + err = value if statusCode < 400 { statusCode = DefaultErrStatusCode } @@ -250,17 +302,15 @@ func DispatchFuncResult(ctx context.Context, values []reflect.Value) { // need to know break the dispatcher if any error. // at the end; we don't want to write anything to the response if error is not nil. } - continue - } - - // else it's a custom struct or a dispatcher, we'll decide later - // because content type and status code matters - // do that check in order to be able to correctly dispatch: - // (customStruct, error) -> customStruct filled and error is nil - if custom == nil && f != nil { - custom = f + default: + // else it's a custom struct or a dispatcher, we'll decide later + // because content type and status code matters + // do that check in order to be able to correctly dispatch: + // (customStruct, error) -> customStruct filled and error is nil + if custom == nil && f != nil { + custom = f + } } - } DispatchCommon(ctx, statusCode, contentType, content, custom, err, found) diff --git a/mvc/ideas/1/main.go b/mvc/ideas/1/main.go index 329f5c66ff..2c46f815eb 100644 --- a/mvc/ideas/1/main.go +++ b/mvc/ideas/1/main.go @@ -15,6 +15,7 @@ import ( // Therefore I should reduce some "freedom of change" for the shake of code maintanability in the core/router files: handler.go | router.go and single change on APIBuilder's field. func main() { app := iris.New() + app.Logger().SetLevel("debug") mvc.New(app.Party("/todo")).Configure(TodoApp) // no let's have a clear "mvc" package without any conversions and type aliases, // it's one extra import path for a whole new world, it worths it. @@ -74,8 +75,8 @@ func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { } func (c *TodoController) AfterActivation(a mvc.AfterActivation) { - if !a.IsRequestScoped() { - panic("TodoController should be request scoped, we have a 'Session' which depends on the context.") + if a.Singleton() { + panic("TodoController should be stateless, a request-scoped, we have a 'Session' which depends on the context.") } } From f2cbba1b0e62cb7ea2189219c1c7ff07e683b094 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 20 Dec 2017 00:01:50 +0200 Subject: [PATCH 32/79] controller's struct: if unexported field(s) is set-ed manually (binder cannot do that) and no other dynamic dependency(depends on context) then tag the controller as singleton, use a single controller instance to serve all clients (the controller's methods are per-request of course but its(controller's) fields will be 'global'- users should be careful because parallel access of those fields are up to them by using mutex or atomic or values that are safe for concurrent access. --- mvc/di/struct.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/mvc/di/struct.go b/mvc/di/struct.go index a0ac9f1983..27c19f6256 100644 --- a/mvc/di/struct.go +++ b/mvc/di/struct.go @@ -83,13 +83,14 @@ func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, s.HasFields = len(s.fields) > 0 // set the overall state of this injector. - s.setState() s.fillStruct() + s.setState() return s } // set the state, once. +// Here the "initRef" have already the static bindings and the manually-filled fields. func (s *StructInjector) setState() { // note for zero length of struct's fields: // if struct doesn't contain any field @@ -102,21 +103,31 @@ func (s *StructInjector) setState() { // added the `.State` now. staticBindingsFieldsLength := s.countBindType(Static) - structFieldsLength := NumFields(s.elemType, false) + allStructFieldsLength := NumFields(s.elemType, false) + // check if unexported(and exported) fields are set-ed manually or via binding (at this time we have all fields set-ed inside the "initRef") + // i.e &Controller{unexportedField: "my value"} + // or dependencies values = "my value" and Controller struct {Field string} + // if so then set the temp staticBindingsFieldsLength to that number, so for example: + // if static binding length is 0 + // but an unexported field is set-ed then act that as singleton. + if allStructFieldsLength > staticBindingsFieldsLength { + structFieldsUnexportedNonZero := LookupNonZeroFieldsValues(s.initRef, false) + staticBindingsFieldsLength = len(structFieldsUnexportedNonZero) + } // println("staticBindingsFieldsLength: ", staticBindingsFieldsLength) - // println("structFieldsLength: ", structFieldsLength) + // println("allStructFieldsLength: ", allStructFieldsLength) // if the number of static values binded is equal to the // total struct's fields(including unexported fields this time) then set as singleton. - if staticBindingsFieldsLength == structFieldsLength { + if staticBindingsFieldsLength == allStructFieldsLength { s.State = Singleton + // the default is `Stateless`, which means that a new instance should be created + // on each inject action by the caller. return } s.CanInject = s.State == Stateless && s.HasFields - // the default is `Stateless`, which means that a new instance should be created - // on each inject action by the caller. } // fill the static bindings values once. From 082c60447b1f3c45501b984a3c7f21f498649e07 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 20 Dec 2017 00:09:17 +0200 Subject: [PATCH 33/79] fix check for singleton on fillStruct - no problem let's set all the static values on the first value although it may never be needed if request-scoped/stateless (No Singleton di.State) --- mvc/di/struct.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mvc/di/struct.go b/mvc/di/struct.go index 27c19f6256..0d077aff71 100644 --- a/mvc/di/struct.go +++ b/mvc/di/struct.go @@ -143,7 +143,7 @@ func (s *StructInjector) fillStruct() { // if field is Static then set it to the value that passed by the caller, // so will have the static bindings already and we can just use that value instead // of creating new instance. - if s.State == Singleton && f.Object.BindType == Static { + if f.Object.BindType == Static { destElem.FieldByIndex(f.FieldIndex).Set(f.Object.Value) } } From d2985b27e945dd1629f70dcff3cae0d2976078c4 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 20 Dec 2017 08:33:53 +0200 Subject: [PATCH 34/79] fix all _examples to the newest mvc, add comments to those examples and add a package-level .Configure in order to make it easier for new users. Add a deprecated panic if app.Controller is used with a small tutorial and future resource link so they can re-write their mvc app's definitions --- .../main.go | 24 ++- .../user/auth.go | 80 ++++----- .../user/controller.go | 160 ++++++++++++------ _examples/tutorial/caddy/server1/main.go | 29 ++-- _examples/tutorial/caddy/server2/main.go | 50 +++--- .../src/web/controllers/todo_controller.go | 6 +- deprecated.go | 54 ++++++ mvc/controller.go | 151 +++++++++-------- mvc/controller_test.go | 2 - mvc/di/TODO.txt | 11 ++ mvc/di/di.go | 11 +- mvc/di/struct.go | 18 +- mvc/di/values.go | 12 +- mvc/engine.go | 30 ++-- mvc/func_result_test.go | 7 +- mvc/handler.go | 17 +- mvc/ideas/1/TODO.txt | 2 + mvc/mvc.go | 39 ++++- mvc/param_test.go | 5 +- mvc/session_controller.go | 3 + 20 files changed, 430 insertions(+), 281 deletions(-) create mode 100644 deprecated.go create mode 100644 mvc/di/TODO.txt create mode 100644 mvc/ideas/1/TODO.txt diff --git a/_examples/structuring/login-mvc-single-responsibility-package/main.go b/_examples/structuring/login-mvc-single-responsibility-package/main.go index 154ec39a05..60b4d36b3b 100644 --- a/_examples/structuring/login-mvc-single-responsibility-package/main.go +++ b/_examples/structuring/login-mvc-single-responsibility-package/main.go @@ -6,6 +6,7 @@ import ( "github.com/kataras/iris/_examples/structuring/login-mvc-single-responsibility-package/user" "github.com/kataras/iris" + "github.com/kataras/iris/mvc" "github.com/kataras/iris/sessions" ) @@ -19,13 +20,7 @@ func main() { app.StaticWeb("/public", "./public") - manager := sessions.New(sessions.Config{ - Cookie: "sessioncookiename", - Expires: 24 * time.Hour, - }) - users := user.NewDataSource() - - app.Controller("/user", new(user.Controller), manager, users) + mvc.Configure(app, configureMVC) // http://localhost:8080/user/register // http://localhost:8080/user/login @@ -35,9 +30,22 @@ func main() { app.Run(iris.Addr(":8080"), configure) } +func configureMVC(app *mvc.Application) { + manager := sessions.New(sessions.Config{ + Cookie: "sessioncookiename", + Expires: 24 * time.Hour, + }) + + userApp := app.NewChild(app.Router.Party("/user")) + userApp.AddDependencies( + user.NewDataSource(), + mvc.Session(manager), + ) + userApp.Register(new(user.Controller)) +} + func configure(app *iris.Application) { app.Configure( iris.WithoutServerError(iris.ErrServerClosed), - iris.WithCharset("UTF-8"), ) } diff --git a/_examples/structuring/login-mvc-single-responsibility-package/user/auth.go b/_examples/structuring/login-mvc-single-responsibility-package/user/auth.go index cda9d3ddf1..0300f03506 100644 --- a/_examples/structuring/login-mvc-single-responsibility-package/user/auth.go +++ b/_examples/structuring/login-mvc-single-responsibility-package/user/auth.go @@ -6,51 +6,52 @@ import ( "strings" "github.com/kataras/iris" + "github.com/kataras/iris/mvc" + "github.com/kataras/iris/sessions" ) -// paths -const ( - PathLogin = "/user/login" - PathLogout = "/user/logout" -) +const sessionIDKey = "UserID" -// the session key for the user id comes from the Session. -const ( - sessionIDKey = "UserID" +// paths +var ( + PathLogin = mvc.Response{Path: "/user/login"} + PathLogout = mvc.Response{Path: "/user/logout"} ) // AuthController is the user authentication controller, a custom shared controller. type AuthController struct { - iris.SessionController + // context is auto-binded if struct depends on this, + // in this controller we don't we do everything with mvc-style, + // and that's neither the 30% of its features. + // Ctx iris.Context - Source *DataSource - User Model `iris:"model"` + Source *DataSource + Session *sessions.Session + + // the whole controller is request-scoped because we already depend on Session, so + // this will be new for each new incoming request, BeginRequest sets that based on the session. + UserID int64 } // BeginRequest saves login state to the context, the user id. func (c *AuthController) BeginRequest(ctx iris.Context) { - c.SessionController.BeginRequest(ctx) - - if userID := c.Session.Get(sessionIDKey); userID != nil { - ctx.Values().Set(sessionIDKey, userID) - } + c.UserID, _ = c.Session.GetInt64(sessionIDKey) } -func (c *AuthController) fireError(err error) { - if err != nil { - c.Ctx.Application().Logger().Debug(err.Error()) +// EndRequest is here just to complete the BaseController +// in order to be tell iris to call the `BeginRequest` before the main method. +func (c *AuthController) EndRequest(ctx iris.Context) {} - c.Status = 400 - c.Data["Title"] = "User Error" - c.Data["Message"] = strings.ToUpper(err.Error()) - c.Tmpl = "shared/error.html" +func (c *AuthController) fireError(err error) mvc.View { + return mvc.View{ + Code: iris.StatusBadRequest, + Name: "shared/error.html", + Data: iris.Map{"Title": "User Error", "Message": strings.ToUpper(err.Error())}, } } -func (c *AuthController) redirectTo(id int64) { - if id > 0 { - c.Path = "/user/" + strconv.Itoa(int(id)) - } +func (c *AuthController) redirectTo(id int64) mvc.Response { + return mvc.Response{Path: "/user/" + strconv.Itoa(int(id))} } func (c *AuthController) createOrUpdate(firstname, username, password string) (user Model, err error) { @@ -75,8 +76,8 @@ func (c *AuthController) createOrUpdate(firstname, username, password string) (u func (c *AuthController) isLoggedIn() bool { // we don't search by session, we have the user id - // already by the `SaveState` middleware. - return c.Values.Get(sessionIDKey) != nil + // already by the `BeginRequest` middleware. + return c.UserID > 0 } func (c *AuthController) verify(username, password string) (user Model, err error) { @@ -101,24 +102,9 @@ func (c *AuthController) verify(username, password string) (user Model, err erro // if logged in then destroy the session // and redirect to the login page // otherwise redirect to the registration page. -func (c *AuthController) logout() { +func (c *AuthController) logout() mvc.Response { if c.isLoggedIn() { - // c.Manager is the Sessions manager created - // by the embedded SessionController, automatically. - c.Manager.DestroyByID(c.Session.ID()) - return - } - - c.Path = PathLogin -} - -// AllowUser will check if this client is a logged user, -// if not then it will redirect that guest to the login page -// otherwise it will allow the execution of the next handler. -func AllowUser(ctx iris.Context) { - if ctx.Values().Get(sessionIDKey) != nil { - ctx.Next() - return + c.Session.Destroy() } - ctx.Redirect(PathLogin) + return PathLogin } diff --git a/_examples/structuring/login-mvc-single-responsibility-package/user/controller.go b/_examples/structuring/login-mvc-single-responsibility-package/user/controller.go index 628fc63f27..3c5e314216 100644 --- a/_examples/structuring/login-mvc-single-responsibility-package/user/controller.go +++ b/_examples/structuring/login-mvc-single-responsibility-package/user/controller.go @@ -1,8 +1,18 @@ package user -const ( - pathMyProfile = "/user/me" - pathRegister = "/user/register" +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" +) + +var ( + // About Code: iris.StatusSeeOther -> + // When redirecting from POST to GET request you -should- use this HTTP status code, + // however there're some (complicated) alternatives if you + // search online or even the HTTP RFC. + // "See Other" RFC 7231 + pathMyProfile = mvc.Response{Path: "/user/me", Code: iris.StatusSeeOther} + pathRegister = mvc.Response{Path: "/user/register"} ) // Controller is responsible to handle the following requests: @@ -17,71 +27,89 @@ type Controller struct { AuthController } +type formValue func(string) string + +// BeforeActivation called once before the server start +// and before the controller's registration, here you can add +// dependencies, to this controller and only, that the main caller may skip. +func (c *Controller) BeforeActivation(b mvc.BeforeActivation) { + // bind the context's `FormValue` as well in order to be + // acceptable on the controller or its methods' input arguments (NEW feature as well). + b.Dependencies().Add(func(ctx iris.Context) formValue { return ctx.FormValue }) +} + +type page struct { + Title string +} + // GetRegister handles GET:/user/register. -func (c *Controller) GetRegister() { +// mvc.Result can accept any struct which contains a `Dispatch(ctx iris.Context)` method. +// Both mvc.Response and mvc.View are mvc.Result. +func (c *Controller) GetRegister() mvc.Result { if c.isLoggedIn() { - c.logout() - return + return c.logout() } - c.Data["Title"] = "User Registration" - c.Tmpl = pathRegister + ".html" + // You could just use it as a variable to win some time in serve-time, + // this is an exersise for you :) + return mvc.View{ + Name: pathRegister.Path + ".html", + Data: page{"User Registration"}, + } } // PostRegister handles POST:/user/register. -func (c *Controller) PostRegister() { +func (c *Controller) PostRegister(form formValue) mvc.Result { // we can either use the `c.Ctx.ReadForm` or read values one by one. var ( - firstname = c.Ctx.FormValue("firstname") - username = c.Ctx.FormValue("username") - password = c.Ctx.FormValue("password") + firstname = form("firstname") + username = form("username") + password = form("password") ) user, err := c.createOrUpdate(firstname, username, password) if err != nil { - c.fireError(err) - return + return c.fireError(err) } // setting a session value was never easier. c.Session.Set(sessionIDKey, user.ID) // succeed, nothing more to do here, just redirect to the /user/me. + return pathMyProfile +} - // When redirecting from POST to GET request you -should- use this HTTP status code, - // however there're some (complicated) alternatives if you - // search online or even the HTTP RFC. - c.Status = 303 // "See Other" RFC 7231 - - // Redirect to GET: /user/me - // by changing the Path (and the status code because we're in POST request at this case). - c.Path = pathMyProfile +// with these static views, +// you can use variables-- that are initialized before server start +// so you can win some time on serving. +// You can do it else where as well but I let them as pracise for you, +// essentialy you can understand by just looking below. +var userLoginView = mvc.View{ + Name: PathLogin.Path + ".html", + Data: page{"User Login"}, } // GetLogin handles GET:/user/login. -func (c *Controller) GetLogin() { +func (c *Controller) GetLogin() mvc.Result { if c.isLoggedIn() { - c.logout() - return + return c.logout() } - c.Data["Title"] = "User Login" - c.Tmpl = PathLogin + ".html" + return userLoginView } // PostLogin handles POST:/user/login. -func (c *Controller) PostLogin() { +func (c *Controller) PostLogin(form formValue) mvc.Result { var ( - username = c.Ctx.FormValue("username") - password = c.Ctx.FormValue("password") + username = form("username") + password = form("password") ) user, err := c.verify(username, password) if err != nil { - c.fireError(err) - return + return c.fireError(err) } c.Session.Set(sessionIDKey, user.ID) - c.Path = pathMyProfile + return pathMyProfile } // AnyLogout handles any method on path /user/logout. @@ -90,44 +118,72 @@ func (c *Controller) AnyLogout() { } // GetMe handles GET:/user/me. -func (c *Controller) GetMe() { +func (c *Controller) GetMe() mvc.Result { id, err := c.Session.GetInt64(sessionIDKey) if err != nil || id <= 0 { - // when not already logged in. - c.Path = PathLogin - return + // when not already logged in, redirect to login. + return PathLogin } u, found := c.Source.GetByID(id) if !found { // if the session exists but for some reason the user doesn't exist in the "database" // then logout him and redirect to the register page. - c.logout() - return + return c.logout() } // set the model and render the view template. - c.User = u - c.Data["Title"] = "Profile of " + u.Username - c.Tmpl = pathMyProfile + ".html" + return mvc.View{ + Name: pathMyProfile.Path + ".html", + Data: iris.Map{ + "Title": "Profile of " + u.Username, + "User": u, + }, + } } -func (c *Controller) renderNotFound(id int64) { - c.Status = 404 - c.Data["Title"] = "User Not Found" - c.Data["ID"] = id - c.Tmpl = "user/notfound.html" +func (c *Controller) renderNotFound(id int64) mvc.View { + return mvc.View{ + Code: iris.StatusNotFound, + Name: "user/notfound.html", + Data: iris.Map{ + "Title": "User Not Found", + "ID": id, + }, + } +} + +// Dispatch completes the `mvc.Result` interface +// in order to be able to return a type of `Model` +// as mvc.Result. +// If this function didn't exist then +// we should explicit set the output result to that Model or to an interface{}. +func (u Model) Dispatch(ctx iris.Context) { + ctx.JSON(u) } // GetBy handles GET:/user/{id:long}, // i.e http://localhost:8080/user/1 -func (c *Controller) GetBy(userID int64) { +func (c *Controller) GetBy(userID int64) mvc.Result { // we have /user/{id} // fetch and render user json. - if user, found := c.Source.GetByID(userID); !found { + user, found := c.Source.GetByID(userID) + if !found { // not user found with that ID. - c.renderNotFound(userID) - } else { - c.Ctx.JSON(user) + return c.renderNotFound(userID) } + + // Q: how the hell Model can be return as mvc.Result? + // A: I told you before on some comments and the docs, + // any struct that has a `Dispatch(ctx iris.Context)` + // can be returned as an mvc.Result(see ~20 lines above), + // therefore we are able to combine many type of results in the same method. + // For example, here, we return either an mvc.View to render a not found custom template + // either a user which returns the Model as JSON via its Dispatch. + // + // We could also return just a struct value that is not an mvc.Result, + // if the output result of the `GetBy` was that struct's type or an interface{} + // and iris would render that with JSON as well, but here we can't do that without complete the `Dispatch` + // function, because we may return an mvc.View which is an mvc.Result. + return user } diff --git a/_examples/tutorial/caddy/server1/main.go b/_examples/tutorial/caddy/server1/main.go index be821797a6..08d1b3d1d3 100644 --- a/_examples/tutorial/caddy/server1/main.go +++ b/_examples/tutorial/caddy/server1/main.go @@ -2,6 +2,7 @@ package main import ( "github.com/kataras/iris" + "github.com/kataras/iris/mvc" ) func main() { @@ -10,7 +11,7 @@ func main() { templates := iris.HTML("./views", ".html").Layout("shared/layout.html") app.RegisterView(templates) - app.Controller("/", new(Controller)) + mvc.New(app).Register(new(Controller)) // http://localhost:9091 app.Run(iris.Addr(":9091")) @@ -21,22 +22,28 @@ type Layout struct { Title string } -// Controller is our example controller. +// Controller is our example controller, request-scoped, each request has its own instance. type Controller struct { - iris.Controller - - Layout Layout `iris:"model"` + Layout Layout } -// BeginRequest is the first method fires when client requests from this Controller's path. +// BeginRequest is the first method fired when client requests from this Controller's root path. func (c *Controller) BeginRequest(ctx iris.Context) { - c.Controller.BeginRequest(ctx) - c.Layout.Title = "Home Page" } +// EndRequest is the last method fired. +// It's here just to complete the BaseController +// in order to be tell iris to call the `BeginRequest` before the main method. +func (c *Controller) EndRequest(ctx iris.Context) {} + // Get handles GET http://localhost:9091 -func (c *Controller) Get() { - c.Tmpl = "index.html" - c.Data["Message"] = "Welcome to my website!" +func (c *Controller) Get() mvc.View { + return mvc.View{ + Name: "index.html", + Data: iris.Map{ + "Layout": c.Layout, + "Message": "Welcome to my website!", + }, + } } diff --git a/_examples/tutorial/caddy/server2/main.go b/_examples/tutorial/caddy/server2/main.go index 72d9e2fd94..96bab8867e 100644 --- a/_examples/tutorial/caddy/server2/main.go +++ b/_examples/tutorial/caddy/server2/main.go @@ -2,13 +2,18 @@ package main import ( "github.com/kataras/iris" + "github.com/kataras/iris/mvc" ) -func main() { +type postValue func(string) string +func main() { app := iris.New() - app.Controller("/user", new(UserController)) + mvc.New(app.Party("/user")).AddDependencies( + func(ctx iris.Context) postValue { + return ctx.PostValue + }).Register(new(UserController)) // GET http://localhost:9092/user // GET http://localhost:9092/user/42 @@ -20,37 +25,44 @@ func main() { } // UserController is our user example controller. -type UserController struct { - iris.Controller -} +type UserController struct{} // Get handles GET /user -func (c *UserController) Get() { - c.Ctx.Writef("Select all users") +func (c *UserController) Get() string { + return "Select all users" } -// GetBy handles GET /user/42 -func (c *UserController) GetBy(id int) { - c.Ctx.Writef("Select user by ID: %d", id) +// User is our test User model, nothing tremendous here. +type User struct{ ID int64 } + +// GetBy handles GET /user/42, equal to .Get("/user/{id:long}") +func (c *UserController) GetBy(id int64) User { + // Select User by ID == $id. + return User{id} } // Post handles POST /user -func (c *UserController) Post() { - username := c.Ctx.PostValue("username") - c.Ctx.Writef("Create by user with username: %s", username) +func (c *UserController) Post(post postValue) string { + username := post("username") + return "Create by user with username: " + username } // PutBy handles PUT /user/42 -func (c *UserController) PutBy(id int) { - c.Ctx.Writef("Update user by ID: %d", id) +func (c *UserController) PutBy(id int) string { + // Update user by ID == $id + return "User updated" } // DeleteBy handles DELETE /user/42 -func (c *UserController) DeleteBy(id int) { - c.Ctx.Writef("Delete user by ID: %d", id) +func (c *UserController) DeleteBy(id int) bool { + // Delete user by ID == %id + // + // when boolean then true = iris.StatusOK, false = iris.StatusNotFound + return true } // GetFollowersBy handles GET /user/followers/42 -func (c *UserController) GetFollowersBy(id int) { - c.Ctx.Writef("Select all followers by user ID: %d", id) +func (c *UserController) GetFollowersBy(id int) []User { + // Select all followers by user ID == $id + return []User{ /* ... */ } } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go index 744eb5224d..cb671bad9c 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -17,13 +17,13 @@ type TodoController struct { } // BeforeActivation called once before the server ran, and before -// the routes and dependency binder builded. +// the routes and dependencies binded. // You can bind custom things to the controller, add new methods, add middleware, // add dependencies to the struct or the method(s) and more. -func (c *TodoController) BeforeActivation(ca *mvc.ControllerActivator) { +func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { // this could be binded to a controller's function input argument // if any, or struct field if any: - ca.Dependencies.Add(func(ctx iris.Context) todo.Item { + b.Dependencies().Add(func(ctx iris.Context) todo.Item { // ctx.ReadForm(&item) var ( owner = ctx.PostValue("owner") diff --git a/deprecated.go b/deprecated.go new file mode 100644 index 0000000000..771f5fc5ce --- /dev/null +++ b/deprecated.go @@ -0,0 +1,54 @@ +package iris + +import ( + "fmt" + + "github.com/kataras/iris/core/router" + "github.com/kataras/iris/mvc" +) + +// Controller method is DEPRECATED, use the "mvc" subpackage instead, i.e +// import "github.com/kataras/iris/mvc" and read its docs among with its new features at: +// https://github.com/kataras/iris/blob/master/HISTORY.md#mo-01-jenuary-2018--v10 +func (app *Application) Controller(relPath string, c interface{}, _ ...interface{}) []*router.Route { + name := mvc.NameOf(c) + panic(fmt.Errorf(`"Controller" method is DEPRECATED, use the "mvc" subpackage instead. + + PREVIOUSLY YOU USED TO CODE IT LIKE THIS: + + import ( + "github.com/kataras/iris" + // ... + ) + + app.Controller("%s", new(%s), Struct_Values_Binded_To_The_Fields_Or_And_Any_Middleware) + + NOW YOU SHOULD CODE IT LIKE THIS: + + import ( + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" + // ... + ) + + // or use it like this: ).AddDependencies(...).Register(new(%s)) + mvc.Configure(app.Party("%s"), myMVC) + + func myMVC(mvcApp *mvc.Application) { + mvcApp.AddDependencies( + Struct_Values_Dependencies_Binded_To_The_Fields_Or_And_To_Methods, + Or_And_Func_Values_Dependencies_Binded_To_The_Fields_Or_And_To_Methods, + ) + + mvcApp.Router.Use(Any_Middleware) + + mvcApp.Register(new(%s)) + } + + The new MVC implementation contains a lot more than the above, + this is the reason you see more lines for a simple controller. + + Please read more about the newest, amazing, features by navigating below + https://github.com/kataras/iris/blob/master/HISTORY.md#mo-01-jenuary-2018--v1000`, // v10.0.0, we skip the number 9. + relPath, name, name, relPath, name)) +} diff --git a/mvc/controller.go b/mvc/controller.go index e578c28bb2..78ec9c6121 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -24,7 +24,7 @@ type BaseController interface { type shared interface { Name() string Router() router.Party - Handle(method, path, funcName string, middleware ...context.Handler) *router.Route + Handle(httpMethod, path, funcName string, middleware ...context.Handler) *router.Route } type BeforeActivation interface { @@ -34,7 +34,7 @@ type BeforeActivation interface { type AfterActivation interface { shared - DependenciesReadOnly() di.ValuesReadOnly + DependenciesReadOnly() ValuesReadOnly Singleton() bool } @@ -66,12 +66,12 @@ type ControllerActivator struct { // on incoming requests. dependencies di.Values - // on activate. + // initialized on the first `Handle`. injector *di.StructInjector } -func getNameOf(typ reflect.Type) string { - elemTyp := di.IndirectType(typ) +func NameOf(v interface{}) string { + elemTyp := di.IndirectType(di.ValueOf(v).Type()) typName := elemTyp.Name() pkgPath := elemTyp.PkgPath() @@ -80,22 +80,17 @@ func getNameOf(typ reflect.Type) string { return fullname } -func newControllerActivator(router router.Party, controller interface{}, dependencies di.Values) *ControllerActivator { - var ( - val = reflect.ValueOf(controller) - typ = val.Type() - - // the full name of the controller: its type including the package path. - fullName = getNameOf(typ) - ) +func newControllerActivator(router router.Party, controller interface{}, dependencies []reflect.Value) *ControllerActivator { + typ := reflect.TypeOf(controller) c := &ControllerActivator{ // give access to the Router to the end-devs if they need it for some reason, // i.e register done handlers. - router: router, - Value: val, - Type: typ, - fullName: fullName, + router: router, + Value: reflect.ValueOf(controller), + Type: typ, + // the full name of the controller: its type including the package path. + fullName: NameOf(controller), // set some methods that end-dev cann't use accidentally // to register a route via the `Handle`, // all available exported and compatible methods @@ -106,7 +101,8 @@ func newControllerActivator(router router.Party, controller interface{}, depende // TODO: now that BaseController is totally optionally // we have to check if BeginRequest and EndRequest should be here. reservedMethods: whatReservedMethods(typ), - dependencies: dependencies, + // CloneWithFieldsOf: include the manual fill-ed controller struct's fields to the dependencies. + dependencies: di.Values(dependencies).CloneWithFieldsOf(controller), } return c @@ -125,7 +121,20 @@ func (c *ControllerActivator) Dependencies() *di.Values { return &c.dependencies } -func (c *ControllerActivator) DependenciesReadOnly() di.ValuesReadOnly { +type ValuesReadOnly interface { + // Has returns true if a binder responsible to + // bind and return a type of "typ" is already registered to this controller. + Has(value interface{}) bool + // Len returns the length of the values. + Len() int + // Clone returns a copy of the current values. + Clone() di.Values + // CloneWithFieldsOf will return a copy of the current values + // plus the "s" struct's fields that are filled(non-zero) by the caller. + CloneWithFieldsOf(s interface{}) di.Values +} + +func (c *ControllerActivator) DependenciesReadOnly() ValuesReadOnly { return c.dependencies } @@ -144,9 +153,9 @@ func (c *ControllerActivator) Router() router.Party { // any unexported fields and all fields are services-like, static. func (c *ControllerActivator) Singleton() bool { if c.injector == nil { - panic("MVC: IsRequestScoped used on an invalid state the API gives access to it only `AfterActivation`, report this as bug") + panic("MVC: Singleton used on an invalid state the API gives access to it only `AfterActivation`, report this as bug") } - return c.injector.State == di.Singleton + return c.injector.Scope == di.Singleton } // checks if a method is already registered. @@ -160,18 +169,12 @@ func (c *ControllerActivator) isReservedMethod(name string) bool { return false } -func (c *ControllerActivator) parseMethod(m reflect.Method) { - httpMethod, httpPath, err := parseMethod(m, c.isReservedMethod) - if err != nil { - if err != errSkip { - err = fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err) - c.router.GetReporter().AddErr(err) - - } - return - } +func (c *ControllerActivator) activate() { + c.parseMethods() +} - c.Handle(httpMethod, httpPath, m.Name) +func (c *ControllerActivator) addErr(err error) bool { + return c.router.GetReporter().AddErr(err) } // register all available, exported methods to handlers if possible. @@ -183,8 +186,17 @@ func (c *ControllerActivator) parseMethods() { } } -func (c *ControllerActivator) activate() { - c.parseMethods() +func (c *ControllerActivator) parseMethod(m reflect.Method) { + httpMethod, httpPath, err := parseMethod(m, c.isReservedMethod) + if err != nil { + if err != errSkip { + c.addErr(fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err)) + } + + return + } + + c.Handle(httpMethod, httpPath, m.Name) } // Handle registers a route based on a http method, the route's path @@ -202,44 +214,18 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . return nil } - // Remember: - // we cannot simply do that and expect to work: - // hasStructInjector = c.injector != nil && c.injector.Valid - // hasFuncInjector = funcInjector != nil && funcInjector.Valid - // because - // the `Handle` can be called from `BeforeActivation` callbacks - // and before activation, the c.injector is nil because - // we may not have the dependencies binded yet. But if `c.injector.Valid` - // inside the Handelr works because it's set on the `activate()` method. - // To solve this we can make check on the FIRST `Handle`, - // if c.injector is nil, then set it with the current bindings, - // so the user should bind the dependencies needed before the `Handle` - // this is a logical flow, so we will choose that one -> - if c.injector == nil { - // first, set these bindings to the passed controller, they will be useless - // if the struct contains any dynamic value because this controller will - // be never fired as it's but we make that in order to get the length of the static - // matched dependencies of the struct. - c.injector = di.MakeStructInjector(c.Value, hijacker, typeChecker, c.dependencies...) - if c.injector.HasFields { - golog.Debugf("MVC dependencies of '%s':\n%s", c.fullName, c.injector.String()) - } - } - // get the method from the controller type. m, ok := c.Type.MethodByName(funcName) if !ok { - err := fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", - funcName, c.fullName) - c.router.GetReporter().AddErr(err) + c.addErr(fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", + funcName, c.fullName)) return nil } // parse a route template which contains the parameters organised. tmpl, err := macro.Parse(path, c.router.Macros()) if err != nil { - err = fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.fullName, funcName, err) - c.router.GetReporter().AddErr(err) + c.addErr(fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.fullName, funcName, err)) return nil } @@ -257,23 +243,40 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // register the handler now. route := c.router.Handle(method, path, append(middleware, handler)...) - if route != nil { - // change the main handler's name in order to respect the controller's and give - // a proper debug message. - route.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName) - - // add this as a reserved method name in order to - // be sure that the same func will not be registered again, - // even if a custom .Handle later on. - c.reservedMethods = append(c.reservedMethods, funcName) + if route == nil { + c.addErr(fmt.Errorf("MVC: unable to register a route for the path for '%s.%s'", c.fullName, funcName)) + return nil } + // change the main handler's name in order to respect the controller's and give + // a proper debug message. + route.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName) + + // add this as a reserved method name in order to + // be sure that the same func will not be registered again, + // even if a custom .Handle later on. + c.reservedMethods = append(c.reservedMethods, funcName) + return route } var emptyIn = []reflect.Value{} func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []reflect.Value) context.Handler { + // Remember: + // The `Handle->handlerOf` can be called from `BeforeActivation` event + // then, the c.injector is nil because + // we may not have the dependencies binded yet. + // To solve this we're doing a check on the FIRST `Handle`, + // if c.injector is nil, then set it with the current bindings, + // these bindings can change after, so first add dependencies and after register routes. + if c.injector == nil { + c.injector = di.MakeStructInjector(c.Value, hijacker, typeChecker, c.dependencies...) + if c.injector.HasFields { + golog.Debugf("MVC dependencies of '%s':\n%s", c.fullName, c.injector.String()) + } + } + // fmt.Printf("for %s | values: %s\n", funcName, funcDependencies) funcInjector := di.MakeFuncInjector(m.Func, hijacker, typeChecker, funcDependencies...) // fmt.Printf("actual injector's inputs length: %d\n", funcInjector.Length) @@ -291,14 +294,14 @@ func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []ref if !implementsBase && !hasBindableFields && !hasBindableFuncInputs { return func(ctx context.Context) { - DispatchFuncResult(ctx, call(c.injector.NewAsSlice())) + DispatchFuncResult(ctx, call(c.injector.AcquireSlice())) } } n := m.Type.NumIn() return func(ctx context.Context) { var ( - ctrl = c.injector.New() + ctrl = c.injector.Acquire() ctxValue reflect.Value ) diff --git a/mvc/controller_test.go b/mvc/controller_test.go index 8238924aaa..1aa218a1f8 100644 --- a/mvc/controller_test.go +++ b/mvc/controller_test.go @@ -486,8 +486,6 @@ func TestControllerNotCreateNewDueManuallySettingAllFields(t *testing.T) { TitlePointer: &testBindType{ title: "my title", }, - }, func(b BeforeActivation) { - }) e := httptest.New(t, app) diff --git a/mvc/di/TODO.txt b/mvc/di/TODO.txt new file mode 100644 index 0000000000..569cb3922f --- /dev/null +++ b/mvc/di/TODO.txt @@ -0,0 +1,11 @@ +I can do one of the followings to this "di" folder when I finish the cleanup and document it a bit, +although I'm sick I will try to finish it tomorrow. + +End-users don't need this. +1) So, rename this to "internal". + +I don't know if something similar exist in Go, +it's a dependency injection framework at the end, and a very fast one. + +2) So I'm thinking to push it to a different repo, + like https://github.com/kataras/di or even to my small common https://github.com/kataras/pkg collection. \ No newline at end of file diff --git a/mvc/di/di.go b/mvc/di/di.go index f0a7ff1bb6..469be60278 100644 --- a/mvc/di/di.go +++ b/mvc/di/di.go @@ -57,15 +57,14 @@ func (d *D) Clone() *D { // with the injector's `Inject` and `InjectElem` methods. func (d *D) Struct(s interface{}) *StructInjector { if s == nil { - return nil + return &StructInjector{HasFields: false} } - v := ValueOf(s) return MakeStructInjector( - v, + ValueOf(s), d.hijacker, d.goodFunc, - d.Values..., + d.Values.CloneWithFieldsOf(s)..., ) } @@ -75,6 +74,10 @@ func (d *D) Struct(s interface{}) *StructInjector { // to the function's input argument when called // with the injector's `Fill` method. func (d *D) Func(fn interface{}) *FuncInjector { + if fn == nil { + return &FuncInjector{Valid: false} + } + return MakeFuncInjector( ValueOf(fn), d.hijacker, diff --git a/mvc/di/struct.go b/mvc/di/struct.go index 0d077aff71..091cd77f24 100644 --- a/mvc/di/struct.go +++ b/mvc/di/struct.go @@ -5,10 +5,10 @@ import ( "reflect" ) -type State uint8 +type Scope uint8 const ( - Stateless State = iota + Stateless Scope = iota Singleton ) @@ -29,7 +29,7 @@ type ( // see `setState`. HasFields bool CanInject bool // if any bindable fields when the state is NOT singleton. - State State + Scope Scope } ) @@ -121,13 +121,13 @@ func (s *StructInjector) setState() { // if the number of static values binded is equal to the // total struct's fields(including unexported fields this time) then set as singleton. if staticBindingsFieldsLength == allStructFieldsLength { - s.State = Singleton + s.Scope = Singleton // the default is `Stateless`, which means that a new instance should be created // on each inject action by the caller. return } - s.CanInject = s.State == Stateless && s.HasFields + s.CanInject = s.Scope == Stateless && s.HasFields } // fill the static bindings values once. @@ -177,15 +177,15 @@ func (s *StructInjector) InjectElem(destElem reflect.Value, ctx ...reflect.Value } } -func (s *StructInjector) New() reflect.Value { - if s.State == Singleton { +func (s *StructInjector) Acquire() reflect.Value { + if s.Scope == Singleton { return s.initRef } return reflect.New(s.elemType) } -func (s *StructInjector) NewAsSlice() []reflect.Value { - if s.State == Singleton { +func (s *StructInjector) AcquireSlice() []reflect.Value { + if s.Scope == Singleton { return s.initRefAsSlice } return []reflect.Value{reflect.New(s.elemType)} diff --git a/mvc/di/values.go b/mvc/di/values.go index f0801b84b3..d7aacb184e 100644 --- a/mvc/di/values.go +++ b/mvc/di/values.go @@ -2,16 +2,6 @@ package di import "reflect" -type ValuesReadOnly interface { - // Has returns true if a binder responsible to - // bind and return a type of "typ" is already registered to this controller. - Has(value interface{}) bool - // Len returns the length of the values. - Len() int - // Clone returns a copy of the current values. - Clone() Values -} - type Values []reflect.Value func NewValues() Values { @@ -30,7 +20,7 @@ func (bv Values) Clone() Values { } // CloneWithFieldsOf will return a copy of the current values -// plus the "v" struct's fields that are filled(non-zero) by the caller. +// plus the "s" struct's fields that are filled(non-zero) by the caller. func (bv Values) CloneWithFieldsOf(s interface{}) Values { values := bv.Clone() diff --git a/mvc/engine.go b/mvc/engine.go index 83e88cd0bb..e27ffc955c 100644 --- a/mvc/engine.go +++ b/mvc/engine.go @@ -84,30 +84,24 @@ func (e *Engine) Handler(handler interface{}) context.Handler { // where Get is an HTTP Method func. // // Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc. -func (e *Engine) Controller(router router.Party, controller interface{}, beforeActivate ...func(BeforeActivation)) { - // add the manual filled fields to the dependencies. - dependencies := e.Dependencies.CloneWithFieldsOf(controller) - ca := newControllerActivator(router, controller, dependencies) +func (e *Engine) Controller(router router.Party, controller interface{}) { + // initialize the controller's activator, nothing too magical so far. + c := newControllerActivator(router, controller, e.Dependencies) - // give a priority to the "beforeActivate" - // callbacks, if any. - for _, cb := range beforeActivate { - cb(ca) - } - - // check if controller has an "BeforeActivation" function - // which accepts the controller activator and call it. - if activateListener, ok := controller.(interface { + // check the controller's "BeforeActivation" or/and "AfterActivation" method(s) between the `activate` + // call, which is simply parses the controller's methods, end-dev can register custom controller's methods + // by using the BeforeActivation's (a ControllerActivation) `.Handle` method. + if before, ok := controller.(interface { BeforeActivation(BeforeActivation) }); ok { - activateListener.BeforeActivation(ca) + before.BeforeActivation(c) } - ca.activate() + c.activate() - if afterActivateListener, ok := controller.(interface { + if after, okAfter := controller.(interface { AfterActivation(AfterActivation) - }); ok { - afterActivateListener.AfterActivation(ca) + }); okAfter { + after.AfterActivation(c) } } diff --git a/mvc/func_result_test.go b/mvc/func_result_test.go index 424f6734eb..0ef9db37c5 100644 --- a/mvc/func_result_test.go +++ b/mvc/func_result_test.go @@ -264,9 +264,10 @@ func (t *testControllerViewResultRespectCtxViewData) Get() Result { func TestControllerViewResultRespectCtxViewData(t *testing.T) { app := iris.New() - NewEngine().Controller(app, new(testControllerViewResultRespectCtxViewData), func(b BeforeActivation) { - b.Dependencies().Add(t) - }) + m := NewEngine() + m.Dependencies.Add(t) + m.Controller(app.Party("/"), new(testControllerViewResultRespectCtxViewData)) + e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusInternalServerError) diff --git a/mvc/handler.go b/mvc/handler.go index 0df5fb42c6..818191433d 100644 --- a/mvc/handler.go +++ b/mvc/handler.go @@ -65,26 +65,23 @@ func MakeHandler(handler interface{}, bindValues ...reflect.Value) (context.Hand return h, nil } - s := di.MakeFuncInjector(fn, hijacker, typeChecker, bindValues...) - if !s.Valid { + funcInjector := di.MakeFuncInjector(fn, hijacker, typeChecker, bindValues...) + if !funcInjector.Valid { pc := fn.Pointer() fpc := runtime.FuncForPC(pc) callerFileName, callerLineNumber := fpc.FileLine(pc) callerName := fpc.Name() err := fmt.Errorf("input arguments length(%d) and valid binders length(%d) are not equal for typeof '%s' which is defined at %s:%d by %s", - n, s.Length, fn.Type().String(), callerFileName, callerLineNumber, callerName) + n, funcInjector.Length, fn.Type().String(), callerFileName, callerLineNumber, callerName) return nil, err } h := func(ctx context.Context) { - in := make([]reflect.Value, n, n) - - s.Inject(&in, reflect.ValueOf(ctx)) - if ctx.IsStopped() { - return - } - DispatchFuncResult(ctx, fn.Call(in)) + // in := make([]reflect.Value, n, n) + // funcInjector.Inject(&in, reflect.ValueOf(ctx)) + // DispatchFuncResult(ctx, fn.Call(in)) + DispatchFuncResult(ctx, funcInjector.Call(reflect.ValueOf(ctx))) } return h, nil diff --git a/mvc/ideas/1/TODO.txt b/mvc/ideas/1/TODO.txt new file mode 100644 index 0000000000..02e3412f65 --- /dev/null +++ b/mvc/ideas/1/TODO.txt @@ -0,0 +1,2 @@ +Remove the "ideas" folder or move this example somewhere in the _examples/mvc or even make a https://medium.com/@kataras +small tutorial about Iris' new MVC implementation. \ No newline at end of file diff --git a/mvc/mvc.go b/mvc/mvc.go index 4f127df349..f94289fa14 100644 --- a/mvc/mvc.go +++ b/mvc/mvc.go @@ -34,6 +34,26 @@ func New(party router.Party) *Application { return newApp(NewEngine(), party) } +// Configure creates a new controller and configures it, +// this function simply calls the `New(party)` and its `.Configure(configurators...)`. +// +// A call of `mvc.New(app.Party("/path").Configure(buildMyMVC)` is equal to +// `mvc.Configure(app.Party("/path"), buildMyMVC)`. +// +// Read more at `New() Application` and `Application#Configure` methods. +func Configure(party router.Party, configurators ...func(*Application)) *Application { + // Author's Notes-> + // About the Configure's comment: +5 space to be shown in equal width to the previous or after line. + // + // About the Configure's design choosen: + // Yes, we could just have a `New(party, configurators...)` + // but I think the `New()` and `Configure(configurators...)` API seems more native to programmers, + // at least to me and the people I ask for their opinion between them. + // Because the `New()` can actually return something that can be fully configured without its `Configure`, + // its `Configure` is there just to design the apps better and help end-devs to split their code wisely. + return New(party).Configure(configurators...) +} + // Configure can be used to pass one or more functions that accept this // Application, use this to add dependencies and controller(s). // @@ -52,9 +72,9 @@ func (app *Application) Configure(configurators ...func(*Application)) *Applicat // will be binded to the controller's field, if matching or to the // controller's methods, if matching. // -// The dependencies can be changed per-controller as well via a `beforeActivate` -// on the `Register` method or when the controller has the `BeforeActivation(b BeforeActivation)` -// method defined. +// These dependencies "values" can be changed per-controller as well, +// via controller's `BeforeActivation` and `AfterActivation` methods, +// look the `Register` method for more. // // It returns this Application. // @@ -68,15 +88,16 @@ func (app *Application) AddDependencies(values ...interface{}) *Application { // It accept any custom struct which its functions will be transformed // to routes. // -// The second, optional and variadic argument is the "beforeActive", -// use that when you want to modify the controller before the activation -// and registration to the main Iris Application. +// If "controller" has `BeforeActivation(b mvc.BeforeActivation)` +// or/and `AfterActivation(a mvc.AfterActivation)` then these will be called between the controller's `.activate`, +// use those when you want to modify the controller before or/and after +// the controller will be registered to the main Iris Application. // -// It returns this Application. +// It returns this mvc Application. // // Example: `.Register(new(TodoController))`. -func (app *Application) Register(controller interface{}, beforeActivate ...func(BeforeActivation)) *Application { - app.Engine.Controller(app.Router, controller, beforeActivate...) +func (app *Application) Register(controller interface{}) *Application { + app.Engine.Controller(app.Router, controller) return app } diff --git a/mvc/param_test.go b/mvc/param_test.go index f9c63329bd..a46d69dfe4 100644 --- a/mvc/param_test.go +++ b/mvc/param_test.go @@ -50,7 +50,9 @@ func TestPathParamBinder(t *testing.T) { t.Fatalf("expected the param 'username' to be '%s' but got '%s'", expectedUsername, got) } - // test the non executed if param not found. + /* this is useless, we don't need to check if ctx.Stopped + inside bindings, this is middleware's job. + // test the non executed if param not found -> this is already done in Iris if real context, so ignore it. executed = false got = "" @@ -63,4 +65,5 @@ func TestPathParamBinder(t *testing.T) { if executed { t.Fatalf("expected the handler to not be executed") } + */ } diff --git a/mvc/session_controller.go b/mvc/session_controller.go index e5670a001e..25b487ab6e 100644 --- a/mvc/session_controller.go +++ b/mvc/session_controller.go @@ -10,6 +10,9 @@ var defaultSessionManager = sessions.New(sessions.Config{}) // SessionController is a simple `Controller` implementation // which requires a binded session manager in order to give // direct access to the current client's session via its `Session` field. +// +// SessionController is deprecated please use the `mvc.Session(manager)` instead, it's more useful, +// also *sessions.Session type can now `Destroy` itself without the need of the manager, embrace it. type SessionController struct { Manager *sessions.Sessions Session *sessions.Session From acebe313fd8f55f09b784ff88b7fb667915d65cc Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 20 Dec 2017 17:56:28 +0200 Subject: [PATCH 35/79] Another new feature: websocket controller, for real --- _examples/README.md | 6 +- _examples/mvc/singleton/main.go | 33 ++++++++ _examples/mvc/websocket/main.go | 82 ++++++++++++++++++++ _examples/mvc/websocket/views/index.html | 63 +++++++++++++++ _examples/websocket/chat/main.go | 3 +- websocket/client.go | 5 +- websocket/client.ts | 5 +- websocket/connection.go | 40 +++++++++- websocket/emitter.go | 6 +- websocket/server.go | 99 +++++++++++++----------- 10 files changed, 281 insertions(+), 61 deletions(-) create mode 100644 _examples/mvc/singleton/main.go create mode 100644 _examples/mvc/websocket/main.go create mode 100644 _examples/mvc/websocket/views/index.html diff --git a/_examples/README.md b/_examples/README.md index 44bb0f2cb7..14f0166667 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -212,8 +212,10 @@ Follow the examples below, - [Hello world](mvc/hello-world/main.go) **UPDATED** - [Session Controller](mvc/session-controller/main.go) **UPDATED** -- [Overview - Plus Repository and Service layers](mvc/overview) **NEW** -- [Login showcase - Plus Repository and Service layers](mvc/login) **NEW** +- [Overview - Plus Repository and Service layers](mvc/overview) **UPDATED** +- [Login showcase - Plus Repository and Service layers](mvc/login) **UPDATED** +- [Singleton](mvc/singleton) **NEW** +- [Websocket Controller](mvc/websocket) **NEW** - - - - {{.Title}} - My App - - - -

{{.MyMessage}}

- - - -``` - -```html - - - - - {{.}}' Portfolio - My App - - - -

Hello {{.}}

- - - -``` - -> Navigate to the [_examples/view](_examples/#view) for more examples -like shared layouts, tmpl funcs, reverse routing and more! - -#### Main - -This file creates any necessary component and links them together. - -```go -// file: main.go - -package main - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datasource" - "github.com/kataras/iris/_examples/mvc/overview/repositories" - "github.com/kataras/iris/_examples/mvc/overview/services" - "github.com/kataras/iris/_examples/mvc/overview/web/controllers" - "github.com/kataras/iris/_examples/mvc/overview/web/middleware" - - "github.com/kataras/iris" -) - -func main() { - app := iris.New() - - // Load the template files. - app.RegisterView(iris.HTML("./web/views", ".html")) - - // Register our controllers. - app.Controller("/hello", new(controllers.HelloController)) - - // Create our movie repository with some (memory) data from the datasource. - repo := repositories.NewMovieRepository(datasource.Movies) - // Create our movie service, we will bind it to the movie controller. - movieService := services.NewMovieService(repo) - - app.Controller("/movies", new(controllers.MovieController), - // Bind the "movieService" to the MovieController's Service (interface) field. - movieService, - // Add the basic authentication(admin:password) middleware - // for the /movies based requests. - middleware.BasicAuth) - - // Start the web server at localhost:8080 - // http://localhost:8080/hello - // http://localhost:8080/hello/iris - // http://localhost:8080/movies - // http://localhost:8080/movies/1 - app.Run( - iris.Addr("localhost:8080"), - iris.WithoutVersionChecker, - iris.WithoutServerError(iris.ErrServerClosed), - iris.WithOptimizations, // enables faster json serialization and more - ) -} -``` - -More folder structure guidelines can be found at the [_examples/#structuring](_examples/#structuring) section. - ## Now you are ready to move to the next step and get closer to becoming a pro gopher Congratulations, since you've made it so far, we've crafted just for you some next level content to turn you into a real pro gopher 😃 diff --git a/README_ZH.md b/README_ZH.md index a4718c1e28..da42f19edd 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -147,834 +147,6 @@ $ go run main.go Iris的一些开发约定可以看看这里[_examples/structuring](_examples/#structuring)。 -### MVC指南 - -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -func main() { - app := iris.New() - - app.Controller("/helloworld", new(HelloWorldController)) - - app.Run(iris.Addr("localhost:8080")) -} - -type HelloWorldController struct { - mvc.C - - // [ Your fields here ] - // Request lifecycle data - // Models - // Database - // Global properties -} - -// -// GET: /helloworld - -func (c *HelloWorldController) Get() string { - return "This is my default action..." -} - -// -// GET: /helloworld/{name:string} - -func (c *HelloWorldController) GetBy(name string) string { - return "Hello " + name -} - -// -// GET: /helloworld/welcome - -func (c *HelloWorldController) GetWelcome() (string, int) { - return "This is the GetWelcome action func...", iris.StatusOK -} - -// -// GET: /helloworld/welcome/{name:string}/{numTimes:int} - -func (c *HelloWorldController) GetWelcomeBy(name string, numTimes int) { - // Access to the low-level Context, - // output arguments are optional of course so we don't have to use them here. - c.Ctx.Writef("Hello %s, NumTimes is: %d", name, numTimes) -} - -``` -> [_examples/mvc](_examples/mvc) 和 [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) 两个简单的例子可以让你更好的了解 Iris MVC 的使用方式 - -每一个在controller中导出的Go方法名都和HTTP方法(`Get`, `Post`, `Put`, `Delete`...) 一一对应 - -在Web应用中一个HTTP访问的资源就是一个URL(统一资源定位符),比如`http://localhost:8080/helloworld`是由HTTP协议、Web服务网络位置(包括TCP端口):`localhost:8080`以及资源名称URI(统一资源标志符) `/helloworld`组成的。 - -上面例子第一个方法映射到[HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp)方法,访问资源是"/helloworld",第三个方法映射到[HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp)方法,访问资源是"/helloworld/welcome" - - -Controller在处理`GetBy`方法时可以识别路径‘name’参数,`GetWelcomeBy`方法可以识别路径‘name’和‘numTimes’参数,因为Controller在识别`By`关键字后可以动态灵活的处理路由;上面第四个方法指示使用 [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp)方法,而且只处理以"/helloworld/welcome"开头的资源位置路径,并且此路径还得包括两部分,第一部分类型没有限制,第二部分只能是数字类型,比如"http://localhost:8080/helloworld/welcome/golang/32719" 是合法的,其它的就会给客户端返回[404 找不到](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5)的提示 - - -### MVC 快速指南 2 - -Iris对MVC的支持非常**棒[看看基准测试](_benchmarks)** ,Iris通过方法的返回值,可以给客户端返回任意类型的数据: - -* 如果返回的是 `string` 类型,就直接给客户端返回字符串 -* 如果第二个返回值是 `string` 类型,那么这个值就是ContentType(HTTP header)的值 -* 如果返回的是 `int` 类型,这个值就是HTTP状态码 -* 如果返回 `error` 值不是空,Iris 将会把这个值作为HTTP 400页面的返回值内容 -*  如果返回 `(int, error)` 类型,并且error不为空,那么Iris返回error的内容,同时把 `int` 值作为HTTP状态码 -* 如果返回 `bool` 类型,并且值是 false ,Iris直接返回404页面 -* 如果返回自定义` struct` 、 `interface{}` 、 `slice` 及 `map` ,Iris 将按照JSON的方式返回,注意如果第二个返回值是 `string`,那么Iris就按照这个 `string` 值的ContentType处理了(不一定是'application/json') -*  如果 `mvc.Result` 调用了 `Dispatch` 函数, 就会按照自己的逻辑重新处理 - -下面这些例子仅供参考,生产环境谨慎使用 - -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/middleware/basicauth" - "github.com/kataras/iris/mvc" -) - -// Movie 是自定义数据结构 -type Movie struct { - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} - -// movies 对象模拟数据源 -var movies = []Movie{ - { - Name: "Casablanca", - Year: 1942, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", - }, - { - Name: "Gone with the Wind", - Year: 1939, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", - }, - { - Name: "Citizen Kane", - Year: 1941, - Genre: "Mystery", - Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", - }, - { - Name: "The Wizard of Oz", - Year: 1939, - Genre: "Fantasy", - Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", - }, -} - - -var basicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) - - -func main() { - app := iris.New() - - app.Use(basicAuth) - - app.Controller("/movies", new(MoviesController)) - - app.Run(iris.Addr(":8080")) -} - -// MoviesController 是 /movies controller. -type MoviesController struct { - mvc.C -} - -// 返回 movies列表 -// 例子: -// curl -i http://localhost:8080/movies -func (c *MoviesController) Get() []Movie { - return movies -} - -// GetBy 返回一个 movie -// 例子: -// curl -i http://localhost:8080/movies/1 -func (c *MoviesController) GetBy(id int) Movie { - return movies[id] -} - -// PutBy 更新一个 movie -// 例子: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MoviesController) PutBy(id int) Movie { -    // 获取一个 movie - m := movies[id] - -    // 获取一个poster文件 -    file, info, err := c.Ctx.FormFile("poster") - if err != nil { - c.Ctx.StatusCode(iris.StatusInternalServerError) - return Movie{} - } -    file.Close()           // 我们不需要这个文件 -    poster := info.Filename // 比如这就是上传的文件url - genre := c.Ctx.FormValue("genre") - -    // 更新poster - m.Poster = poster - m.Genre = genre - movies[id] = m - - return m -} - -// DeleteBy 删除一个 movie -// 例子: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MoviesController) DeleteBy(id int) iris.Map { -    //从movies slice中删除索引 -    deleted := movies[id].Name - movies = append(movies[:id], movies[id+1:]...) -    // 返回删除movie的名称 -    return iris.Map{"deleted": deleted} -} -``` - -### MVC 快速指南 3 - -Iris是一个底层的Web开发框架,如果你喜欢按 **目录结构** 的约定方式开发,那么Iris框架对此毫无影响。 - -你可以根据自己的需求来创建目录结构,但是我建议你还是最好看看如下的目录结构例子: - -[![目录结构例子](_examples/mvc/overview/folder_structure.png)](_examples/mvc/overview) - -好了,直接上代码。 - - -#### 数据模型层 - -```go -// file: datamodels/movie.go - -package datamodels - -// Movie是我们例子数据结构 -// 此Movie可能会定义在类似"web/viewmodels/movie.go"的文件 -// Movie的数据模型在应用中只有一个,这样使用就很简单了 -type Movie struct { - ID int64 `json:"id"` - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} -``` - -#### 数据层 / 数据存储层 - -```go -// file: datasource/movies.go - -package datasource - -import "github.com/kataras/iris/_examples/mvc/overview/datamodels" - -// Movies是模拟的数据源 -var Movies = map[int64]datamodels.Movie{ - 1: { - ID: 1, - Name: "Casablanca", - Year: 1942, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", - }, - 2: { - ID: 2, - Name: "Gone with the Wind", - Year: 1939, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", - }, - 3: { - ID: 3, - Name: "Citizen Kane", - Year: 1941, - Genre: "Mystery", - Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", - }, - 4: { - ID: 4, - Name: "The Wizard of Oz", - Year: 1939, - Genre: "Fantasy", - Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", - }, - 5: { - ID: 5, - Name: "North by Northwest", - Year: 1959, - Genre: "Thriller", - Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg", - }, -} -``` - -#### 数据仓库 - -数据仓库层直接访问数据源 - -```go -// file: repositories/movie_repository.go - -package repositories - -import ( - "errors" - "sync" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" -) - -// Query 是数据访问的集合入口 -type Query func(datamodels.Movie) bool - -// MovieRepository 中会有对movie实体的基本操作 -type MovieRepository interface { - Exec(query Query, action Query, limit int, mode int) (ok bool) - - Select(query Query) (movie datamodels.Movie, found bool) - SelectMany(query Query, limit int) (results []datamodels.Movie) - - InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error) - Delete(query Query, limit int) (deleted bool) -} - -// NewMovieRepository 返回movie内存数据 -func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository { - return &movieMemoryRepository{source: source} -} - -// movieMemoryRepository 就是 "MovieRepository",它管理movie的内存数据 -type movieMemoryRepository struct { - source map[int64]datamodels.Movie - mu sync.RWMutex -} - -const ( -    // 只读模式 - ReadOnlyMode = iota -    // 读写模式 - ReadWriteMode -) - -func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) { - loops := 0 - - if mode == ReadOnlyMode { - r.mu.RLock() - defer r.mu.RUnlock() - } else { - r.mu.Lock() - defer r.mu.Unlock() - } - - for _, movie := range r.source { - ok = query(movie) - if ok { - if action(movie) { - loops++ - if actionLimit >= loops { - break // break - } - } - } - } - - return -} - -// Select方法返回从模拟数据源找出的一个movie数据。 -// 当找到时就返回true,并停止迭代 -// -// Select 将会返回查询到的最新找到的movie数据,这样可以减少代码量 -// -// 自从我第一次想到用这种简单的原型函数后,我就经常用它了,希望这也对你有用 -func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) { - found = r.Exec(query, func(m datamodels.Movie) bool { - movie = m - return true - }, 1, ReadOnlyMode) - -    // 如果没有找到就让datamodels.Movie为空 -    // set an empty datamodels.Movie if not found at all. - if !found { - movie = datamodels.Movie{} - } - - return -} - -// 如果要查找很多值,用法基本一致,不过会返回datamodels.Movie slice。 -// 如果limit<=0,将返回全部数据 -func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) { - r.Exec(query, func(m datamodels.Movie) bool { - results = append(results, m) - return true - }, limit, ReadOnlyMode) - - return -} - -// 插入或更新数据 -// -// 返回一个新的movie对象和error对象 -func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) { - id := movie.ID - - if id == 0 { // Create new action - var lastID int64 -        // 为了数据不重复,找到最大的ID。 -        // 生产环境你可以用第三方库生成一个UUID字串 - r.mu.RLock() - for _, item := range r.source { - if item.ID > lastID { - lastID = item.ID - } - } - r.mu.RUnlock() - - id = lastID + 1 - movie.ID = id - - // map-specific thing - r.mu.Lock() - r.source[id] = movie - r.mu.Unlock() - - return movie, nil - } -    //通过movie.ID更新数据 -    //这里举个例子看如果更新非空的poster和genre -    //其实我们可以直接更新对象r.source[id] = movie -    //用Select的话如下所示 - current, exists := r.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) - -    if !exists { // ID不存在,返回error ID - return datamodels.Movie{}, errors.New("failed to update a nonexistent movie") - } - -    // 或者直接对象操作替换 -    // or comment these and r.source[id] = m for pure replace - if movie.Poster != "" { - current.Poster = movie.Poster - } - - if movie.Genre != "" { - current.Genre = movie.Genre - } - -    // 类map结构的处理 -    r.mu.Lock() - r.source[id] = current - r.mu.Unlock() - - return movie, nil -} - -func (r *movieMemoryRepository) Delete(query Query, limit int) bool { - return r.Exec(query, func(m datamodels.Movie) bool { - delete(r.source, m.ID) - return true - }, limit, ReadWriteMode) -} -``` - -#### 服务层 - -服务层主要调用“数据仓库”和“数据模型”的方法(即使是数据模型很简单的应用)。这一层将包含主要的数据处理逻辑。 - - -```go -// file: services/movie_service.go - -package services - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - "github.com/kataras/iris/_examples/mvc/overview/repositories" -) - -// MovieService主要包括对movie的CRUID(增删改查)操作。 -// MovieService主要调用movie 数据仓库的方法。 -// 下面例子的数据源是更高级别的组件 -// 这样可以用同样的逻辑可以返回不同的数据仓库 -// MovieService是一个接口,任何实现的地方都能用,这样可以替换不同的业务逻辑用来测试 -type MovieService interface { - GetAll() []datamodels.Movie - GetByID(id int64) (datamodels.Movie, bool) - DeleteByID(id int64) bool - UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) -} - -// NewMovieService 返回一个 movie 服务. -func NewMovieService(repo repositories.MovieRepository) MovieService { - return &movieService{ - repo: repo, - } -} - -type movieService struct { - repo repositories.MovieRepository -} - -// GetAll 返回所有 movies. -func (s *movieService) GetAll() []datamodels.Movie { - return s.repo.SelectMany(func(_ datamodels.Movie) bool { - return true - }, -1) -} - -// GetByID 是通过id找到movie. -func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) { - return s.repo.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) -} - - -// UpdatePosterAndGenreByID 更新一个 movie的 poster 和 genre. -func (s *movieService) UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) { - // update the movie and return it. - return s.repo.InsertOrUpdate(datamodels.Movie{ - ID: id, - Poster: poster, - Genre: genre, - }) -} - -// DeleteByID 通过id删除一个movie -// -// 返回true表示成功,其它都是失败 -func (s *movieService) DeleteByID(id int64) bool { - return s.repo.Delete(func(m datamodels.Movie) bool { - return m.ID == id - }, 1) -} -``` - -#### 视图模型 - -视图模型将处理结果返回给客户端 - -例子: -Example: - -```go -import ( - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - - "github.com/kataras/iris/context" -) - -type Movie struct { - datamodels.Movie -} - -func (m Movie) IsValid() bool { -    /* 做一些检测,如果ID合法就返回true */ - return m.ID > 0 -} -``` - -Iris允许在HTTP Response Dispatcher中使用任何自定义数据结构, -所以理论上来说,除非万不得已,下面的代码不建议使用 - -```go -// Dispatch实现了`kataras/iris/mvc#Result`接口。在函数最后发送了一个`Movie`对象作为http response对象。 -// 如果ID小于等于0就回返回404,或者就返回json数据。 -//(这样就像控制器的方法默认返回自定义类型一样) -// -// 不要在这里写过多的代码,应用的主要逻辑不在这里 -// 在方法返回之前可以做个简单验证处理等等; -// -// 这里只是一个小例子,想想这个优势在设计大型应用是很有作用的 -// -// 这个方法是在`Movie`类型的控制器调用的。 -// 例子在这里:`controllers/movie_controller.go#GetBy`。 -func (m Movie) Dispatch(ctx context.Context) { - if !m.IsValid() { - ctx.NotFound() - return - } - ctx.JSON(m, context.JSON{Indent: " "}) -} -``` -然而,我们仅仅用"datamodels"作为一个数据模型包,是因为Movie数据结构没有包含敏感数据,客户端可以访问到其所有字段,我们不需要再有额外的功能去做验证处理了 - - -#### 控制器 - -控制器处理Web请求,它是服务层和客户端之间的桥梁 - -```go -// file: web/controllers/movie_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - "github.com/kataras/iris/_examples/mvc/overview/services" - - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -// MovieController是/movies的控制器 -type MovieController struct { - mvc.C - -    // MovieService是一个接口,主app对象会持有它 - Service services.MovieService -} - -// 获取movies列表 -// 例子: -// curl -i http://localhost:8080/movies -// -// 如果你有一些敏感的数据要处理的话,可以按照如下所示的方式: -// func (c *MovieController) Get() (results []viewmodels.Movie) { -// data := c.Service.GetAll() -// -// for _, movie := range data { -// results = append(results, viewmodels.Movie{movie}) -// } -// return -// } -//否则直接返回数据模型 -func (c *MovieController) Get() (results []datamodels.Movie) { - return c.Service.GetAll() -} - -// GetBy返回一个movie对象 -// 例子: -// curl -i http://localhost:8080/movies/1 -func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) { -    return c.Service.GetByID(id) // 404 没有找到 -} - -// PutBy更新一个movie. -// 例子: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MovieController) PutBy(id int64) (datamodels.Movie, error) { -    // 从请求中获取poster和genre - file, info, err := c.Ctx.FormFile("poster") - if err != nil { - return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") - } -    // 关闭文件 -    file.Close() - -    //想象这就是一个上传文件的url - poster := info.Filename - genre := c.Ctx.FormValue("genre") - - return c.Service.UpdatePosterAndGenreByID(id, poster, genre) -} - -// DeleteBy删除一个movie对象 -// 例子: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MovieController) DeleteBy(id int64) interface{} { - wasDel := c.Service.DeleteByID(id) - if wasDel { -        // 返回要删除的ID - return iris.Map{"deleted": id} - } -    //现在我们可以看到这里可以返回一个有2个返回值(map或int)的函数 -    //我们并没有指定一个返回的类型 - return iris.StatusBadRequest -} -``` - -```go -// file: web/controllers/hello_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/mvc" -) - -// HelloController是控制器的例子 -// 下面会处理GET: /hello and GET: /hello/{name} -type HelloController struct { - mvc.C -} - -var helloView = mvc.View{ - Name: "hello/index.html", - Data: map[string]interface{}{ - "Title": "Hello Page", - "MyMessage": "Welcome to my awesome website", - }, -} - -// Get会返回预定义绑定数据的视图 -// -// `mvc.Result`是一个含有`Dispatch`方法的接口 -// `mvc.Response` 和 `mvc.View` dispatchers 内置类型 -// 你也可以通过实现`github.com/kataras/iris/mvc#Result`接口来自定义dispatchers -func (c *HelloController) Get() mvc.Result { - return helloView -} - -// 你可以定义一个标准通用的error -var errBadName = errors.New("bad name") - -//你也可以将error包裹在mvc.Response中,这样就和mvc.Result类型兼容了 -var badName = mvc.Response{Err: errBadName, Code: 400} - -// GetBy 返回 "Hello {name}" response -// 例子: -// curl -i http://localhost:8080/hello/iris -// curl -i http://localhost:8080/hello/anything -func (c *HelloController) GetBy(name string) mvc.Result { - if name != "iris" { - return badName -        // 或者 -        // GetBy(name string) (mvc.Result, error) { - // return nil, errBadName - // } - } - -    // 返回 mvc.Response{Text: "Hello " + name} 或者: - return mvc.View{ - Name: "hello/name.html", - Data: name, - } -} -``` - -```go -// file: web/middleware/basicauth.go - -package middleware - -import "github.com/kataras/iris/middleware/basicauth" - -// BasicAuth 中间件例 -var BasicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) -``` - -```html - - - - - {{.Title}} - My App - - - -

{{.MyMessage}}

- - - -``` - -```html - - - - - {{.}}' Portfolio - My App - - - -

Hello {{.}}

- - - -``` - -> 戳[_examples/view](_examples/#view) 可以找到更多关于layouts,tmpl,routing的例子 - - -#### 程序入口 - -程序入口可以将任何组件包含进来 - -```go -// file: main.go - -package main - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datasource" - "github.com/kataras/iris/_examples/mvc/overview/repositories" - "github.com/kataras/iris/_examples/mvc/overview/services" - "github.com/kataras/iris/_examples/mvc/overview/web/controllers" - "github.com/kataras/iris/_examples/mvc/overview/web/middleware" - - "github.com/kataras/iris" -) - -func main() { - app := iris.New() - -    // 加载模板文件 -    app.RegisterView(iris.HTML("./web/views", ".html")) - -    // 注册控制器 -    app.Controller("/hello", new(controllers.HelloController)) - -    // 创建movie 数据仓库,次仓库包含的是内存级的数据源 -    repo := repositories.NewMovieRepository(datasource.Movies) -    // 创建movie服务, 然后将其与控制器绑定 -    movieService := services.NewMovieService(repo) - - app.Controller("/movies", new(controllers.MovieController), -        // 将"movieService"绑定在 MovieController的Service接口 -        movieService, -        // 为/movies请求添加basic authentication(admin:password)中间件 - middleware.BasicAuth) - -    // 启动应用localhost:8080 - // http://localhost:8080/hello - // http://localhost:8080/hello/iris - // http://localhost:8080/movies - // http://localhost:8080/movies/1 - app.Run( - iris.Addr("localhost:8080"), - iris.WithoutVersionChecker, - iris.WithoutServerError(iris.ErrServerClosed), -        iris.WithOptimizations, // 可以启用快速json序列化等优化配置 -    ) -} -``` - -更多指南戳 [_examples/#structuring](_examples/#structuring) ## 现在你已经准备好进入下一阶段,又向专家级gopher迈进一步了 diff --git a/_examples/mvc/README.md b/_examples/mvc/README.md index a47ef736e1..e898b22e8d 100644 --- a/_examples/mvc/README.md +++ b/_examples/mvc/README.md @@ -133,872 +133,13 @@ By creating components that are independent of one another, developers are able If you're new to back-end web development read about the MVC architectural pattern first, a good start is that [wikipedia article](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). -## Quick MVC Tutorial Part 1 (without output result) +## Examples -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -func main() { - app := iris.New() - - app.Controller("/helloworld", new(HelloWorldController)) - - app.Run(iris.Addr("localhost:8080")) -} - -type HelloWorldController struct { - mvc.C - - // [ Your fields here ] - // Request lifecycle data - // Models - // Database - // Global properties -} - -// -// GET: /helloworld - -func (c *HelloWorldController) Get() string { - return "This is my default action..." -} - -// -// GET: /helloworld/{name:string} - -func (c *HelloWorldController) GetBy(name string) string { - return "Hello " + name -} - -// -// GET: /helloworld/welcome - -func (c *HelloWorldController) GetWelcome() (string, int) { - return "This is the GetWelcome action func...", iris.StatusOK -} - -// -// GET: /helloworld/welcome/{name:string}/{numTimes:int} - -func (c *HelloWorldController) GetWelcomeBy(name string, numTimes int) { - // Access to the low-level Context, - // output arguments are optional of course so we don't have to use them here. - c.Ctx.Writef("Hello %s, NumTimes is: %d", name, numTimes) -} - -/* -func (c *HelloWorldController) Post() {} handles HTTP POST method requests -func (c *HelloWorldController) Put() {} handles HTTP PUT method requests -func (c *HelloWorldController) Delete() {} handles HTTP DELETE method requests -func (c *HelloWorldController) Connect() {} handles HTTP CONNECT method requests -func (c *HelloWorldController) Head() {} handles HTTP HEAD method requests -func (c *HelloWorldController) Patch() {} handles HTTP PATCH method requests -func (c *HelloWorldController) Options() {} handles HTTP OPTIONS method requests -func (c *HelloWorldController) Trace() {} handles HTTP TRACE method requests -*/ - -/* -func (c *HelloWorldController) All() {} handles All method requests -// OR -func (c *HelloWorldController) Any() {} handles All method requests -*/ -``` - -> The [_examples/mvc](https://github.com/kataras/iris/tree/master/_examples/mvc) and [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) files explain each feature with simple paradigms, they show how you can take advantage of the Iris MVC Binder, Iris MVC Models and many more... - -Every `exported` func prefixed with an HTTP Method(`Get`, `Post`, `Put`, `Delete`...) in a controller is callable as an HTTP endpoint. In the sample above, all funcs writes a string to the response. Note the comments preceding each method. - -An HTTP endpoint is a targetable URL in the web application, such as `http://localhost:8080/helloworld`, and combines the protocol used: HTTP, the network location of the web server (including the TCP port): `localhost:8080` and the target URI `/helloworld`. - -The first comment states this is an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld" to the base URL. The third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld/welcome" to the URL. - -Controller knows how to handle the "name" on `GetBy` or the "name" and "numTimes" at `GetWelcomeBy`, because of the `By` keyword, and builds the dynamic route without boilerplate; the third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) dynamic method that is invoked by any URL that starts with "/helloworld/welcome" and followed by two more path parts, the first one can accept any value and the second can accept only numbers, i,e: "http://localhost:8080/helloworld/welcome/golang/32719", otherwise a [404 Not Found HTTP Error](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5) will be sent to the client instead. - ----- - -### Quick MVC Tutorial #2 - -Iris has a very powerful and **blazing [fast](_benchmarks)** MVC support, you can return any value of any type from a method function -and it will be sent to the client as expected. - -* if `string` then it's the body. -* if `string` is the second output argument then it's the content type. -* if `int` then it's the status code. -* if `error` and not nil then (any type) response will be omitted and error's text with a 400 bad request will be rendered instead. -* if `(int, error)` and error is not nil then the response result will be the error's text with the status code as `int`. -* if `bool` is false then it throws 404 not found http error by skipping everything else. -* if `custom struct` or `interface{}` or `slice` or `map` then it will be rendered as json, unless a `string` content type is following. -* if `mvc.Result` then it executes its `Dispatch` function, so good design patters can be used to split the model's logic where needed. - -The example below is not intended to be used in production but it's a good showcase of some of the return types we saw before; - -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/middleware/basicauth" - "github.com/kataras/iris/mvc" -) - -// Movie is our sample data structure. -type Movie struct { - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} - -// movies contains our imaginary data source. -var movies = []Movie{ - { - Name: "Casablanca", - Year: 1942, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", - }, - { - Name: "Gone with the Wind", - Year: 1939, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", - }, - { - Name: "Citizen Kane", - Year: 1941, - Genre: "Mystery", - Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", - }, - { - Name: "The Wizard of Oz", - Year: 1939, - Genre: "Fantasy", - Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", - }, -} - - -var basicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) - - -func main() { - app := iris.New() - - app.Use(basicAuth) - - app.Controller("/movies", new(MoviesController)) - - app.Run(iris.Addr(":8080")) -} - -// MoviesController is our /movies controller. -type MoviesController struct { - mvc.C -} - -// Get returns list of the movies -// Demo: -// curl -i http://localhost:8080/movies -func (c *MoviesController) Get() []Movie { - return movies -} - -// GetBy returns a movie -// Demo: -// curl -i http://localhost:8080/movies/1 -func (c *MoviesController) GetBy(id int) Movie { - return movies[id] -} - -// PutBy updates a movie -// Demo: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MoviesController) PutBy(id int) Movie { - // get the movie - m := movies[id] - - // get the request data for poster and genre - file, info, err := c.Ctx.FormFile("poster") - if err != nil { - c.Ctx.StatusCode(iris.StatusInternalServerError) - return Movie{} - } - file.Close() // we don't need the file - poster := info.Filename // imagine that as the url of the uploaded file... - genre := c.Ctx.FormValue("genre") - - // update the poster - m.Poster = poster - m.Genre = genre - movies[id] = m - - return m -} - -// DeleteBy deletes a movie -// Demo: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MoviesController) DeleteBy(id int) iris.Map { - // delete the entry from the movies slice - deleted := movies[id].Name - movies = append(movies[:id], movies[id+1:]...) - // and return the deleted movie's name - return iris.Map{"deleted": deleted} -} -``` - -### Quick MVC Tutorial #3 - -Nothing stops you from using your favorite **folder structure**. Iris is a low level web framework, it has got MVC first-class support but it doesn't limit your folder structure, this is your choice. - -Structuring depends on your own needs. We can't tell you how to design your own application for sure but you're free to take a closer look to one typical example below; - -[![folder structure example](_examples/mvc/overview/folder_structure.png)](_examples/mvc/overview) - -Shhh, let's spread the code itself. - -#### Data Model Layer - -```go -// file: datamodels/movie.go - -package datamodels - -// Movie is our sample data structure. -// Keep note that the tags for public-use (for our web app) -// should be kept in other file like "web/viewmodels/movie.go" -// which could wrap by embedding the datamodels.Movie or -// declare new fields instead butwe will use this datamodel -// as the only one Movie model in our application, -// for the shake of simplicty. -type Movie struct { - ID int64 `json:"id"` - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} -``` - -#### Data Source / Data Store Layer - -```go -// file: datasource/movies.go - -package datasource - -import "github.com/kataras/iris/_examples/mvc/overview/datamodels" - -// Movies is our imaginary data source. -var Movies = map[int64]datamodels.Movie{ - 1: { - ID: 1, - Name: "Casablanca", - Year: 1942, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", - }, - 2: { - ID: 2, - Name: "Gone with the Wind", - Year: 1939, - Genre: "Romance", - Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", - }, - 3: { - ID: 3, - Name: "Citizen Kane", - Year: 1941, - Genre: "Mystery", - Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", - }, - 4: { - ID: 4, - Name: "The Wizard of Oz", - Year: 1939, - Genre: "Fantasy", - Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", - }, - 5: { - ID: 5, - Name: "North by Northwest", - Year: 1959, - Genre: "Thriller", - Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg", - }, -} -``` - -#### Repositories - -The layer which has direct access to the "datasource" and can manipulate data directly. - -```go -// file: repositories/movie_repository.go - -package repositories - -import ( - "errors" - "sync" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" -) - -// Query represents the visitor and action queries. -type Query func(datamodels.Movie) bool - -// MovieRepository handles the basic operations of a movie entity/model. -// It's an interface in order to be testable, i.e a memory movie repository or -// a connected to an sql database. -type MovieRepository interface { - Exec(query Query, action Query, limit int, mode int) (ok bool) - - Select(query Query) (movie datamodels.Movie, found bool) - SelectMany(query Query, limit int) (results []datamodels.Movie) - - InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error) - Delete(query Query, limit int) (deleted bool) -} - -// NewMovieRepository returns a new movie memory-based repository, -// the one and only repository type in our example. -func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository { - return &movieMemoryRepository{source: source} -} - -// movieMemoryRepository is a "MovieRepository" -// which manages the movies using the memory data source (map). -type movieMemoryRepository struct { - source map[int64]datamodels.Movie - mu sync.RWMutex -} - -const ( - // ReadOnlyMode will RLock(read) the data . - ReadOnlyMode = iota - // ReadWriteMode will Lock(read/write) the data. - ReadWriteMode -) - -func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) { - loops := 0 - - if mode == ReadOnlyMode { - r.mu.RLock() - defer r.mu.RUnlock() - } else { - r.mu.Lock() - defer r.mu.Unlock() - } - - for _, movie := range r.source { - ok = query(movie) - if ok { - if action(movie) { - loops++ - if actionLimit >= loops { - break // break - } - } - } - } - - return -} - -// Select receives a query function -// which is fired for every single movie model inside -// our imaginary data source. -// When that function returns true then it stops the iteration. -// -// It returns the query's return last known "found" value -// and the last known movie model -// to help callers to reduce the LOC. -// -// It's actually a simple but very clever prototype function -// I'm using everywhere since I firstly think of it, -// hope you'll find it very useful as well. -func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) { - found = r.Exec(query, func(m datamodels.Movie) bool { - movie = m - return true - }, 1, ReadOnlyMode) - - // set an empty datamodels.Movie if not found at all. - if !found { - movie = datamodels.Movie{} - } - - return -} - -// SelectMany same as Select but returns one or more datamodels.Movie as a slice. -// If limit <=0 then it returns everything. -func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) { - r.Exec(query, func(m datamodels.Movie) bool { - results = append(results, m) - return true - }, limit, ReadOnlyMode) - - return -} - -// InsertOrUpdate adds or updates a movie to the (memory) storage. -// -// Returns the new movie and an error if any. -func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) { - id := movie.ID - - if id == 0 { // Create new action - var lastID int64 - // find the biggest ID in order to not have duplications - // in productions apps you can use a third-party - // library to generate a UUID as string. - r.mu.RLock() - for _, item := range r.source { - if item.ID > lastID { - lastID = item.ID - } - } - r.mu.RUnlock() - - id = lastID + 1 - movie.ID = id - - // map-specific thing - r.mu.Lock() - r.source[id] = movie - r.mu.Unlock() - - return movie, nil - } - - // Update action based on the movie.ID, - // here we will allow updating the poster and genre if not empty. - // Alternatively we could do pure replace instead: - // r.source[id] = movie - // and comment the code below; - current, exists := r.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) - - if !exists { // ID is not a real one, return an error. - return datamodels.Movie{}, errors.New("failed to update a nonexistent movie") - } - - // or comment these and r.source[id] = m for pure replace - if movie.Poster != "" { - current.Poster = movie.Poster - } - - if movie.Genre != "" { - current.Genre = movie.Genre - } - - // map-specific thing - r.mu.Lock() - r.source[id] = current - r.mu.Unlock() - - return movie, nil -} - -func (r *movieMemoryRepository) Delete(query Query, limit int) bool { - return r.Exec(query, func(m datamodels.Movie) bool { - delete(r.source, m.ID) - return true - }, limit, ReadWriteMode) -} -``` - -#### Services - -The layer which has access to call functions from the "repositories" and "models" (or even "datamodels" if simple application). It should contain the most of the domain logic. - -```go -// file: services/movie_service.go - -package services - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - "github.com/kataras/iris/_examples/mvc/overview/repositories" -) - -// MovieService handles some of the CRUID operations of the movie datamodel. -// It depends on a movie repository for its actions. -// It's here to decouple the data source from the higher level compoments. -// As a result a different repository type can be used with the same logic without any aditional changes. -// It's an interface and it's used as interface everywhere -// because we may need to change or try an experimental different domain logic at the future. -type MovieService interface { - GetAll() []datamodels.Movie - GetByID(id int64) (datamodels.Movie, bool) - DeleteByID(id int64) bool - UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) -} - -// NewMovieService returns the default movie service. -func NewMovieService(repo repositories.MovieRepository) MovieService { - return &movieService{ - repo: repo, - } -} - -type movieService struct { - repo repositories.MovieRepository -} - -// GetAll returns all movies. -func (s *movieService) GetAll() []datamodels.Movie { - return s.repo.SelectMany(func(_ datamodels.Movie) bool { - return true - }, -1) -} - -// GetByID returns a movie based on its id. -func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) { - return s.repo.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) -} - -// UpdatePosterAndGenreByID updates a movie's poster and genre. -func (s *movieService) UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) { - // update the movie and return it. - return s.repo.InsertOrUpdate(datamodels.Movie{ - ID: id, - Poster: poster, - Genre: genre, - }) -} - -// DeleteByID deletes a movie by its id. -// -// Returns true if deleted otherwise false. -func (s *movieService) DeleteByID(id int64) bool { - return s.repo.Delete(func(m datamodels.Movie) bool { - return m.ID == id - }, 1) -} -``` - -#### View Models - -There should be the view models, the structure that the client will be able to see. - -Example: - -```go -import ( - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - - "github.com/kataras/iris/context" -) - -type Movie struct { - datamodels.Movie -} - -func (m Movie) IsValid() bool { - /* do some checks and return true if it's valid... */ - return m.ID > 0 -} -``` - -Iris is able to convert any custom data Structure into an HTTP Response Dispatcher, -so theoretically, something like the following is permitted if it's really necessary; - -```go -// Dispatch completes the `kataras/iris/mvc#Result` interface. -// Sends a `Movie` as a controlled http response. -// If its ID is zero or less then it returns a 404 not found error -// else it returns its json representation, -// (just like the controller's functions do for custom types by default). -// -// Don't overdo it, the application's logic should not be here. -// It's just one more step of validation before the response, -// simple checks can be added here. -// -// It's just a showcase, -// imagine the potentials this feature gives when designing a bigger application. -// -// This is called where the return value from a controller's method functions -// is type of `Movie`. -// For example the `controllers/movie_controller.go#GetBy`. -func (m Movie) Dispatch(ctx context.Context) { - if !m.IsValid() { - ctx.NotFound() - return - } - ctx.JSON(m, context.JSON{Indent: " "}) -} -``` - -However, we will use the "datamodels" as the only one models package because -Movie structure doesn't contain any sensitive data, clients are able to see all of its fields -and we don't need any extra functionality or validation inside it. - -#### Controllers - -Handles web requests, bridge between the services and the client. - -```go -// file: web/controllers/movie_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" - "github.com/kataras/iris/_examples/mvc/overview/services" - - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -// MovieController is our /movies controller. -type MovieController struct { - mvc.C - - // Our MovieService, it's an interface which - // is binded from the main application. - Service services.MovieService -} - -// Get returns list of the movies. -// Demo: -// curl -i http://localhost:8080/movies -// -// The correct way if you have sensitive data: -// func (c *MovieController) Get() (results []viewmodels.Movie) { -// data := c.Service.GetAll() -// -// for _, movie := range data { -// results = append(results, viewmodels.Movie{movie}) -// } -// return -// } -// otherwise just return the datamodels. -func (c *MovieController) Get() (results []datamodels.Movie) { - return c.Service.GetAll() -} - -// GetBy returns a movie. -// Demo: -// curl -i http://localhost:8080/movies/1 -func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) { - return c.Service.GetByID(id) // it will throw 404 if not found. -} - -// PutBy updates a movie. -// Demo: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MovieController) PutBy(id int64) (datamodels.Movie, error) { - // get the request data for poster and genre - file, info, err := c.Ctx.FormFile("poster") - if err != nil { - return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") - } - // we don't need the file so close it now. - file.Close() - - // imagine that is the url of the uploaded file... - poster := info.Filename - genre := c.Ctx.FormValue("genre") - - return c.Service.UpdatePosterAndGenreByID(id, poster, genre) -} - -// DeleteBy deletes a movie. -// Demo: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MovieController) DeleteBy(id int64) interface{} { - wasDel := c.Service.DeleteByID(id) - if wasDel { - // return the deleted movie's ID - return iris.Map{"deleted": id} - } - // right here we can see that a method function can return any of those two types(map or int), - // we don't have to specify the return type to a specific type. - return iris.StatusBadRequest -} -``` - -```go -// file: web/controllers/hello_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/mvc" -) - -// HelloController is our sample controller -// it handles GET: /hello and GET: /hello/{name} -type HelloController struct { - mvc.C -} - -var helloView = mvc.View{ - Name: "hello/index.html", - Data: map[string]interface{}{ - "Title": "Hello Page", - "MyMessage": "Welcome to my awesome website", - }, -} - -// Get will return a predefined view with bind data. -// -// `mvc.Result` is just an interface with a `Dispatch` function. -// `mvc.Response` and `mvc.View` are the built'n result type dispatchers -// you can even create custom response dispatchers by -// implementing the `github.com/kataras/iris/mvc#Result` interface. -func (c *HelloController) Get() mvc.Result { - return helloView -} - -// you can define a standard error in order to be re-usable anywhere in your app. -var errBadName = errors.New("bad name") - -// you can just return it as error or even better -// wrap this error with an mvc.Response to make it an mvc.Result compatible type. -var badName = mvc.Response{Err: errBadName, Code: 400} - -// GetBy returns a "Hello {name}" response. -// Demos: -// curl -i http://localhost:8080/hello/iris -// curl -i http://localhost:8080/hello/anything -func (c *HelloController) GetBy(name string) mvc.Result { - if name != "iris" { - return badName - // or - // GetBy(name string) (mvc.Result, error) { - // return nil, errBadName - // } - } - - // return mvc.Response{Text: "Hello " + name} OR: - return mvc.View{ - Name: "hello/name.html", - Data: name, - } -} -``` - -```go -// file: web/middleware/basicauth.go - -package middleware - -import "github.com/kataras/iris/middleware/basicauth" - -// BasicAuth middleware sample. -var BasicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) -``` - -```html - - - - - {{.Title}} - My App - - - -

{{.MyMessage}}

- - - -``` - -```html - - - - - {{.}}' Portfolio - My App - - - -

Hello {{.}}

- - - -``` - -> Navigate to the [_examples/view](https://github.com/kataras/iris/tree/master/_examples/#view) for more examples -like shared layouts, tmpl funcs, reverse routing and more! - -#### Main - -This file creates any necessary component and links them together. - -```go -// file: main.go - -package main - -import ( - "github.com/kataras/iris/_examples/mvc/overview/datasource" - "github.com/kataras/iris/_examples/mvc/overview/repositories" - "github.com/kataras/iris/_examples/mvc/overview/services" - "github.com/kataras/iris/_examples/mvc/overview/web/controllers" - "github.com/kataras/iris/_examples/mvc/overview/web/middleware" - - "github.com/kataras/iris" -) - -func main() { - app := iris.New() - - // Load the template files. - app.RegisterView(iris.HTML("./web/views", ".html")) - - // Register our controllers. - app.Controller("/hello", new(controllers.HelloController)) - - // Create our movie repository with some (memory) data from the datasource. - repo := repositories.NewMovieRepository(datasource.Movies) - // Create our movie service, we will bind it to the movie controller. - movieService := services.NewMovieService(repo) - - app.Controller("/movies", new(controllers.MovieController), - // Bind the "movieService" to the MovieController's Service (interface) field. - movieService, - // Add the basic authentication(admin:password) middleware - // for the /movies based requests. - middleware.BasicAuth) - - // Start the web server at localhost:8080 - // http://localhost:8080/hello - // http://localhost:8080/hello/iris - // http://localhost:8080/movies - // http://localhost:8080/movies/1 - app.Run( - iris.Addr("localhost:8080"), - iris.WithoutVersionChecker, - iris.WithoutServerError(iris.ErrServerClosed), - iris.WithOptimizations, // enables faster json serialization and more - ) -} -``` +- [Hello world](mvc/hello-world/main.go) **UPDATED** +- [Session Controller](mvc/session-controller/main.go) **UPDATED** +- [Overview - Plus Repository and Service layers](mvc/overview) **UPDATED** +- [Login showcase - Plus Repository and Service layers](mvc/login) **UPDATED** +- [Singleton](mvc/singleton) **NEW** +- [Websocket Controller](mvc/websocket) **NEW** -More folder structure guidelines can be found at the [_examples/#structuring](https://github.com/kataras/iris/tree/master/_examples/#structuring) section. \ No newline at end of file +Folder structure guidelines can be found at the [_examples/#structuring](https://github.com/kataras/iris/tree/master/_examples/#structuring) section. \ No newline at end of file From d2251f4f681488ebe6784235c9905f564c56150a Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Fri, 22 Dec 2017 11:07:13 +0200 Subject: [PATCH 40/79] add some comment docs at the mvc/controller.go --- mvc/controller.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/mvc/controller.go b/mvc/controller.go index 78ec9c6121..1c44b97b6d 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -27,11 +27,27 @@ type shared interface { Handle(httpMethod, path, funcName string, middleware ...context.Handler) *router.Route } +// BeforeActivation is being used as the onle one input argument of a +// `func(c *Controller) BeforeActivation(b mvc.BeforeActivation) {}`. +// +// It's being called before the controller's dependencies binding to the fields or the input arguments +// but before server ran. +// +// It's being used to customize a controller if needed inside the controller itself, +// it's called once per application. type BeforeActivation interface { shared Dependencies() *di.Values } +// AfterActivation is being used as the onle one input argument of a +// `func(c *Controller) AfterActivation(a mvc.AfterActivation) {}`. +// +// It's being called after the `BeforeActivation`, +// and after controller's dependencies binded to the fields or the input arguments but before server ran. +// +// It's being used to customize a controller if needed inside the controller itself, +// it's called once per application. type AfterActivation interface { shared DependenciesReadOnly() ValuesReadOnly @@ -70,6 +86,8 @@ type ControllerActivator struct { injector *di.StructInjector } +// NameOf returns the package name + the struct type's name, +// it's used to take the full name of an Controller, the `ControllerActivator#Name`. func NameOf(v interface{}) string { elemTyp := di.IndirectType(di.ValueOf(v).Type()) @@ -117,10 +135,15 @@ func whatReservedMethods(typ reflect.Type) []string { return methods } +// Dependencies returns the write and read access of the dependencies that are +// came from the parent MVC Application, with this you can customize +// the dependencies per controller, used at the `BeforeActivation`. func (c *ControllerActivator) Dependencies() *di.Values { return &c.dependencies } +// ValuesReadOnly returns the read-only access type of the controller's dependencies. +// Used at `AfterActivation`. type ValuesReadOnly interface { // Has returns true if a binder responsible to // bind and return a type of "typ" is already registered to this controller. @@ -134,14 +157,26 @@ type ValuesReadOnly interface { CloneWithFieldsOf(s interface{}) di.Values } +// DependenciesReadOnly returns the read-only access type of the controller's dependencies. +// Used at `AfterActivation`. func (c *ControllerActivator) DependenciesReadOnly() ValuesReadOnly { return c.dependencies } +// Name returns the full name of the controller, its package name + the type name. +// Can used at both `BeforeActivation` and `AfterActivation`. func (c *ControllerActivator) Name() string { return c.fullName } +// Router is the standard Iris router's public API. +// With this you can register middleware, view layouts, subdomains, serve static files +// and even add custom standard iris handlers as normally. +// +// This Router is the router instance that came from the parent MVC Application, +// it's the `app.Party(...)` argument. +// +// Can used at both `BeforeActivation` and `AfterActivation`. func (c *ControllerActivator) Router() router.Party { return c.router } From c986c6803518e5d1ba685777646553da8e9af73d Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Fri, 22 Dec 2017 11:08:16 +0200 Subject: [PATCH 41/79] misspell fix --- mvc/mvc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mvc/mvc.go b/mvc/mvc.go index f94289fa14..fb56a8174c 100644 --- a/mvc/mvc.go +++ b/mvc/mvc.go @@ -45,7 +45,7 @@ func Configure(party router.Party, configurators ...func(*Application)) *Applica // Author's Notes-> // About the Configure's comment: +5 space to be shown in equal width to the previous or after line. // - // About the Configure's design choosen: + // About the Configure's design chosen: // Yes, we could just have a `New(party, configurators...)` // but I think the `New()` and `Configure(configurators...)` API seems more native to programmers, // at least to me and the people I ask for their opinion between them. From b84062589dd72f1b63bb1f7bdd01175cd53088a4 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 23 Dec 2017 17:07:39 +0200 Subject: [PATCH 42/79] finish the first state of the vuejs todo mvc example, a simple rest api - todo: websocket and live updates between browser tabs with the same session id --- .../login/web/controllers/users_controller.go | 4 +- .../tutorial/vuejs-todo-mvc/src/todo/item.go | 24 +--- .../vuejs-todo-mvc/src/todo/service.go | 50 ++----- .../src/web/controllers/todo_controller.go | 64 ++++----- .../tutorial/vuejs-todo-mvc/src/web/main.go | 18 ++- .../vuejs-todo-mvc/src/web/public/index.html | 122 ++++++++++-------- .../vuejs-todo-mvc/src/web/public/js/app.js | 56 ++++++-- mvc/di/func.go | 16 +++ mvc/di/object.go | 21 ++- mvc/di/struct.go | 9 ++ mvc/di/values.go | 6 + 11 files changed, 216 insertions(+), 174 deletions(-) diff --git a/_examples/mvc/login/web/controllers/users_controller.go b/_examples/mvc/login/web/controllers/users_controller.go index 5e7b9187fd..ed11170630 100644 --- a/_examples/mvc/login/web/controllers/users_controller.go +++ b/_examples/mvc/login/web/controllers/users_controller.go @@ -14,7 +14,7 @@ import ( // DELETE /users/{id:long} | delete by id // Requires basic authentication. type UsersController struct { - // context is auto-binded by Iris on each request, + // Optionally: context is auto-binded by Iris on each request, // remember that on each incoming request iris creates a new UserController each time, // so all fields are request-scoped by-default, only dependency injection is able to set // custom fields like the Service which is the same for all requests (static binding). @@ -84,5 +84,5 @@ func (c *UsersController) DeleteBy(id int64) interface{} { // right here we can see that a method function // can return any of those two types(map or int), // we don't have to specify the return type to a specific type. - return 400 // same as `iris.StatusBadRequest`. + return iris.StatusBadRequest // same as 400. } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go index 744880c4c7..eb464be25c 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go @@ -1,24 +1,8 @@ package todo -type State uint32 - -const ( - StateActive State = iota - StateCompleted -) - -func ParseState(s string) State { - switch s { - case "completed": - return StateCompleted - default: - return StateActive - } -} - type Item struct { - OwnerID string - ID int64 - Body string - CurrentState State + SessionID string `json:"-"` + ID int64 `json:"id,omitempty"` + Title string `json:"title"` + Completed bool `json:"completed"` } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go index 32e53a2c4a..50f3df0312 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go @@ -1,53 +1,31 @@ package todo type Service interface { - GetByID(id int64) (Item, bool) - GetByOwner(owner string) []Item - Complete(item Item) bool - Save(newItem Item) error + Get(owner string) []Item + Save(owner string, newItems []Item) error } type MemoryService struct { - items map[int64]Item + items map[string][]Item } -func (s *MemoryService) getLatestID() (id int64) { - for k := range s.items { - if k > id { - id = k - } - } - - return +func NewMemoryService() *MemoryService { + return &MemoryService{make(map[string][]Item, 0)} } -func (s *MemoryService) GetByID(id int64) (Item, bool) { - item, found := s.items[id] - return item, found +func (s *MemoryService) Get(sessionOwner string) (items []Item) { + return s.items[sessionOwner] } -func (s *MemoryService) GetByOwner(owner string) (items []Item) { - for _, item := range s.items { - if item.OwnerID != owner { - continue +func (s *MemoryService) Save(sessionOwner string, newItems []Item) error { + var prevID int64 + for i := range newItems { + if newItems[i].ID == 0 { + newItems[i].ID = prevID + prevID++ } - items = append(items, item) - } - return -} - -func (s *MemoryService) Complete(item Item) bool { - item.CurrentState = StateCompleted - return s.Save(item) == nil -} - -func (s *MemoryService) Save(newItem Item) error { - if newItem.ID == 0 { - // create - newItem.ID = s.getLatestID() + 1 } - // full replace here for the shake of simplicity) - s.items[newItem.ID] = newItem + s.items[sessionOwner] = newItems return nil } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go index cb671bad9c..5688ed0a52 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -4,9 +4,9 @@ import ( "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo" "github.com/kataras/iris" - "github.com/kataras/iris/sessions" - "github.com/kataras/iris/mvc" + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" ) // TodoController is our TODO app's web controller. @@ -23,55 +23,39 @@ type TodoController struct { func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { // this could be binded to a controller's function input argument // if any, or struct field if any: - b.Dependencies().Add(func(ctx iris.Context) todo.Item { - // ctx.ReadForm(&item) - var ( - owner = ctx.PostValue("owner") - body = ctx.PostValue("body") - state = ctx.PostValue("state") - ) - - return todo.Item{ - OwnerID: owner, - Body: body, - CurrentState: todo.ParseState(state), - } + b.Dependencies().Add(func(ctx iris.Context) (items []todo.Item) { + ctx.ReadJSON(&items) + return }) - - // ca.Router.Use(...).Done(...).Layout(...) } -// Get handles the GET: /todo route. +// Get handles the GET: /todos route. func (c *TodoController) Get() []todo.Item { - return c.Service.GetByOwner(c.Session.ID()) + return c.Service.Get(c.Session.ID()) } -// PutCompleteBy handles the PUT: /todo/complete/{id:long} route. -func (c *TodoController) PutCompleteBy(id int64) int { - item, found := c.Service.GetByID(id) - if !found { - return iris.StatusNotFound - } +// PostItemResponse the response data that will be returned as json +// after a post save action of all todo items. +type PostItemResponse struct { + Success bool `json:"success"` +} - if item.OwnerID != c.Session.ID() { - return iris.StatusForbidden - } +var emptyResponse = PostItemResponse{Success: false} - if !c.Service.Complete(item) { - return iris.StatusBadRequest +// Post handles the POST: /todos route. +func (c *TodoController) Post(newItems []todo.Item) PostItemResponse { + if err := c.Service.Save(c.Session.ID(), newItems); err != nil { + return emptyResponse } - return iris.StatusOK + return PostItemResponse{Success: true} } -// Post handles the POST: /todo route. -func (c *TodoController) Post(newItem todo.Item) int { - if newItem.OwnerID != c.Session.ID() { - return iris.StatusForbidden - } +func (c *TodoController) GetSync(conn websocket.Connection) { + conn.Join(c.Session.ID()) + conn.On("save", func() { // "save" event from client. + conn.To(c.Session.ID()).Emit("saved", nil) // fire a "save" event to the rest of the clients. + }) - if err := c.Service.Save(newItem); err != nil { - return iris.StatusBadRequest - } - return iris.StatusOK + conn.Wait() } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go index dbfaa446b8..0136edfa43 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go @@ -6,12 +6,14 @@ import ( "github.com/kataras/iris" "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" "github.com/kataras/iris/mvc" ) func main() { app := iris.New() + // serve our app in public, public folder // contains the client-side vue.js application, // no need for any server-side template here, @@ -20,20 +22,28 @@ func main() { app.StaticWeb("/", "./public") sess := sessions.New(sessions.Config{ - Cookie: "_iris_session", + Cookie: "iris_session", }) - m := mvc.New(app.Party("/todo")) + ws := websocket.New(websocket.Config{}) + // create our mvc application targeted to /todos relative sub path. + m := mvc.New(app.Party("/todos")) // any dependencies bindings here... m.AddDependencies( + todo.NewMemoryService(), mvc.Session(sess), - new(todo.MemoryService), + ws.Upgrade, ) + // http://localhost:8080/iris-ws.js + // serve the javascript client library to communicate with + // the iris high level websocket event system. + m.Router.Any("/iris-ws.js", websocket.ClientHandler()) + // controllers registration here... m.Register(new(controllers.TodoController)) // start the web server at http://localhost:8080 - app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker, iris.WithOptimizations) + app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker) } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html index f282a65d30..3763042c24 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html @@ -1,63 +1,71 @@ - - - Iris + Vue.js • TodoMVC - - - - - - - -
-
-

todos

- -
-
- -
    -
  • -
    - - - -
    - -
  • -
-
-
- - {{ remaining }} {{ remaining | pluralize }} left - - - -
+ + + + Iris + Vue.js • TodoMVC + + + + + + + + + + + + + + +
+
+

todos

+ +
+
+ +
    +
  • +
    + + + +
    + +
  • +
-
-

Double-click to edit a todo

+
+ + {{ remaining }} {{ remaining | pluralize }} left + + +
+
+
+

Double-click to edit a todo

+
+ + + - - \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js index 248c5333bc..b06ba921fa 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js @@ -1,19 +1,42 @@ -// Full spec-compliant TodoMVC with localStorage persistence +// Full spec-compliant TodoMVC with Iris // and hash-based routing in ~120 effective lines of JavaScript. -// localStorage persistence -var STORAGE_KEY = 'todos-vuejs-2.0' +// var socket = new Ws("ws://localhost:8080/todos/sync"); + +// socket.On("saved", function () { +// console.log("receive: on saved"); +// todoStorage.fetch(); +// }); + +var todos = []; + var todoStorage = { fetch: function () { - var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') - todos.forEach(function (todo, index) { - todo.id = index - }) - todoStorage.uid = todos.length + axios.get("/todos").then(response => { + if (response.data == null) { + return; + } + for (var i = 0; i < response.data.length; i++) { + // if (todos.length <=i || todos[i] === null) { + // todos.push(response.data[i]); + // } else { + // todos[i] = response.data[i]; + // } + todos.push(response.data[i]); + } + }); + return todos }, save: function (todos) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)) + axios.post("/todos", JSON.stringify(todos)).then(response => { + if (!response.data.success) { + window.alert("saving had a failure"); + return; + } + // console.log("send: save"); + // socket.Emit("save") + }); } } @@ -44,11 +67,18 @@ var app = new Vue({ visibility: 'all' }, - // watch todos change for localStorage persistence + // watch todos change for persistence watch: { todos: { handler: function (todos) { - todoStorage.save(todos) + // // saved by this client. + // if (todos[todos.length - 1].id === 0) { + // todoStorage.save(todos); + // } else { + // console.log("item cannot be saved, already exists."); + // console.log(todos[todos.length - 1]); + // } + todoStorage.save(todos); }, deep: true } @@ -90,7 +120,7 @@ var app = new Vue({ return } this.todos.push({ - id: todoStorage.uid++, + id: 0, // just for the client-side. title: value, completed: false }) @@ -140,7 +170,7 @@ var app = new Vue({ }) // handle routing -function onHashChange () { +function onHashChange() { var visibility = window.location.hash.replace(/#\/?/, '') if (filters[visibility]) { app.visibility = visibility diff --git a/mvc/di/func.go b/mvc/di/func.go index 36424b0666..e49a55c004 100644 --- a/mvc/di/func.go +++ b/mvc/di/func.go @@ -11,6 +11,8 @@ type ( InputIndex int } + // FuncInjector keeps the data that are needed in order to do the binding injection + // as fast as possible and with the best possible and safest way. FuncInjector struct { // the original function, is being used // only the .Call, which is referring to the same function, always. @@ -27,6 +29,10 @@ type ( } ) +// MakeFuncInjector returns a new func injector, which will be the object +// that the caller should use to bind input arguments of the "fn" function. +// +// The hijack and the goodFunc are optional, the "values" is the dependencies values. func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *FuncInjector { typ := IndirectType(fn.Type()) s := &FuncInjector{ @@ -100,10 +106,14 @@ func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, v return s } +// String returns a debug trace text. func (s *FuncInjector) String() string { return s.trace } +// Inject accepts an already created slice of input arguments +// and fills them, the "ctx" is optional and it's used +// on the dependencies that depends on one or more input arguments, these are the "ctx". func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) { args := *in for _, input := range s.inputs { @@ -118,6 +128,12 @@ func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) { *in = args } +// Call calls the "Inject" with a new slice of input arguments +// that are computed by the length of the input argument from the MakeFuncInjector's "fn" function. +// +// If the function needs a receiver, so +// the caller should be able to in[0] = receiver before injection, +// then the `Inject` method should be used instead. func (s *FuncInjector) Call(ctx ...reflect.Value) []reflect.Value { in := make([]reflect.Value, s.Length, s.Length) s.Inject(&in, ctx...) diff --git a/mvc/di/object.go b/mvc/di/object.go index f33e7036c6..392abcc587 100644 --- a/mvc/di/object.go +++ b/mvc/di/object.go @@ -5,11 +5,17 @@ import ( "reflect" ) +// BindType is the type of a binded object/value, it's being used to +// check if the value is accessible after a function call with a "ctx" when needed ( Dynamic type) +// or it's just a struct value (a service | Static type). type BindType uint32 const ( - Static BindType = iota // simple assignable value, a static value. - Dynamic // dynamic value, depends on some input arguments from the caller. + // Static is the simple assignable value, a static value. + Static BindType = iota + // Dynamic returns a value but it depends on some input arguments from the caller, + // on serve time. + Dynamic ) func bindTypeString(typ BindType) string { @@ -21,6 +27,9 @@ func bindTypeString(typ BindType) string { } } +// BindObject contains the dependency value's read-only information. +// FuncInjector and StructInjector keeps information about their +// input arguments/or fields, these properties contain a `BindObject` inside them. type BindObject struct { Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' . Value reflect.Value @@ -29,6 +38,11 @@ type BindObject struct { ReturnValue func([]reflect.Value) reflect.Value } +// MakeBindObject accepts any "v" value, struct, pointer or a function +// and a type checker that is used to check if the fields (if "v.elem()" is struct) +// or the input arguments (if "v.elem()" is func) +// are valid to be included as the final object's dependencies, even if the caller added more +// the "di" is smart enough to select what each "v" needs and what not before serve time. func MakeBindObject(v reflect.Value, goodFunc TypeChecker) (b BindObject, err error) { if IsFunc(v) { b.BindType = Dynamic @@ -93,10 +107,13 @@ func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Val return bf, outTyp, nil } +// IsAssignable checks if "to" type can be used as "b.Value/ReturnValue". func (b *BindObject) IsAssignable(to reflect.Type) bool { return equalTypes(b.Type, to) } +// Assign sets the values to a setter, "toSetter" contains the setter, so the caller +// can use it for multiple and different structs/functions as well. func (b *BindObject) Assign(ctx []reflect.Value, toSetter func(reflect.Value)) { if b.BindType == Dynamic { toSetter(b.ReturnValue(ctx)) diff --git a/mvc/di/struct.go b/mvc/di/struct.go index 091cd77f24..df20d7c878 100644 --- a/mvc/di/struct.go +++ b/mvc/di/struct.go @@ -18,6 +18,8 @@ type ( FieldIndex []int } + // StructInjector keeps the data that are needed in order to do the binding injection + // as fast as possible and with the best possible and safest way. StructInjector struct { initRef reflect.Value initRefAsSlice []reflect.Value // useful when the struct is passed on a func as input args via reflection. @@ -42,6 +44,12 @@ func (s *StructInjector) countBindType(typ BindType) (n int) { return } +// MakeStructInjector returns a new struct injector, which will be the object +// that the caller should use to bind exported fields or +// embedded unexported fields that contain exported fields +// of the "v" struct value or pointer. +// +// The hijack and the goodFunc are optional, the "values" is the dependencies values. func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *StructInjector { s := &StructInjector{ initRef: v, @@ -149,6 +157,7 @@ func (s *StructInjector) fillStruct() { } } +// String returns a debug trace message. func (s *StructInjector) String() (trace string) { for i, f := range s.fields { elemField := s.elemType.FieldByIndex(f.FieldIndex) diff --git a/mvc/di/values.go b/mvc/di/values.go index d7aacb184e..1033b95752 100644 --- a/mvc/di/values.go +++ b/mvc/di/values.go @@ -2,8 +2,11 @@ package di import "reflect" +// Values is a shortcut of []reflect.Value, +// it makes easier to remove and add dependencies. type Values []reflect.Value +// NewValues returns new empty (dependencies) values. func NewValues() Values { return Values{} } @@ -30,6 +33,7 @@ func (bv Values) CloneWithFieldsOf(s interface{}) Values { return values } +// Len returns the length of the current "bv" values slice. func (bv Values) Len() int { return len(bv) } @@ -41,6 +45,8 @@ func (bv *Values) Add(values ...interface{}) { bv.AddValues(ValuesOf(values)...) } +// AddValues same as `Add` but accepts reflect.Value dependencies instead of interface{} +// and appends them to the list if they pass some checks. func (bv *Values) AddValues(values ...reflect.Value) { for _, v := range values { if !goodVal(v) { From 7c7b78685fdd55910833d88d20c08177a048294f Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 23 Dec 2017 18:11:00 +0200 Subject: [PATCH 43/79] _examples/tutorial/vuejs-todo-mvc finished, live updates on all clients with the same session id (u can do it with login id for example) and memory storage on the server side, works perfectly and amazing fast, nice vue and iris --- .../vuejs-todo-mvc/src/web/public/index.html | 3 +- .../vuejs-todo-mvc/src/web/public/js/app.js | 99 +++++++++++-------- 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html index 3763042c24..38cca166ae 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html @@ -32,7 +32,8 @@

todos