diff --git a/HISTORY.md b/HISTORY.md index 0bddb041..1666ed21 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -29,6 +29,10 @@ Thanks to [Santosh Anand](https://github.com/santoshanand) the http://iris-go.co The amount of the next two or three donations you'll send they will be immediately transferred to his own account balance, so be generous please! +# Th, 08 June 2017 | v7.0.2 + +- Able to set **immutable** data on sessions and context's storage. Aligned to fix an issue on slices and maps as reported [here](https://github.com/iris-contrib/community-board/issues/5). + # We, 07 June 2017 | v7.0.1 - Proof of concept of an internal release generator, navigate [here](https://github.com/iris-contrib/community-board/issues/2) to read more. diff --git a/README.md b/README.md index db179b40..dbb53e46 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A fast, cross-platform and efficient web framework with robust set of well-desig [![Report card](https://img.shields.io/badge/report%20card%20-a%2B-F44336.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![Support forum](https://img.shields.io/badge/support-page-ec2eb4.svg?style=flat-square)](http://support.iris-go.com) [![Examples](https://img.shields.io/badge/howto-examples-3362c2.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples#table-of-contents) -[![Godocs](https://img.shields.io/badge/7.0.1-%20documentation-5272B4.svg?style=flat-square)](https://godoc.org/github.com/kataras/iris) +[![Godocs](https://img.shields.io/badge/7.0.2-%20documentation-5272B4.svg?style=flat-square)](https://godoc.org/github.com/kataras/iris) [![Chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![Buy me a cup of coffee](https://img.shields.io/badge/support-%20open--source-F4A460.svg?logo=data:image%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIGZpbGw9InJnYigyMjAsMjIwLDIyMCkiIGQ9Ik04ODYuNiwzMDUuM2MtNDUuNywyMDMuMS0xODcsMzEwLjMtNDA5LjYsMzEwLjNoLTc0LjFsLTUxLjUsMzI2LjloLTYybC0zLjIsMjEuMWMtMi4xLDE0LDguNiwyNi40LDIyLjYsMjYuNGgxNTguNWMxOC44LDAsMzQuNy0xMy42LDM3LjctMzIuMmwxLjUtOGwyOS45LTE4OS4zbDEuOS0xMC4zYzIuOS0xOC42LDE4LjktMzIuMiwzNy43LTMyLjJoMjMuNWMxNTMuNSwwLDI3My43LTYyLjQsMzA4LjktMjQyLjdDOTIxLjYsNDA2LjgsOTE2LjcsMzQ4LjYsODg2LjYsMzA1LjN6Ii8%2BPHBhdGggZmlsbD0icmdiKDIyMCwyMjAsMjIwKSIgZD0iTTc5MS45LDgzLjlDNzQ2LjUsMzIuMiw2NjQuNCwxMCw1NTkuNSwxMEgyNTVjLTIxLjQsMC0zOS44LDE1LjUtNDMuMSwzNi44TDg1LDg1MWMtMi41LDE1LjksOS44LDMwLjIsMjUuOCwzMC4ySDI5OWw0Ny4zLTI5OS42bC0xLjUsOS40YzMuMi0yMS4zLDIxLjQtMzYuOCw0Mi45LTM2LjhINDc3YzE3NS41LDAsMzEzLTcxLjIsMzUzLjItMjc3LjVjMS4yLTYuMSwyLjMtMTIuMSwzLjEtMTcuOEM4NDUuMSwxODIuOCw4MzMuMiwxMzAuOCw3OTEuOSw4My45TDc5MS45LDgzLjl6Ii8%2BPC9zdmc%2B)](https://github.com/kataras/iris#buy-me-a-cup-of-coffee) @@ -394,7 +394,7 @@ Besides the fact that we have a [community chat][Chat] for questions or reports Version ------------ -Current: v7.0.1 +Current: v7.0.2 Each new release is pushed to the master. It stays there until the next version. When a next version is released then the previous version goes to its own branch with `gopkg.in` as its import path (and its own vendor folder), in order to keep it working "for-ever". diff --git a/_examples/beginner/http-errors/main.go b/_examples/beginner/http-errors/main.go index 8647c34c..aa020c52 100644 --- a/_examples/beginner/http-errors/main.go +++ b/_examples/beginner/http-errors/main.go @@ -22,7 +22,7 @@ func main() { }) app.Get("/u/{firstname:alphabetical}", func(ctx context.Context) { - ctx.Writef("Hello %s", ctx.Values().GetString("firstname")) + ctx.Writef("Hello %s", ctx.Params().Get("firstname")) }) app.Run(iris.Addr(":8080")) diff --git a/_examples/intermediate/sessions/standalone/main.go b/_examples/intermediate/sessions/standalone/main.go index f04a2620..4ba505a2 100644 --- a/_examples/intermediate/sessions/standalone/main.go +++ b/_examples/intermediate/sessions/standalone/main.go @@ -8,6 +8,10 @@ import ( "github.com/kataras/iris/sessions" ) +type myobject struct { + name string +} + func main() { app := iris.New() // enable all (error) logs @@ -29,11 +33,6 @@ func main() { // want to be crazy safe? Take a look at the "securecookie" example folder. }) - // OPTIONALLY: - // import "github.com/kataras/iris/sessions/sessiondb/redis" - // or import "github.com/kataras/go-sessions/sessiondb/$any_available_community_database" - // mySessions.UseDatabase(redis.New(...)) - app.AttachSessionManager(mySessions) // Attach the session manager we just created. app.Get("/", func(ctx context.Context) { @@ -41,15 +40,24 @@ func main() { }) app.Get("/set", func(ctx context.Context) { - //set session values + //set session values. + ctx.Session().Set("name", "iris") //test if setted here ctx.Writef("All ok session setted to: %s", ctx.Session().GetString("name")) + + // Set will set the value as-it-is, + // if it's a slice or map + // you will be able to change it on .Get directly! + // Keep note that I don't recommend saving big data neither slices or maps on a session + // but if you really need it then use the `SetImmutable` instead of `Set`. + // Use `SetImmutable` consistently, it's slower. + // Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 }) app.Get("/get", func(ctx context.Context) { - // get a specific key, as string, if no found returns just an empty string + // get a specific value, as string, if no found returns just an empty string name := ctx.Session().GetString("name") ctx.Writef("The name on the /set was: %s", name) diff --git a/context/context.go b/context/context.go index da86607b..b30b48b0 100644 --- a/context/context.go +++ b/context/context.go @@ -31,6 +31,7 @@ import ( "github.com/russross/blackfriday" "github.com/kataras/iris/core/errors" + "github.com/kataras/iris/core/memstore" "github.com/kataras/iris/sessions" ) @@ -73,6 +74,64 @@ func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error { return u(data, v) } +// RequestParams is a key string - value string storage which context's request params should implement. +// RequestValues is for communication between middleware, RequestParams cannot be changed, are setted at the routing +// time, stores the dynamic named parameters, can be empty if the route is static. +type RequestParams struct { + store memstore.Store +} + +// Set shouldn't be used as a local storage, context's values store +// is the local storage, not params. +func (r *RequestParams) Set(key, value string) { + r.store.Set(key, value) +} + +// Visit accepts a visitor which will be filled +// by the key-value params. +func (r *RequestParams) Visit(visitor func(key string, value interface{})) { + r.store.Visit(visitor) +} + +// Get returns a path parameter's value based on its route's dynamic path key. +func (r RequestParams) Get(key string) string { + return r.store.GetString(key) +} + +// GetInt returns the param's value as int, based on its key. +func (r RequestParams) GetInt(key string) (int, error) { + return r.store.GetInt(key) +} + +// GetInt64 returns the user's value as int64, based on its key. +func (r RequestParams) GetInt64(key string) (int64, error) { + return r.store.GetInt64(key) +} + +// GetDecoded returns the url-query-decoded user's value based on its key. +func (r RequestParams) GetDecoded(key string) string { + return DecodeQuery(DecodeQuery(r.Get(key))) +} + +// GetIntUnslashed same as Get but it removes the first slash if found. +// Usage: Get an id from a wildcard path. +// +// Returns -1 with an error if the parameter couldn't be found. +func (r RequestParams) GetIntUnslashed(key string) (int, error) { + v := r.Get(key) + if v != "" { + if len(v) > 1 { + if v[0] == '/' { + v = v[1:] + } + } + return strconv.Atoi(v) + + } + + return -1, memstore.ErrIntParse.Format(v) +} + // Context is the midle-man server's "object" for the clients. // // A New context is being acquired from a sync.Pool on each connection. @@ -163,9 +222,7 @@ type Context interface { // Params returns the current url's named parameters key-value storage. // Named path parameters are being saved here. // This storage, as the whole Context, is per-request lifetime. - Params() RequestParams - // SetParams sets/or/overrides the request's url named parameters key-value storage. - SetParams(RequestParams) + Params() *RequestParams // Values returns the current "user" storage. // Named path parameters and any optional data can be saved here. @@ -173,7 +230,7 @@ type Context interface { // // You can use this function to Set and Get local values // that can be used to share information between handlers and middleware. - Values() *RequestValues + Values() *memstore.Store // Translate is the i18n (localization) middleware's function, // it calls the Get("translate") to return the translated value. // @@ -684,8 +741,8 @@ type context struct { // the original http.Request request *http.Request // the local key-value storage - params RequestParams // url named parameters - values RequestValues // generic storage, middleware communication + params RequestParams // url named parameters + values memstore.Store // generic storage, middleware communication // the underline application framework framework Application @@ -720,7 +777,7 @@ func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) { ctx.handlers = nil // will be filled by router.Serve/HTTP ctx.session = nil // >> >> by sessions.Session() ctx.values = ctx.values[0:0] // >> >> by context.Values().Set - ctx.params = ctx.params[0:0] + ctx.params.store = ctx.params.store[0:0] ctx.request = r ctx.currentHandlerIndex = 0 ctx.writer = AcquireResponseWriter() @@ -861,13 +918,8 @@ func (ctx *context) IsStopped() bool { // Params returns the current url's named parameters key-value storage. // Named path parameters are being saved here. // This storage, as the whole context, is per-request lifetime. -func (ctx *context) Params() RequestParams { - return ctx.params -} - -// SetParams sets/or/overrides the request's url named parameters key-value storage. -func (ctx *context) SetParams(params RequestParams) { - ctx.params = params +func (ctx *context) Params() *RequestParams { + return &ctx.params } // Values returns the current "user" storage. @@ -876,7 +928,7 @@ func (ctx *context) SetParams(params RequestParams) { // // You can use this function to Set and Get local values // that can be used to share information between handlers and middleware. -func (ctx *context) Values() *RequestValues { +func (ctx *context) Values() *memstore.Store { return &ctx.values } diff --git a/context/values.go b/context/values.go deleted file mode 100644 index fcfbe65f..00000000 --- a/context/values.go +++ /dev/null @@ -1,193 +0,0 @@ -package context - -import ( - "strconv" - - "github.com/kataras/iris/core/errors" -) - -type ( - // RequestValue is the entry of the context storage RequestValues - .Values() - RequestValue struct { - key string - value interface{} - } - - // RequestValues is just a key-value storage which context's request values should implement. - RequestValues []RequestValue -) - -// RequestValuesReadOnly the request values with read-only access. -type RequestValuesReadOnly struct { - RequestValues -} - -// Set does nothing. -func (r RequestValuesReadOnly) Set(string, interface{}) {} - -// Set sets a value to the key-value context storage, can be familiar as "User Values". -func (r *RequestValues) Set(key string, value interface{}) { - args := *r - n := len(args) - for i := 0; i < n; i++ { - kv := &args[i] - if kv.key == key { - kv.value = value - return - } - } - - c := cap(args) - if c > n { - args = args[:n+1] - kv := &args[n] - kv.key = key - kv.value = value - *r = args - return - } - - kv := RequestValue{} - kv.key = key - kv.value = value - *r = append(args, kv) -} - -// Get returns the user's value based on its key. -func (r *RequestValues) Get(key string) interface{} { - args := *r - n := len(args) - for i := 0; i < n; i++ { - kv := &args[i] - if kv.key == key { - return kv.value - } - } - return nil -} - -// Visit accepts a visitor which will be filled -// by the key-value objects, the caller should not try to change the underline values. -func (r *RequestValues) Visit(visitor func(key string, value interface{})) { - args := *r - for i, n := 0, len(args); i < n; i++ { - visitor(args[i].key, args[i].value) - } -} - -// GetString returns the user's value as string, based on its key. -func (r *RequestValues) GetString(key string) string { - if v, ok := r.Get(key).(string); ok { - return v - } - - return "" -} - -var errIntParse = errors.New("unable to find or parse the integer, found: %#v") - -// GetInt returns the user's value as int, based on its key. -func (r *RequestValues) GetInt(key string) (int, error) { - v := r.Get(key) - if vint, ok := v.(int); ok { - return vint, nil - } else if vstring, sok := v.(string); sok { - return strconv.Atoi(vstring) - } - - return -1, errIntParse.Format(v) -} - -// GetInt64 returns the user's value as int64, based on its key. -func (r *RequestValues) GetInt64(key string) (int64, error) { - return strconv.ParseInt(r.GetString(key), 10, 64) -} - -// Reset clears all the request values. -func (r *RequestValues) Reset() { - *r = (*r)[0:0] -} - -// ReadOnly returns a new request values with read-only access. -func (r *RequestValues) ReadOnly() RequestValuesReadOnly { - args := *r - values := make(RequestValues, len(args)) - copy(values, args) - return RequestValuesReadOnly{values} -} - -// Len returns the full length of the values. -func (r *RequestValues) Len() int { - args := *r - return len(args) -} - -// RequestParam is the entry of RequestParams, request's url named parameters are storaged here. -type RequestParam struct { - Key string - Value string -} - -// RequestParams is a key string - value string storage which context's request params should implement. -// RequestValues is for communication between middleware, RequestParams cannot be changed, are setted at the routing -// time, stores the dynamic named parameters, can be empty if the route is static. -type RequestParams []RequestParam - -// Get returns the param's value based on its key. -func (r RequestParams) Get(key string) string { - for _, p := range r { - if p.Key == key { - return p.Value - } - } - return "" -} - -// Visit accepts a visitor which will be filled -// by the key and value. -// The caller should not try to change the underline values. -func (r RequestParams) Visit(visitor func(key string, value string)) { - for i, n := 0, len(r); i < n; i++ { - visitor(r[i].Key, r[i].Value) - } -} - -// GetInt returns the user's value as int, based on its key. -func (r RequestParams) GetInt(key string) (int, error) { - v := r.Get(key) - return strconv.Atoi(v) -} - -// GetInt64 returns the user's value as int64, based on its key. -func (r RequestParams) GetInt64(key string) (int64, error) { - return strconv.ParseInt(r.Get(key), 10, 64) -} - -// GetDecoded returns the url-query-decoded user's value based on its key. -func (r RequestParams) GetDecoded(key string) string { - return DecodeQuery(DecodeQuery(r.Get(key))) -} - -// GetIntUnslashed same as Get but it removes the first slash if found. -// Usage: Get an id from a wildcard path. -// -// Returns -1 with an error if the parameter couldn't be found. -func (r RequestParams) GetIntUnslashed(key string) (int, error) { - v := r.Get(key) - if v != "" { - if len(v) > 1 { - if v[0] == '/' { - v = v[1:] - } - } - return strconv.Atoi(v) - - } - - return -1, errIntParse.Format(v) -} - -// Len returns the full length of the params. -func (r RequestParams) Len() int { - return len(r) -} diff --git a/core/memstore/memstore.go b/core/memstore/memstore.go new file mode 100644 index 00000000..674981b9 --- /dev/null +++ b/core/memstore/memstore.go @@ -0,0 +1,210 @@ +// Package memstore contains a store which is just +// a collection of key-value entries with immutability capabilities. +// +// Created after that proposal: https://github.com/iris-contrib/community-board/issues/5 +// and it's being used by session entries (session lifetime) and context's entries (per-request). +// +// Developers can use that storage to their own apps if they like its behavior. +// It's fast and in the same time you get read-only access (safety) when you need it. +package memstore + +import ( + "reflect" + "strconv" + + "github.com/kataras/iris/core/errors" +) + +type ( + // Entry is the entry of the context storage Store - .Values() + Entry struct { + Key string + value interface{} + immutable bool // if true then it can't change by its caller. + } + + // Store is a collection of key-value entries with immutability capabilities. + Store []Entry +) + +// Value returns the value of the entry, +// respects the immutable. +func (e Entry) Value() interface{} { + if e.immutable { + // take its value, no pointer even if setted with a rreference. + vv := reflect.Indirect(reflect.ValueOf(e.value)) + + // return copy of that slice + if vv.Type().Kind() == reflect.Slice { + newSlice := reflect.MakeSlice(vv.Type(), vv.Len(), vv.Cap()) + reflect.Copy(newSlice, vv) + return newSlice.Interface() + } + // return a copy of that map + if vv.Type().Kind() == reflect.Map { + newMap := reflect.MakeMap(vv.Type()) + for _, k := range vv.MapKeys() { + newMap.SetMapIndex(k, vv.MapIndex(k)) + } + return newMap.Interface() + } + // if was *value it will return value{}. + return vv.Interface() + } + return e.value +} + +// the id is immutable(true or false)+key +// so the users will be able to use the same key +// to store two different entries (one immutable and other mutable). +// or no? better no, that will confuse and maybe result on unexpected results. +// I will just replace the value and the immutable bool value when Set if +// a key is already exists. +// func (e Entry) identifier() string {} + +func (r *Store) save(key string, value interface{}, immutable bool) { + args := *r + n := len(args) + + // replace if we can, else just return + for i := 0; i < n; i++ { + kv := &args[i] + if kv.Key == key { + if immutable && kv.immutable { + // if called by `SetImmutable` + // then allow the update, maybe it's a slice that user wants to update by SetImmutable method, + // we should allow this + kv.value = value + kv.immutable = immutable + } else if kv.immutable == false { + // if it was not immutable then user can alt it via `Set` and `SetImmutable` + kv.value = value + kv.immutable = immutable + } + // else it was immutable and called by `Set` then disallow the update + return + } + } + + // expand slice to add it + c := cap(args) + if c > n { + args = args[:n+1] + kv := &args[n] + kv.Key = key + kv.value = value + kv.immutable = immutable + *r = args + return + } + + // add + kv := Entry{ + Key: key, + value: value, + immutable: immutable, + } + *r = append(args, kv) +} + +// Set saves a value to the key-value storage. +// See `SetImmutable` and `Get`. +func (r *Store) Set(key string, value interface{}) { + r.save(key, value, false) +} + +// SetImmutable saves a value to the key-value storage. +// Unlike `Set`, the output value cannot be changed by the caller later on (when .Get OR .Set) +// +// An Immutable entry should be only changed with a `SetImmutable`, simple `Set` will not work +// if the entry was immutable, for your own safety. +// +// Use it consistently, it's far slower than `Set`. +// Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 +func (r *Store) SetImmutable(key string, value interface{}) { + r.save(key, value, true) +} + +// Get returns the entry's value based on its key. +func (r *Store) Get(key string) interface{} { + args := *r + n := len(args) + for i := 0; i < n; i++ { + kv := &args[i] + if kv.Key == key { + return kv.Value() + } + } + + return nil +} + +// Visit accepts a visitor which will be filled +// by the key-value objects. +func (r *Store) Visit(visitor func(key string, value interface{})) { + args := *r + for i, n := 0, len(args); i < n; i++ { + kv := args[i] + visitor(kv.Key, kv.Value()) + } +} + +// GetString returns the entry's value as string, based on its key. +func (r *Store) GetString(key string) string { + if v, ok := r.Get(key).(string); ok { + return v + } + + return "" +} + +// ErrIntParse returns an error message when int parse failed +// it's not statical error, it depends on the failed value. +var ErrIntParse = errors.New("unable to find or parse the integer, found: %#v") + +// GetInt returns the entry's value as int, based on its key. +func (r *Store) GetInt(key string) (int, error) { + v := r.Get(key) + if vint, ok := v.(int); ok { + return vint, nil + } else if vstring, sok := v.(string); sok { + return strconv.Atoi(vstring) + } + + return -1, ErrIntParse.Format(v) +} + +// GetInt64 returns the entry's value as int64, based on its key. +func (r *Store) GetInt64(key string) (int64, error) { + return strconv.ParseInt(r.GetString(key), 10, 64) +} + +// Remove deletes an entry linked to that "key", +// returns true if an entry is actually removed. +func (r *Store) Remove(key string) bool { + args := *r + n := len(args) + for i := 0; i < n; i++ { + kv := &args[i] + if kv.Key == key { + // we found the index, + // let's remove the item by appending to the temp and + // after set the pointer of the slice to this temp args + args = append(args[:i], args[i+1:]...) + *r = args + return true + } + } + return false +} + +// Reset clears all the request entries. +func (r *Store) Reset() { + *r = (*r)[0:0] +} + +// Len returns the full length of the entries. +func (r *Store) Len() int { + args := *r + return len(args) +} diff --git a/core/memstore/memstore_test.go b/core/memstore/memstore_test.go new file mode 100644 index 00000000..4290dd6e --- /dev/null +++ b/core/memstore/memstore_test.go @@ -0,0 +1,93 @@ +package memstore + +import ( + "testing" +) + +type myTestObject struct { + name string +} + +func TestMuttable(t *testing.T) { + var p Store + + // slice + p.Set("slice", []myTestObject{{"value 1"}, {"value 2"}}) + v := p.Get("slice").([]myTestObject) + v[0].name = "modified" + + vv := p.Get("slice").([]myTestObject) + if vv[0].name != "modified" { + t.Fatalf("expected slice to be muttable but caller was not able to change its value") + } + + // map + + p.Set("map", map[string]myTestObject{"key 1": myTestObject{"value 1"}, "key 2": myTestObject{"value 2"}}) + vMap := p.Get("map").(map[string]myTestObject) + vMap["key 1"] = myTestObject{"modified"} + + vvMap := p.Get("map").(map[string]myTestObject) + if vvMap["key 1"].name != "modified" { + t.Fatalf("expected map to be muttable but caller was not able to change its value") + } + + // object pointer of a value, it can change like maps or slices and arrays. + p.Set("objp", &myTestObject{"value"}) + // we expect pointer here, as we set it. + vObjP := p.Get("objp").(*myTestObject) + + vObjP.name = "modified" + + vvObjP := p.Get("objp").(*myTestObject) + if vvObjP.name != "modified" { + t.Fatalf("expected objp to be muttable but caller was able to change its value") + } +} + +func TestImmutable(t *testing.T) { + var p Store + + // slice + p.SetImmutable("slice", []myTestObject{{"value 1"}, {"value 2"}}) + v := p.Get("slice").([]myTestObject) + v[0].name = "modified" + + vv := p.Get("slice").([]myTestObject) + if vv[0].name == "modified" { + t.Fatalf("expected slice to be immutable but caller was able to change its value") + } + + // map + p.SetImmutable("map", map[string]myTestObject{"key 1": myTestObject{"value 1"}, "key 2": myTestObject{"value 2"}}) + vMap := p.Get("map").(map[string]myTestObject) + vMap["key 1"] = myTestObject{"modified"} + + vvMap := p.Get("map").(map[string]myTestObject) + if vvMap["key 1"].name == "modified" { + t.Fatalf("expected map to be immutable but caller was able to change its value") + } + + // object value, it's immutable at all cases. + p.SetImmutable("obj", myTestObject{"value"}) + vObj := p.Get("obj").(myTestObject) + vObj.name = "modified" + + vvObj := p.Get("obj").(myTestObject) + if vvObj.name == "modified" { + t.Fatalf("expected obj to be immutable but caller was able to change its value") + } + + // object pointer of a value, it's immutable at all cases. + p.SetImmutable("objp", &myTestObject{"value"}) + // we expect no pointer here if SetImmutable. + // so it can't be changed by-design + vObjP := p.Get("objp").(myTestObject) + + vObjP.name = "modified" + + vvObjP := p.Get("objp").(myTestObject) + if vvObjP.name == "modified" { + t.Fatalf("expected objp to be immutable but caller was able to change its value") + } +} diff --git a/core/router/handler.go b/core/router/handler.go index c38f8462..4c907982 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -173,9 +173,8 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { } } - handlers, params, mustRedirect := t.Entry.ResolveRoute(ctx) + handlers, mustRedirect := t.Entry.ResolveRoute(ctx) if len(handlers) > 0 { - ctx.SetParams(params) ctx.SetHandlers(handlers) ctx.Handlers()[0](ctx) // to remove the .Next(maybe not a good idea), reduces the performance a bit: diff --git a/core/router/httprouter/node.go b/core/router/httprouter/node.go index 3078b954..2bd72840 100644 --- a/core/router/httprouter/node.go +++ b/core/router/httprouter/node.go @@ -329,11 +329,9 @@ func (n *Node) insertChild(numParams uint8, path, fullPath string, handle contex // given context. // // ResolveRoute finds the correct registered route from the Node when the ctx.Handlers() > 0. -func (n *Node) ResolveRoute(ctx context.Context) (handlers context.Handlers, p context.RequestParams, tsr bool) { //(p context.RequestParams, tsr bool) { +func (n *Node) ResolveRoute(ctx context.Context) (handlers context.Handlers, tsr bool) { //(p context.RequestParams, tsr bool) { path := ctx.Request().URL.Path handlers = ctx.Handlers() - // values := ctx.Values() - p = ctx.Params() walk: // outer loop for walking the tree for { if len(path) > len(n.path) { @@ -370,14 +368,7 @@ walk: // outer loop for walking the tree } // save param value - if cap(p) < int(n.maxParams) { - p = make(context.RequestParams, 0, n.maxParams) - } - i := len(p) - p = p[:i+1] // expand - p[i].Key = n.path[1:] - p[i].Value = path[:end] - + ctx.Params().Set(n.path[1:], path[:end]) // we need to go deeper! if end < len(path) { if len(n.children) > 0 { @@ -403,15 +394,7 @@ walk: // outer loop for walking the tree return case catchAll: - // save param value - if cap(p) < int(n.maxParams) { - p = make(context.RequestParams, 0, n.maxParams) - } - i := len(p) - p = p[:i+1] // expand - - p[i].Key = n.path[2:] - p[i].Value = path + ctx.Params().Set(n.path[2:], path) handlers = n.handle return diff --git a/iris.go b/iris.go index 73ed5f20..3b0d416c 100644 --- a/iris.go +++ b/iris.go @@ -40,7 +40,7 @@ const ( // Version is the current version number of the Iris Web framework. // // Look https://github.com/kataras/iris#where-can-i-find-older-versions for older versions. - Version = "7.0.1" + Version = "7.0.2" ) const ( diff --git a/sessions/provider.go b/sessions/provider.go index d5ba518f..240dd37e 100644 --- a/sessions/provider.go +++ b/sessions/provider.go @@ -7,6 +7,8 @@ package sessions import ( "sync" "time" + + "github.com/kataras/iris/core/memstore" ) type ( @@ -44,7 +46,7 @@ func (p *provider) newSession(sid string, expires time.Duration) *session { sess := &session{ sid: sid, provider: p, - values: p.loadSessionValues(sid), + values: p.loadSessionValuesFromDB(sid), flashes: make(map[string]*flashMessage), } @@ -60,20 +62,32 @@ func (p *provider) newSession(sid string, expires time.Duration) *session { return sess } -func (p *provider) loadSessionValues(sid string) map[string]interface{} { +// can return nil +func (p *provider) loadSessionValuesFromDB(sid string) memstore.Store { + var store memstore.Store for i, n := 0, len(p.databases); i < n; i++ { if dbValues := p.databases[i].Load(sid); dbValues != nil && len(dbValues) > 0 { - return dbValues // return the first non-empty from the registered stores. + for k, v := range dbValues { + store.Set(k, v) + } } } - values := make(map[string]interface{}) - return values + return store } -func (p *provider) updateDatabases(sid string, newValues map[string]interface{}) { - for i, n := 0, len(p.databases); i < n; i++ { - p.databases[i].Update(sid, newValues) +func (p *provider) updateDatabases(sid string, store memstore.Store) { + + if l := store.Len(); l > 0 { + mapValues := make(map[string]interface{}, l) + + store.Visit(func(k string, v interface{}) { + mapValues[k] = v + }) + + for i, n := 0, len(p.databases); i < n; i++ { + p.databases[i].Update(sid, mapValues) + } } } diff --git a/sessions/session.go b/sessions/session.go index ffab66ee..9c8520c7 100644 --- a/sessions/session.go +++ b/sessions/session.go @@ -10,6 +10,7 @@ import ( "time" "github.com/kataras/iris/core/errors" + "github.com/kataras/iris/core/memstore" ) type ( @@ -18,7 +19,7 @@ type ( // implements the context.Session interface session struct { sid string - values map[string]interface{} // here are the real values + values memstore.Store // here are the real values // we could set the flash messages inside values but this will bring us more problems // because of session databases and because of // users may want to get all sessions and save them or display them @@ -45,10 +46,10 @@ func (s *session) ID() string { return s.sid } -// Get returns the value of an entry by its key +// Get returns a value based on its "key". func (s *session) Get(key string) interface{} { s.mu.RLock() - value := s.values[key] + value := s.values.Get(key) s.mu.RUnlock() return value @@ -215,8 +216,8 @@ func (s *session) GetBoolean(key string) (bool, error) { func (s *session) GetAll() map[string]interface{} { items := make(map[string]interface{}, len(s.values)) s.mu.RLock() - for key, v := range s.values { - items[key] = v + for _, kv := range s.values { + items[kv.Key] = kv.Value() } s.mu.RUnlock() return items @@ -237,21 +238,36 @@ func (s *session) GetFlashes() map[string]interface{} { // VisitAll loop each one entry and calls the callback function func(key,value) func (s *session) VisitAll(cb func(k string, v interface{})) { - for key := range s.values { - cb(key, s.values[key]) - } + s.values.Visit(cb) } -// Set fills the session with an entry, it receives a key and a value -// returns an error, which is always nil -func (s *session) Set(key string, value interface{}) { +func (s *session) set(key string, value interface{}, immutable bool) { s.mu.Lock() - s.values[key] = value + if immutable { + s.values.Set(key, value) + } else { + s.values.SetImmutable(key, value) + } s.mu.Unlock() s.updateDatabases() } +// Set fills the session with an entry"value", based on its "key". +func (s *session) Set(key string, value interface{}) { + s.set(key, value, false) +} + +// SetImmutable fills the session with an entry "value", based on its "key". +// Unlike `Set`, the output value cannot be changed by the caller later on (when .Get) +// An Immutable entry should be only changed with a `SetImmutable`, simple `Set` will not work +// if the entry was immutable, for your own safety. +// Use it consistently, it's far slower than `Set`. +// Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 +func (s *session) SetImmutable(key string, value interface{}) { + s.set(key, value, true) +} + // SetFlash sets a flash message by its key. // // A flash message is used in order to keep a message in session through one or several requests of the same user. @@ -278,13 +294,15 @@ func (s *session) SetFlash(key string, value interface{}) { s.mu.Unlock() } -// Delete removes an entry by its key -func (s *session) Delete(key string) { +// Delete removes an entry by its key, +// returns true if actually something was removed. +func (s *session) Delete(key string) bool { s.mu.Lock() - delete(s.values, key) + removed := s.values.Remove(key) s.mu.Unlock() s.updateDatabases() + return removed } func (s *session) updateDatabases() { @@ -301,9 +319,7 @@ func (s *session) DeleteFlash(key string) { // Clear removes all entries func (s *session) Clear() { s.mu.Lock() - for key := range s.values { - delete(s.values, key) - } + s.values.Reset() s.mu.Unlock() s.updateDatabases() diff --git a/sessions/sessions.go b/sessions/sessions.go index 814679c2..d982b355 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -47,8 +47,9 @@ type ( GetFlashes() map[string]interface{} VisitAll(cb func(k string, v interface{})) Set(string, interface{}) + SetImmutable(key string, value interface{}) SetFlash(string, interface{}) - Delete(string) + Delete(string) bool DeleteFlash(string) Clear() ClearFlashes()