From 7043f352d9d4a737b1b5ffde314d0f6bb9026a2c Mon Sep 17 00:00:00 2001 From: kataras Date: Mon, 4 Dec 2017 05:06:03 +0200 Subject: [PATCH] made it work but looking for another approach Former-commit-id: e61c4573543c57b8d6d4ef2583e40f52c391402f --- 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 | 547 +++++++++++++++++++---- mvc2/controller_handler_test.go | 21 +- mvc2/handler.go | 120 +++-- 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 | 274 ++++++++---- mvc2/service_test.go | 46 -- 20 files changed, 855 insertions(+), 792 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 bb656678..9739bf0f 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 c315b41d..e395f31c 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 1593996e..ad13f735 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 1e39bc85..23e17236 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 ea0fecc6..84b17fb0 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 755e66a6..205687c1 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 3eaed501..2a618b6e 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 baba2a9d..00000000 --- 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 099a3578..00000000 --- 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 452da4b6..9d7aad3d 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. // -// // BaseController is the interface that all controllers should implement. -// type BaseController interface { -// BeginRequest(ctx context.Context) -// EndRequest(ctx context.Context) -// } +// 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 +} -// // type ControllerInitializer interface { -// // Init(r router.Party) -// // } +var _ BaseController = &C{} -// // type activator struct { -// // Router router.Party -// // container *Mvc -// // } +// BeginRequest starts the request by initializing the `Context` field. +func (c *C) BeginRequest(ctx context.Context) { c.Ctx = ctx } -// func registerController(m *Mvc, r router.Party, c BaseController) { +// 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 -// // 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. + initRef BaseController // the BaseController as it's passed from the end-dev. -// 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 -// } + // 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 -// 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 -// } + // key = the method's name. + methods map[string]reflect.Method -// info, ok := methodfunc.FetchFuncInfo(m) -// if !ok { -// golog.Errorf("mvc controller handler: could not resolve the func info from '%s'", funcName) -// return nil -// } + // services []field + // bindServices func(elem reflect.Value) + s services +} -// methodFunc, err := methodfunc.ResolveMethodFunc(info) -// if err != nil { -// golog.Errorf("mvc controller handler: %v", err) -// return nil -// } +func newControllerActivator(engine *Engine, router router.Party, controller BaseController) *ControllerActivator { + c := &ControllerActivator{ + Engine: engine, + Router: router, + initRef: controller, + } -// 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{} { + c.analyze() + return c +} -// // // job here. +var reservedMethodNames = []string{ + "BeginRequest", + "EndRequest", + "OnActivate", +} -// // return controller -// // }) +func isReservedMethod(name string) bool { + for _, s := range reservedMethodNames { + if s == name { + return true + } + } -// h := m.Handler(fn.Interface()) -// return func(ctx context.Context) { -// controller.BeginRequest(ctx) -// h(ctx) -// controller.EndRequest(ctx) -// } -// } + 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 d3e11ea7..3a48f8b4 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 bda3dc05..85be1d52 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) + // } - hasIn := len(m) > 0 - fn := reflect.ValueOf(handler) + // 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 + // } - // if has no input to bind then execute the "handler" using the mvc style - // for any output parameters. - if !hasIn { + // return makeHandler(reflect.ValueOf(handler), m), nil +} + +func makeHandler(fn reflect.Value, inputBinders []reflect.Value) context.Handler { + inLen := fn.Type().NumIn() + + 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 - } + // 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)) - in[k+1] = item - } - } else { - in[k] = result - } - */ + s.FillFuncInput(ctxValue, &in) - if ctx.IsStopped() { - return - } - } methodfunc.DispatchFuncResult(ctx, fn.Call(in)) - }, nil + } } diff --git a/mvc2/handler_test.go b/mvc2/handler_test.go index bc8def58..358a38c1 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 e02bc43d..00000000 --- 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 4bcbfe06..e33f11b0 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 ea0da3f7..00000000 --- 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 c10e47c5..00000000 --- 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 931aa4b8..69f533fb 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 f3155b8f..03ab07e5 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 -// } +type service struct { + Type reflect.Type + Value reflect.Value + StructFieldIndex []int -// // Valid checks if the service's Value's Value is valid for set or get. -// func (s Service) Valid() bool { -// return goodVal(s.Value) -// } + // for func input. + ReturnValue func(ctx []reflect.Value) reflect.Value + FuncInputIndex int + FuncInputContextIndex int +} -// // Equal returns if the -// func (s Service) Equal(other Service) bool { -// return equalTypes(s.typ, other.typ) -// } +type services []*service -// 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) +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 } - -// 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 deleted file mode 100644 index 88779dce..00000000 --- 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{}{})) - -}