From 14df01f540cee3dcd29a16ec7aa4d6252237abaf Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Sat, 31 Dec 2022 14:25:54 -0800 Subject: [PATCH] Allow Session to store values with a specified type V * `Session` state is now a `map[string]V` instead of a `map[string]any` and uses a generics type parameter * Update `Set`, `Get`, and `GetOk` methods to use generic type `V` * Change `Session` to `Session[V any]` to specify the type of value stored in the Session * See updated usage docs for examples * Change `Store` to `Store[V any]` to specify the type of value stored in sessions * Change `NewCookieStore` to `NewCookieStore[V any]` to specify the type of value stored in sessions If you prefer to continue storing a map with `any` values, ``` // Store[any] store := NewCookieStore[any](cookieConfig, keyPairs...) ``` --- CHANGES.md | 8 ++++++++ README.md | 14 +++++++++++--- sessions.go | 24 ++++++++++++------------ store.go | 26 ++++++++++++++------------ 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 11ca8fb..f08d6aa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,14 @@ Notable changes between releases. ## Latest +* Allow `Session` to store values with specified type (`V`) (i.e. generics) ([#22](https://github.com/dghubble/sessions/pull/21)) + * `Session` state is now a `map[string]V` instead of a `map[string]any` + * Update `Set`, `Get`, and `GetOk` methods to use generic type `V` + * Change `Session` to `Session[V any]` to specify the type of value stored in the Session + * See updated usage docs for examples +* Change `Store` to `Store[V any]` to specify the type of value stored in sessions +* Change `NewCookieStore` to `NewCookieStore[V any]` to specify the type of value stored in sessions + ## v0.3.0 * Change `CookieStore` and its fields to be non-exported ([#19](https://github.com/dghubble/sessions/pull/19)) diff --git a/README.md b/README.md index 758577f..cbf78be 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Read [GoDoc](https://godoc.org/github.com/dghubble/sessions) Create a `Store` for managing `Session`'s. `NewCookieStore` returns a `Store` that signs and optionally encrypts cookies to support user sessions. +A `Session` stores a map of key/value pairs (e.g. "userID": "a1b2c3"). Starting with v0.4.0, `sessions` uses Go generics to allow specifying a type for stored values.Previously, values were type `interface{}` or `any`, which required type assertions. + ```go import ( "github.com/dghubble/sessions" @@ -31,7 +33,7 @@ import ( func NewServer() (http.Handler) { ... // client-side cookies - sessionProvider := sessions.NewCookieStore( + store := sessions.NewCookieStore[string]( sessions.DefaultCookieConfig, // use a 32 byte or 64 byte hash key []byte("signing-secret"), @@ -39,6 +41,7 @@ func NewServer() (http.Handler) { []byte("encryption-secret") ) ... + server.sessions = store } ``` @@ -50,7 +53,7 @@ func (s server) Login() http.Handler { // create a session session := s.sessions.New("my-app") // add user-id to session - session.Set("user-id", 123) + session.Set("user-id", "a1b2c3") // save the session to the response if err := session.Save(w); err != nil { // handle error @@ -72,7 +75,12 @@ func (s server) RequireLogin() http.Handler { return } - userID := session.Get("user-id") + userID, present := session.GetOk("user-id") + if !present { + http.Error(w, "missing user-id", http.StatusUnauthorized) + return + } + fmt.Fprintf(w, `

Welcome %d!

