diff --git a/HISTORY.md b/HISTORY.md index 0a24f31f..a4206c94 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,30 @@ **How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`. + +## v4 -> 5.0.1 + +- **IMPROVE**: [Iris command line tool](https://github.com/kataras/iris/tree/master/iris) introduces a **new** `get` command (replacement for the old `create`) + + +**The get command** downloads, installs and runs a project based on a `prototype`, such as `basic`, `static` and `mongo` . + +> These projects are located [online](https://github.com/iris-contrib/examples/tree/master/AIO_examples) + + +```sh +iris get basic +``` + +Downloads the [basic](https://github.com/iris-contrib/examples/tree/master/AIO_examples/basic) sample protoype project to the `$GOPATH/src/github.com/iris-contrib/examples` directory(the iris cmd will open this folder to you, automatically) builds, runs and watch for source code changes (hot-reload) + +[![Iris get command preview](https://raw.githubusercontent.com/iris-contrib/website/gh-pages/assets/iriscmd.gif)](https://raw.githubusercontent.com/iris-contrib/website/gh-pages/assets/iriscmd.gif) + + +- **CHANGE**: The `Path parameters` are now **immutable**. Now you don't have to copy a `path parameter` before passing to another function which maybe modifies it, this has a side-affect of `context.GetString("key") = context.Param("key")` so you have to be careful to not override a path parameter via other custom (per-context) user value. + + + ## v3 -> v4 long term support - **NEW**: `iris.StaticEmbedded`/`app := iris.New(); app.StaticEmbedded` - Embed static assets into your executable with [go-bindata](https://github.com/jteeuwen/go-bindata) and serve them. diff --git a/README.md b/README.md index 070c9b7b..75905613 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@
-Releases +Releases Examples @@ -870,14 +870,9 @@ I recommend writing your API tests using this new library, [httpexpect](https:// Versioning ------------ -Current: **V4 LTS** +Current: **5.0.1** -A new LTS version is released every 6 months - -> LTS stands for Long Term Support - - -Todo for the next release +Todo ------------ - [ ] Server-side React render, as requested [here](https://github.com/kataras/iris/issues/503) @@ -913,7 +908,7 @@ under the Apache Version 2 license found in the [LICENSE file](LICENSE). [Travis]: http://travis-ci.org/kataras/iris [License Widget]: https://img.shields.io/badge/license-Apache%20Version%202-E91E63.svg?style=flat-square [License]: https://github.com/kataras/iris/blob/master/LICENSE -[Release Widget]: https://img.shields.io/badge/release-V4%20LTS%20-blue.svg?style=flat-square +[Release Widget]: https://img.shields.io/badge/release-V5.0.1%20-blue.svg?style=flat-square [Release]: https://github.com/kataras/iris/releases [Chat Widget]: https://img.shields.io/badge/community-chat%20-00BCD4.svg?style=flat-square [Chat]: https://kataras.rocket.chat/channel/iris diff --git a/context.go b/context.go index 6af586c1..3f660fa8 100644 --- a/context.go +++ b/context.go @@ -2,6 +2,7 @@ package iris import ( "bufio" + "bytes" "encoding/base64" "encoding/json" "encoding/xml" @@ -93,7 +94,6 @@ type ( // it is not good practice to use this object in goroutines, for these cases use the .Clone() Context struct { *fasthttp.RequestCtx - Params PathParameters framework *Framework //keep track all registed middleware (handlers) Middleware Middleware // exported because is useful for debugging @@ -145,36 +145,6 @@ func (ctx *Context) GetHandlerName() string { /* Request */ -// Param returns the string representation of the key's path named parameter's value -// -// Return value should be never stored directly, instead store it to a local variable, -// for example -// instead of: context.Session().Set("name", ctx.Param("user")) -// do this: username:= ctx.Param("user");ctx.Session().Set("name", username) -func (ctx *Context) Param(key string) string { - return ctx.Params.Get(key) -} - -// ParamInt returns the int representation of the key's path named parameter's value -// -// Return value should be never stored directly, instead store it to a local variable, -// for example -// instead of: context.Session().Set("age", ctx.Param("age")) -// do this: age:= ctx.Param("age");ctx.Session().Set("age", age) -func (ctx *Context) ParamInt(key string) (int, error) { - return strconv.Atoi(ctx.Param(key)) -} - -// ParamInt64 returns the int64 representation of the key's path named parameter's value -// -// Return value should be never stored directly, instead store it to a local variable, -// for example -// instead of: context.Session().Set("ms", ctx.ParamInt64("ms")) -// do this: ms:= ctx.ParamInt64("ms");ctx.Session().Set("ms", ms) -func (ctx *Context) ParamInt64(key string) (int64, error) { - return strconv.ParseInt(ctx.Param(key), 10, 64) -} - // URLParam returns the get parameter from a request , if any func (ctx *Context) URLParam(key string) string { return string(ctx.RequestCtx.Request.URI().QueryArgs().Peek(key)) @@ -826,6 +796,14 @@ func (ctx *Context) StreamReader(bodyStream io.Reader, bodySize int) { /* Storage */ +// ValuesLen returns the total length of the user values storage, some of them maybe path parameters +func (ctx *Context) ValuesLen() (n int) { + ctx.VisitUserValues(func([]byte, interface{}) { + n++ + }) + return +} + // Get returns the user's value from a key // if doesn't exists returns nil func (ctx *Context) Get(key string) interface{} { @@ -852,14 +830,19 @@ func (ctx *Context) GetString(key string) string { return "" } -// GetInt same as Get but returns the value as int -// if nothing founds returns -1 -func (ctx *Context) GetInt(key string) int { - if v, ok := ctx.Get(key).(int); ok { - return v +var errIntParse = errors.New("Unable to find or parse the integer, found: %#v") + +// GetInt same as Get but tries to convert the return value as integer +// if nothing found or canno be parsed to integer it returns an error +func (ctx *Context) GetInt(key string) (int, error) { + v := ctx.Get(key) + if vint, ok := v.(int); ok { + return vint, nil + } else if vstring, sok := v.(string); sok { + return strconv.Atoi(vstring) } - return -1 + return -1, errIntParse.Format(v) } // Set sets a value to a key in the values map @@ -867,6 +850,60 @@ func (ctx *Context) Set(key string, value interface{}) { ctx.RequestCtx.SetUserValue(key, value) } +// ParamsLen tries to return all the stored values which values are string, probably most of them will be the path parameters +func (ctx *Context) ParamsLen() (n int) { + ctx.VisitUserValues(func(kb []byte, vg interface{}) { + if _, ok := vg.(string); ok { + n++ + } + + }) + return +} + +// Param returns the string representation of the key's path named parameter's value +// same as GetString +func (ctx *Context) Param(key string) string { + return ctx.GetString(key) +} + +// ParamInt returns the int representation of the key's path named parameter's value +// same as GetInt +func (ctx *Context) ParamInt(key string) (int, error) { + return ctx.GetInt(key) +} + +// ParamInt64 returns the int64 representation of the key's path named parameter's value +func (ctx *Context) ParamInt64(key string) (int64, error) { + return strconv.ParseInt(ctx.Param(key), 10, 64) +} + +// ParamsSentence returns a string implementation of all parameters that this context keeps +// hasthe form of key1=value1,key2=value2... +func (ctx *Context) ParamsSentence() string { + var buff bytes.Buffer + ctx.VisitUserValues(func(kb []byte, vg interface{}) { + v, ok := vg.(string) + if !ok { + return + } + k := string(kb) + buff.WriteString(k) + buff.WriteString("=") + buff.WriteString(v) + // we don't know where that (yet) stops so... + buff.WriteString(",") + + }) + result := buff.String() + if len(result) < 2 { + return "" + } + + return result[0 : len(result)-1] + +} + // VisitAllCookies takes a visitor which loops on each (request's) cookie key and value // // Note: the method ctx.Request.Header.VisitAllCookie by fasthttp, has a strange bug which I cannot solve at the moment. diff --git a/context_test.go b/context_test.go index 143df767..5f5b053d 100644 --- a/context_test.go +++ b/context_test.go @@ -72,16 +72,24 @@ func TestContextDoNextStop(t *testing.T) { } } +type pathParameter struct { + Key string + Value string +} +type pathParameters []pathParameter + // White-box testing * func TestContextParams(t *testing.T) { - var context iris.Context - params := iris.PathParameters{ - iris.PathParameter{Key: "testkey", Value: "testvalue"}, - iris.PathParameter{Key: "testkey2", Value: "testvalue2"}, - iris.PathParameter{Key: "id", Value: "3"}, - iris.PathParameter{Key: "bigint", Value: "548921854390354"}, + context := &iris.Context{RequestCtx: &fasthttp.RequestCtx{}} + params := pathParameters{ + pathParameter{Key: "testkey", Value: "testvalue"}, + pathParameter{Key: "testkey2", Value: "testvalue2"}, + pathParameter{Key: "id", Value: "3"}, + pathParameter{Key: "bigint", Value: "548921854390354"}, + } + for _, p := range params { + context.Set(p.Key, p.Value) } - context.Params = params if v := context.Param(params[0].Key); v != params[0].Value { t.Fatalf("Expecting parameter value to be %s but we got %s", params[0].Value, context.Param("testkey")) @@ -90,8 +98,8 @@ func TestContextParams(t *testing.T) { t.Fatalf("Expecting parameter value to be %s but we got %s", params[1].Value, context.Param("testkey2")) } - if len(context.Params) != len(params) { - t.Fatalf("Expecting to have %d parameters but we got %d", len(params), len(context.Params)) + if context.ParamsLen() != len(params) { + t.Fatalf("Expecting to have %d parameters but we got %d", len(params), context.ParamsLen()) } if vi, err := context.ParamInt(params[2].Key); err != nil { @@ -111,7 +119,7 @@ func TestContextParams(t *testing.T) { iris.ResetDefault() expectedParamsStr := "param1=myparam1,param2=myparam2,param3=myparam3afterstatic,anything=/andhere/anything/you/like" iris.Get("/path/:param1/:param2/staticpath/:param3/*anything", func(ctx *iris.Context) { - paramsStr := ctx.Params.String() + paramsStr := ctx.ParamsSentence() ctx.Write(paramsStr) }) @@ -349,8 +357,8 @@ func TestContextRedirectTo(t *testing.T) { iris.Get("/mypath", h)("my-path") iris.Get("/mypostpath", h)("my-post-path") iris.Get("mypath/with/params/:param1/:param2", func(ctx *iris.Context) { - if len(ctx.Params) != 2 { - t.Fatalf("Strange error, expecting parameters to be two but we got: %d", len(ctx.Params)) + if l := ctx.ParamsLen(); l != 2 { + t.Fatalf("Strange error, expecting parameters to be two but we got: %d", l) } ctx.Write(ctx.PathString()) })("my-path-with-params") @@ -462,7 +470,7 @@ func TestContextFlashMessages(t *testing.T) { firstKey := "name" lastKey := "package" - values := iris.PathParameters{iris.PathParameter{Key: firstKey, Value: "kataras"}, iris.PathParameter{Key: lastKey, Value: "iris"}} + values := pathParameters{pathParameter{Key: firstKey, Value: "kataras"}, pathParameter{Key: lastKey, Value: "iris"}} jsonExpected := map[string]string{firstKey: "kataras", lastKey: "iris"} // set the flashes, the cookies are filled iris.Put("/set", func(ctx *iris.Context) { diff --git a/http.go b/http.go index af06b5ce..c854c378 100644 --- a/http.go +++ b/http.go @@ -317,16 +317,6 @@ const ( ) type ( - // PathParameter is a struct which contains Key and Value, used for named path parameters - PathParameter struct { - Key string - Value string - } - - // PathParameters type for a slice of PathParameter - // Tt's a slice of PathParameter type, because it's faster than map - PathParameters []PathParameter - // entryCase is the type which the type of muxEntryusing in order to determinate what type (parameterized, anything, static...) is the perticular node entryCase uint8 @@ -356,58 +346,6 @@ var ( errMuxEntryWildcardMissingSlash = errors.New("Router: No slash(/) were found before wildcard in the route path: '%s' !") ) -// Get returns a value from a key inside this Parameters -// If no parameter with this key given then it returns an empty string -func (params PathParameters) Get(key string) string { - for _, p := range params { - if p.Key == key { - return p.Value - } - } - return "" -} - -// String returns a string implementation of all parameters that this PathParameters object keeps -// hasthe form of key1=value1,key2=value2... -func (params PathParameters) String() string { - var buff bytes.Buffer - for i := range params { - buff.WriteString(params[i].Key) - buff.WriteString("=") - buff.WriteString(params[i].Value) - if i < len(params)-1 { - buff.WriteString(",") - } - - } - return buff.String() -} - -// ParseParams receives a string and returns PathParameters (slice of PathParameter) -// received string must have this form: key1=value1,key2=value2... -func ParseParams(str string) PathParameters { - _paramsstr := strings.Split(str, ",") - if len(_paramsstr) == 0 { - return nil - } - - params := make(PathParameters, 0) // PathParameters{} - - // for i := 0; i < len(_paramsstr); i++ { - for i := range _paramsstr { - idxOfEq := strings.IndexRune(_paramsstr[i], '=') - if idxOfEq == -1 { - //error - return nil - } - - key := _paramsstr[i][:idxOfEq] - val := _paramsstr[i][idxOfEq+1:] - params = append(params, PathParameter{key, val}) - } - return params -} - // getParamsLen returns the parameters length from a given path func getParamsLen(path string) uint8 { var n uint @@ -644,8 +582,7 @@ func (e *muxEntry) addNode(numParams uint8, path string, fullPath string, middle } // get is used by the Router, it finds and returns the correct muxEntry for a path -func (e *muxEntry) get(path string, _params PathParameters) (middleware Middleware, params PathParameters, mustRedirect bool) { - params = _params +func (e *muxEntry) get(path string, ctx *Context) (mustRedirect bool) { loop: for { if len(path) > len(e.part) { @@ -674,13 +611,7 @@ loop: end++ } - if cap(params) < int(e.paramsLen) { - params = make(PathParameters, 0, e.paramsLen) - } - i := len(params) - params = params[:i+1] - params[i].Key = e.part[1:] - params[i].Value = path[:end] + ctx.Set(e.part[1:], path[:end]) if end < len(path) { if len(e.nodes) > 0 { @@ -692,8 +623,7 @@ loop: mustRedirect = (len(path) == end+1) return } - - if middleware = e.middleware; middleware != nil { + if ctx.Middleware = e.middleware; ctx.Middleware != nil { return } else if len(e.nodes) == 1 { e = e.nodes[0] @@ -703,15 +633,9 @@ loop: return case matchEverything: - if cap(params) < int(e.paramsLen) { - params = make(PathParameters, 0, e.paramsLen) - } - i := len(params) - params = params[:i+1] - params[i].Key = e.part[2:] - params[i].Value = path - middleware = e.middleware + ctx.Set(e.part[2:], path) + ctx.Middleware = e.middleware return default: @@ -719,7 +643,7 @@ loop: } } } else if path == e.part { - if middleware = e.middleware; middleware != nil { + if ctx.Middleware = e.middleware; ctx.Middleware != nil { return } @@ -1159,11 +1083,9 @@ func (mux *serveMux) BuildHandler() HandlerFunc { } } - middleware, params, mustRedirect := tree.entry.get(routePath, context.Params) // pass the parameters here for 0 allocation - if middleware != nil { + mustRedirect := tree.entry.get(routePath, context) // pass the parameters here for 0 allocation + if context.Middleware != nil { // ok we found the correct route, serve it and exit entirely from here - context.Params = params - context.Middleware = middleware //ctx.Request.Header.SetUserAgentBytes(DefaultUserAgent) context.Do() return diff --git a/http_test.go b/http_test.go index 5a6dd83b..751f2612 100644 --- a/http_test.go +++ b/http_test.go @@ -348,7 +348,7 @@ func TestMuxSimple(t *testing.T) { iris.HandleFunc(r.Method, r.Path, func(ctx *iris.Context) { ctx.SetStatusCode(r.Status) if r.Params != nil && len(r.Params) > 0 { - ctx.SetBodyString(ctx.Params.String()) + ctx.SetBodyString(ctx.ParamsSentence()) } else if r.URLParams != nil && len(r.URLParams) > 0 { if len(r.URLParams) != len(ctx.URLParams()) { t.Fatalf("Error when comparing length of url parameters %d != %d", len(r.URLParams), len(ctx.URLParams())) diff --git a/iris.go b/iris.go index 8de536c9..53082a66 100644 --- a/iris.go +++ b/iris.go @@ -64,6 +64,7 @@ import ( "sync" "time" + "bytes" "github.com/kataras/go-errors" "github.com/kataras/go-fs" "github.com/kataras/go-serializer" @@ -76,9 +77,9 @@ import ( const ( // IsLongTermSupport flag is true when the below version number is a long-term-support version - IsLongTermSupport = true + IsLongTermSupport = false // Version is the current version number of the Iris web framework - Version = "4" + Version = "5.0.1" banner = ` _____ _ |_ _| (_) @@ -681,7 +682,6 @@ func ReleaseCtx(ctx *Context) { // ReleaseCtx puts the Iris' Context back to the pool in order to be re-used // see .AcquireCtx & .Serve func (s *Framework) ReleaseCtx(ctx *Context) { - ctx.Params = ctx.Params[0:0] ctx.Middleware = nil ctx.session = nil s.contextPool.Put(ctx) @@ -1425,7 +1425,7 @@ func (api *muxAPI) API(path string, restAPI HandlerAPI, middleware ...HandlerFun // or no, I changed my mind, let all be named parameters and let users to decide what info they need, // using the Context to take more values (post form,url params and so on).- - paramPrefix := "param" + paramPrefix := []byte("param") for _, methodName := range AllMethods { methodWithBy := strings.Title(strings.ToLower(methodName)) + "By" if method, found := typ.MethodByName(methodWithBy); found { @@ -1439,9 +1439,9 @@ func (api *muxAPI) API(path string, restAPI HandlerAPI, middleware ...HandlerFun for i := 1; i < numInLen; i++ { // from 1 because the first is the 'object' if registedPath[len(registedPath)-1] == slashByte { - registedPath += ":" + paramPrefix + strconv.Itoa(i) + registedPath += ":" + string(paramPrefix) + strconv.Itoa(i) } else { - registedPath += "/:" + paramPrefix + strconv.Itoa(i) + registedPath += "/:" + string(paramPrefix) + strconv.Itoa(i) } } @@ -1454,15 +1454,15 @@ func (api *muxAPI) API(path string, restAPI HandlerAPI, middleware ...HandlerFun newController.FieldByName("Context").Set(reflect.ValueOf(ctx)) args := make([]reflect.Value, paramsLen+1, paramsLen+1) args[0] = newController - realParamsLen := len(ctx.Params) j := 1 - for i := 0; i < realParamsLen; i++ { // here we don't looping with the len we are already known by the 'API' because maybe there is a party/or/path witch accepting parameters before, see https://github.com/kataras/iris/issues/293 - if strings.HasPrefix(ctx.Params[i].Key, paramPrefix) { - args[j] = reflect.ValueOf(ctx.Params[i].Value) + + ctx.VisitUserValues(func(k []byte, v interface{}) { + if bytes.HasPrefix(k, paramPrefix) { + args[j] = reflect.ValueOf(v.(string)) j++ // the first parameter is the context, other are the path parameters, j++ to be align with (API's registered)paramsLen } - } + }) methodFunc.Call(args) }) diff --git a/iris/get.go b/iris/get.go index 2921d654..00ddfc34 100644 --- a/iris/get.go +++ b/iris/get.go @@ -91,7 +91,7 @@ func buildGetCommand() *cli.Cmd { availabletypes = append(availabletypes, "'"+k+"'") } // comma separated of projects' map key - return cli.Command("get", "gets & runs a simple protoype-based project"). + return cli.Command("get", "gets & runs a simple prototype-based project"). Flag("type", "basic", // we take the os.Args in order to have access both via subcommand and when flag passed