Skip to content

Commit

Permalink
Allow Session to store values with a specified type V
Browse files Browse the repository at this point in the history
* `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...)
```
  • Loading branch information
dghubble committed Dec 31, 2022
1 parent fa6acac commit 14df01f
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 27 deletions.
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -31,14 +33,15 @@ 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"),
// use a 32 byte (AES-256) encryption key
[]byte("encryption-secret")
)
...
server.sessions = store
}
```

Expand All @@ -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
Expand All @@ -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, `<p>Welcome %d!</p>
<form action="/logout" method="post">
<input type="submit" value="Logout">
Expand Down
24 changes: 12 additions & 12 deletions sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
26 changes: 14 additions & 12 deletions store.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,48 @@ 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
}

// 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)
Expand All @@ -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
Expand All @@ -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}))
}

0 comments on commit 14df01f

Please sign in to comment.