diff --git a/sessions.go b/sessions.go index 0e3021a..4975a63 100644 --- a/sessions.go +++ b/sessions.go @@ -9,51 +9,51 @@ const ( ) // Session represents state values maintained in a sessions Store. -type Session struct { +type Session[V any] struct { name string - values map[string]any + values map[string]V // convenience methods Save and Destroy use store - store Store + store Store[V] } // NewSession returns a new Session. -func NewSession(store Store, name string) *Session { - return &Session{ +func NewSession[V any](store Store[V], name string) *Session[V] { + return &Session[V]{ name: name, - values: make(map[string]any), + values: make(map[string]V), store: store, } } // Name returns the name of the session. -func (s *Session) Name() string { +func (s *Session[V]) Name() string { return s.name } // Set sets a key/value pair in the session state. -func (s *Session) Set(key string, value any) { +func (s *Session[V]) Set(key string, value V) { s.values[key] = value } // Get returns the state value for the given key. -func (s *Session) Get(key string) any { +func (s *Session[V]) Get(key string) V { return s.values[key] } // GetOk returns the state value for the given key and whether they key exists. -func (s *Session) GetOk(key string) (any, bool) { +func (s *Session[V]) GetOk(key string) (V, bool) { value, ok := s.values[key] return value, ok } // Save adds or updates the session. Identical to calling // store.Save(w, session). -func (s *Session) Save(w http.ResponseWriter) error { +func (s *Session[V]) Save(w http.ResponseWriter) error { return s.store.Save(w, s) } // Destroy destroys the session. Identical to calling // store.Destroy(w, session.name). -func (s *Session) Destroy(w http.ResponseWriter) { +func (s *Session[V]) Destroy(w http.ResponseWriter) { s.store.Destroy(w, s.name) } diff --git a/store.go b/store.go index 30437c6..474badf 100644 --- a/store.go +++ b/store.go @@ -7,19 +7,21 @@ import ( ) // A Store manages creating, accessing, writing, and expiring Sessions. -type Store interface { +type Store[V any] interface { // New returns a new named Session - New(name string) *Session + New(name string) *Session[V] // Get a named Session from the request - Get(req *http.Request, name string) (*Session, error) + Get(req *http.Request, name string) (*Session[V], error) // Save writes a Session to the ResponseWriter - Save(w http.ResponseWriter, session *Session) error + Save(w http.ResponseWriter, session *Session[V]) error // Destroy removes (expires) a named Session Destroy(w http.ResponseWriter, name string) } +var _ Store[any] = &cookieStore[any]{} + // CookieStore stores Sessions in secure cookies (i.e. client-side) -type cookieStore struct { +type cookieStore[V any] struct { config *CookieConfig // encodes and decodes signed and optionally encrypted cookie values codecs []securecookie.Codec @@ -27,26 +29,26 @@ type cookieStore struct { // NewCookieStore returns a new Store that signs and optionally encrypts // session state in http cookies. -func NewCookieStore(config *CookieConfig, keyPairs ...[]byte) Store { +func NewCookieStore[V any](config *CookieConfig, keyPairs ...[]byte) Store[V] { if config == nil { config = DefaultCookieConfig } - return &cookieStore{ + return &cookieStore[V]{ config: config, codecs: securecookie.CodecsFromPairs(keyPairs...), } } // New returns a new named Session. -func (s *cookieStore) New(name string) *Session { - return NewSession(s, name) +func (s *cookieStore[V]) New(name string) *Session[V] { + return NewSession[V](s, name) } // Get returns the named Session from the Request. Returns an error if the // session cookie cannot be found, the cookie verification fails, or an error // occurs decoding the cookie value. -func (s *cookieStore) Get(req *http.Request, name string) (session *Session, err error) { +func (s *cookieStore[V]) Get(req *http.Request, name string) (session *Session[V], err error) { cookie, err := req.Cookie(name) if err == nil { session = s.New(name) @@ -58,7 +60,7 @@ func (s *cookieStore) Get(req *http.Request, name string) (session *Session, err // Save adds or updates the Session on the response via a signed and optionally // encrypted session cookie. Session Values are encoded into the cookie value // and the session Config sets cookie properties. -func (s *cookieStore) Save(w http.ResponseWriter, session *Session) error { +func (s *cookieStore[V]) Save(w http.ResponseWriter, session *Session[V]) error { cookieValue, err := securecookie.EncodeMulti(session.Name(), &session.values, s.codecs...) if err != nil { return err @@ -69,6 +71,6 @@ func (s *cookieStore) Save(w http.ResponseWriter, session *Session) error { // Destroy deletes the Session with the given name by issuing an expired // session cookie with the same name. -func (s *cookieStore) Destroy(w http.ResponseWriter, name string) { +func (s *cookieStore[V]) Destroy(w http.ResponseWriter, name string) { http.SetCookie(w, newCookie(name, "", &CookieConfig{MaxAge: -1, Path: s.config.Path})) }