diff --git a/mvc2/controller.go b/mvc2/controller.go index 6244674e..ff7fb964 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) { + 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 } - 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 00000000..63d7a671 --- /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 2e0cd38e..677222e5 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 4f6080e8..4405d784 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)