diff --git a/HISTORY.md b/HISTORY.md index 7cdbe1ba..7f6968b5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -18,6 +18,48 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`. +# Fr, 15 September 2017 | v8.4.2 + +## MVC + +Support more than one dynamic method function receivers. + +```go +package main + +import "github.com/kataras/iris" + +func main() { + app := iris.New() + app.Controller("/user", new(UserController)) + app.Run(iris.Addr("localhost:8080")) +} + +type UserController struct { iris.Controller } + +// Maps to GET /user +// Request example: http://localhost:8080/user +// as usual. +func (c *UserController) Get() { + c.Text = "hello from /user" +} + +// Maps to GET /user/{paramfirst:long} +// Request example: http://localhost:8080/user/42 +// as usual. +func (c *UserController) GetBy(userID int64) { + c.Ctx.Writef("hello user with id: %d", userID) +} + +// NEW: +// Maps to GET /user/{paramfirst:long}/business/{paramsecond:long} +// Request example: http://localhost:8080/user/42/business/93 +func (c *UserController) GetByBusinessBy(userID int64, businessID int64) { + c.Ctx.Writef("fetch a business id: %d that user with id: %d owns, may make your db query faster", + businessID, userID) +} +``` + # Th, 07 September 2017 | v8.4.1 ## Routing diff --git a/README.md b/README.md index 0186879e..e3ffd96e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Iris may have reached version 8, but we're not stopping there. We have many feat ### 📑 Table of contents * [Installation](#-installation) -* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#th-07-september-2017--v841) +* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-15-september-2017--v842) * [Learn](#-learn) * [HTTP Listening](_examples/#http-listening) * [Configuration](_examples/#configuration) diff --git a/VERSION b/VERSION index bf677efe..0d2d877f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.4.1:https://github.com/kataras/iris/blob/master/HISTORY.md#th-07-september-2017--v841 \ No newline at end of file +8.4.2:https://github.com/kataras/iris/blob/master/HISTORY.md#fr-15-september-2017--v842 \ No newline at end of file diff --git a/_examples/routing/overview/main.go b/_examples/routing/overview/main.go index 80e7133d..dbf52938 100644 --- a/_examples/routing/overview/main.go +++ b/_examples/routing/overview/main.go @@ -98,6 +98,11 @@ func main() { // GET: http://any_thing_here.localhost:8080 dynamicSubdomainRoutes.Get("/", info) + app.Delete("/something", func(ctx iris.Context) { + name := ctx.URLParam("name") + ctx.Writef(name) + }) + // GET: http://localhost:8080/ // GET: http://localhost:8080/profile/anyusername // GET: http://localhost:8080/profile/anyusername/backups/any/number/of/paths/here @@ -108,13 +113,12 @@ func main() { // POST: http://localhost:8080/users // PUT: http://localhost:8080/users // DELETE: http://localhost:8080/users/42 + // DELETE: http://localhost:8080/something?name=iris // GET: http://admin.localhost:8080 // GET: http://admin.localhost:8080/settings // GET: http://any_thing_here.localhost:8080 - if err := app.Run(iris.Addr(":8080")); err != nil { - panic(err) - } + app.Run(iris.Addr(":8080")) } func info(ctx iris.Context) { diff --git a/_examples/routing/overview/public/images/favicon.ico b/_examples/routing/overview/public/images/favicon.ico new file mode 100644 index 00000000..961ef6da Binary files /dev/null and b/_examples/routing/overview/public/images/favicon.ico differ diff --git a/doc.go b/doc.go index 24360949..c363585c 100644 --- a/doc.go +++ b/doc.go @@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub: Current Version -8.4.0 +8.4.2 Installation @@ -820,6 +820,8 @@ and it adds its logic to its `BeginRequest`. Source file: https://github.com/kat Read access to the current route via the `Route` field. +Support for more than one input arguments (map to dynamic request path parameters). + Register one or more relative paths and able to get path parameters, i.e If `app.Controller("/user", new(user.Controller))` @@ -843,6 +845,7 @@ Register one or more relative paths and able to get path parameters, i.e If `app.Controller("/equality", new(profile.Equality))` - `func(*Controller) GetBy(is bool)` - `GET:/equality/{param:boolean}` + - `func(*Controller) GetByOtherBy(is bool, otherID int64)` - `GET:/equality/{paramfirst:boolean}/other/{paramsecond:long}` Supported types for method functions receivers: int, int64, bool and string. diff --git a/iris.go b/iris.go index 81691018..62310740 100644 --- a/iris.go +++ b/iris.go @@ -32,7 +32,7 @@ import ( const ( // Version is the current version number of the Iris Web Framework. - Version = "8.4.1" + Version = "8.4.2" ) // HTTP status codes as registered with IANA. diff --git a/mvc/activator/activator.go b/mvc/activator/activator.go index af363f4a..740a1c8f 100644 --- a/mvc/activator/activator.go +++ b/mvc/activator/activator.go @@ -141,7 +141,7 @@ func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler // 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).Interface()) + handleRequest(ctx, c.Method(methodFunc.Index)) // if had models, set them after the end-developer's handler. if hasModels { t.modelController.Handle(ctx, c) diff --git a/mvc/activator/methodfunc/func_caller.go b/mvc/activator/methodfunc/func_caller.go index 96ce2cfd..039d2d5c 100644 --- a/mvc/activator/methodfunc/func_caller.go +++ b/mvc/activator/methodfunc/func_caller.go @@ -1,68 +1,62 @@ package methodfunc import ( + "reflect" + "github.com/kataras/iris/context" ) -// FuncCaller is responsible to call the controller's function -// which is responsible -// for that request for this http method. -type FuncCaller interface { - // 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(ctx context.Context, f interface{}) -} +// 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 accepts one or more parameters. + if a.dynamic { + // if one function input argument then call the function + // by "casting" (faster). + if l := len(a.paramKeys); l == 1 { + paramType := a.paramTypes[0] + paramKey := a.paramKeys[0] -type callerFunc func(ctx context.Context, f interface{}) + if paramType == paramTypeInt { + return func(ctx context.Context, f reflect.Value) { + v, _ := ctx.Params().GetInt(paramKey) + f.Interface().(func(int))(v) + } + } -func (c callerFunc) MethodCall(ctx context.Context, f interface{}) { - c(ctx, f) -} + if paramType == paramTypeLong { + return func(ctx context.Context, f reflect.Value) { + v, _ := ctx.Params().GetInt64(paramKey) + f.Interface().(func(int64))(v) + } -func resolveCaller(p pathInfo) callerFunc { - // if it's standard `Get`, `Post` without parameters. - if p.ParamType == "" { - return func(ctx context.Context, f interface{}) { - f.(func())() + } + + if paramType == paramTypeBoolean { + return func(ctx context.Context, f reflect.Value) { + v, _ := ctx.Params().GetBool(paramKey) + f.Interface().(func(bool))(v) + } + } + + // string, path... + return func(ctx context.Context, f reflect.Value) { + f.Interface().(func(string))(ctx.Params().Get(paramKey)) + } + + } + + // if func input arguments are more than one then + // use the Call method (slower). + return func(ctx context.Context, f reflect.Value) { + f.Call(a.paramValues(ctx)) } } - // remember, - // the router already checks for the correct type, - // we did pre-calculate everything - // and now we will pre-calculate the method caller itself as well. - - if p.ParamType == paramTypeInt { - return func(ctx context.Context, f interface{}) { - paramValue, _ := ctx.Params().GetInt(paramName) - f.(func(int))(paramValue) - } - } - - if p.ParamType == paramTypeLong { - return func(ctx context.Context, f interface{}) { - paramValue, _ := ctx.Params().GetInt64(paramName) - f.(func(int64))(paramValue) - } - } - - if p.ParamType == paramTypeBoolean { - return func(ctx context.Context, f interface{}) { - paramValue, _ := ctx.Params().GetBool(paramName) - f.(func(bool))(paramValue) - } - } - - // else it's string or path, both of them are simple strings. - return func(ctx context.Context, f interface{}) { - paramValue := ctx.Params().Get(paramName) - f.(func(string))(paramValue) + // if it's static without any receivers then just call it. + return func(ctx context.Context, f reflect.Value) { + f.Interface().(func())() } } diff --git a/mvc/activator/methodfunc/func_lexer.go b/mvc/activator/methodfunc/func_lexer.go new file mode 100644 index 00000000..e06352a7 --- /dev/null +++ b/mvc/activator/methodfunc/func_lexer.go @@ -0,0 +1,89 @@ +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 new file mode 100644 index 00000000..cedc716f --- /dev/null +++ b/mvc/activator/methodfunc/func_parser.go @@ -0,0 +1,162 @@ +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 { + typ := p.info.Type + funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver. + + if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path + a.relPath += "/" + strings.ToLower(w) + continue + } + + if typ.NumIn() <= funcArgPos { + return nil, errors.New("keyword 'By' found but length of input receivers are not match for " + + p.info.Name) + } + + var ( + paramKey = genParamKey(funcArgPos) // paramfirst, paramsecond... + paramType = paramTypeString // default string + ) + + // string, int... + goType := typ.In(funcArgPos).Name() + + if p.lexer.peekNext() == 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 nil, 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 + continue + } + + a.relPath += "/" + strings.ToLower(w) + } + return a, 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 { + 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_path.go b/mvc/activator/methodfunc/func_path.go deleted file mode 100644 index 26ba8022..00000000 --- a/mvc/activator/methodfunc/func_path.go +++ /dev/null @@ -1,121 +0,0 @@ -package methodfunc - -import ( - "bytes" - "fmt" - "strings" - "unicode" -) - -const ( - by = "By" - wildcard = "Wildcard" - paramName = "param" -) - -type pathInfo struct { - GoParamType string - ParamType string - RelPath string -} - -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. -} - -func resolveRelativePath(info FuncInfo) (p pathInfo, ok bool) { - if info.Trailing == "" { - // it's valid - // it's just don't have a relative path, - // therefore p.RelPath will be empty, as we want. - return p, true - } - - var ( - typ = info.Type - tr = info.Trailing - relPath = resolvePathFromFunc(tr) - - goType, paramType string - ) - - byKeywordIdx := strings.LastIndex(tr, by) - if byKeywordIdx != -1 && typ.NumIn() == 2 { // first is the struct receiver. - funcPath := tr[0:byKeywordIdx] // remove the "By" - goType = typ.In(1).Name() - afterBy := byKeywordIdx + len(by) - if len(tr) > afterBy { - if tr[afterBy:] == wildcard { - paramType = paramTypePath - } else { - // invalid syntax - return p, false - } - } else { - // it's not wildcard, so check base on our available macro types. - if paramType, ok = macroTypes[goType]; !ok { - // ivalid type - return p, false - } - } - - // int, int64, bool and string are supported. - // as there is no way to get the parameter name - // we will use the "param" everywhere. - suffix := fmt.Sprintf("/{%s:%s}", paramName, paramType) - relPath = resolvePathFromFunc(funcPath) + suffix - } - - // if GetSomething/PostSomething/PutSomething... - // we will not check for "Something" because we could - // occur unexpected behaviors to the existing users - // who using exported functions for controller's internal - // functionalities and not for serving a request path. - - return pathInfo{ - GoParamType: goType, - ParamType: paramType, - RelPath: relPath, - }, true -} - -func resolvePathFromFunc(funcName string) string { - end := len(funcName) - start := -1 - buf := &bytes.Buffer{} - - for i, n := 0, end; i < n; i++ { - c := rune(funcName[i]) - if unicode.IsUpper(c) { - // it doesn't count the last uppercase - if start != -1 { - end = i - s := "/" + strings.ToLower(funcName[start:end]) - buf.WriteString(s) - } - start = i - continue - } - end = i + 1 - } - - if end > 0 && len(funcName) >= end { - buf.WriteString("/" + strings.ToLower(funcName[start:end])) - } - - return buf.String() -} diff --git a/mvc/activator/methodfunc/methodfunc.go b/mvc/activator/methodfunc/methodfunc.go index 1012355f..2dc720be 100644 --- a/mvc/activator/methodfunc/methodfunc.go +++ b/mvc/activator/methodfunc/methodfunc.go @@ -2,13 +2,25 @@ package methodfunc import ( "reflect" + + "github.com/kataras/golog" + "github.com/kataras/iris/context" ) // MethodFunc the handler function. type MethodFunc struct { FuncInfo - FuncCaller - RelPath string + // 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 @@ -17,15 +29,16 @@ type MethodFunc struct { func Resolve(typ reflect.Type) (methodFuncs []MethodFunc) { infos := fetchInfos(typ) for _, info := range infos { - p, ok := resolveRelativePath(info) - if !ok { + parser := newFuncParser(info) + a, err := parser.parse() + if err != nil { + golog.Errorf("MVC: %s\n", err) continue } - caller := resolveCaller(p) methodFunc := MethodFunc{ - RelPath: p.RelPath, + RelPath: a.relPath, FuncInfo: info, - FuncCaller: caller, + MethodCall: buildMethodCall(a), } methodFuncs = append(methodFuncs, methodFunc) diff --git a/mvc/controller_test.go b/mvc/controller_test.go index 9b7ad13b..a7d3d17e 100644 --- a/mvc/controller_test.go +++ b/mvc/controller_test.go @@ -429,6 +429,7 @@ func TestControllerInsideControllerRecursively(t *testing.T) { ) app := iris.New() + app.Controller("/user/{username}", new(testCtrl0), &testBindType{title: title}) @@ -453,8 +454,9 @@ func (c *testControllerRelPathFromFunc) PostLogin() {} func (c *testControllerRelPathFromFunc) GetAdminLogin() {} -func (c *testControllerRelPathFromFunc) PutSomethingIntoThis() {} -func (c *testControllerRelPathFromFunc) GetSomethingBy(bool) {} +func (c *testControllerRelPathFromFunc) PutSomethingIntoThis() {} +func (c *testControllerRelPathFromFunc) GetSomethingBy(bool) {} +func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} // two input arguments func TestControllerRelPathFromFunc(t *testing.T) { app := iris.New() @@ -472,6 +474,8 @@ func TestControllerRelPathFromFunc(t *testing.T) { Body().Equal("GET:/something/false") e.GET("/something/truee").Expect().Status(httptest.StatusNotFound) e.GET("/something/falsee").Expect().Status(httptest.StatusNotFound) + e.GET("/something/true/else/this/42").Expect().Status(httptest.StatusOK). + Body().Equal("GET:/something/true/else/this/42") e.GET("/login").Expect().Status(httptest.StatusOK). Body().Equal("GET:/login")