Skip to content
This repository has been archived by the owner on May 1, 2020. It is now read-only.

Commit

Permalink
Introduce the SessionInfo interface
Browse files Browse the repository at this point in the history
Introduce a new SessionInfo interface and a SessionInfoParser
function type to make session info objects immutable and thus
data race save.

Adjust tests and examples according to new API changes.
  • Loading branch information
romshark committed Apr 8, 2018
1 parent 6c4011d commit 2b6da64
Show file tree
Hide file tree
Showing 56 changed files with 646 additions and 246 deletions.
43 changes: 29 additions & 14 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,24 @@ func (clt *Client) CreateSession(attachment SessionInfo) error {
}

func (clt *Client) notifySessionCreated(newSession *Session) error {
encoded, err := json.Marshal(&newSession)
// Serialize session info
var sessionInfo map[string]interface{}
if newSession.Info != nil {
sessionInfo = make(map[string]interface{})
for _, field := range newSession.Info.Fields() {
sessionInfo[field] = newSession.Info.Value(field)
}
}

encoded, err := json.Marshal(struct {
Key string `json:"k"`
Creation time.Time `json:"c"`
Info map[string]interface{} `json:"i"`
}{
newSession.Key,
newSession.Creation,
sessionInfo,
})
if err != nil {
return fmt.Errorf("Couldn't marshal session object: %s", err)
}
Expand Down Expand Up @@ -191,19 +208,22 @@ func (clt *Client) HasSession() bool {
return clt.session != nil
}

// Session returns either a shallow copy of the session if there's a session currently assigned
// to the server this user agent refers to, or nil if there's none
// Session returns an exact copy of the session object or nil if there's no
// session currently assigned to this client
func (clt *Client) Session() *Session {
clt.sessionLock.RLock()
defer clt.sessionLock.RUnlock()
if clt.session == nil {
return nil
}
return &Session{
clone := &Session{
Key: clt.session.Key,
Creation: clt.session.Creation,
Info: clt.session.Info,
}
if clt.session.Info != nil {
clone.Info = clt.session.Info.Copy()
}
return clone
}

// SessionKey returns the key of the currently assigned session of the client this client agent
Expand All @@ -229,18 +249,13 @@ func (clt *Client) SessionCreation() time.Time {
return clt.session.Creation
}

// SessionInfo returns the value of a session info field identified by the given key
// in the form of an empty interface that could be casted to either a string, bool, float64 number
// a map[string]interface{} object or an []interface{} array according to JSON data types.
// Returns nil if either there's no session or if the given field doesn't exist
func (clt *Client) SessionInfo(key string) interface{} {
// SessionInfo returns a copy of the session info field value
// in the form of an empty interface to be casted to either concrete type
func (clt *Client) SessionInfo(name string) interface{} {
clt.sessionLock.RLock()
defer clt.sessionLock.RUnlock()
if clt.session == nil || clt.session.Info == nil {
return nil
}
if value, exists := clt.session.Info[key]; exists {
return value
}
return nil
return clt.session.Info.Value(name)
}
31 changes: 18 additions & 13 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const (
type Client struct {
serverAddr string
impl Implementation
sessionInfoParser SessionInfoParser
status Status
defaultReqTimeout time.Duration
reconnInterval time.Duration
Expand Down Expand Up @@ -98,6 +99,7 @@ func NewClient(
newClt := &Client{
serverAddress,
implementation,
opts.SessionInfoParser,
StatDisconnected,
opts.DefaultRequestTimeout,
opts.ReconnectionInterval,
Expand Down Expand Up @@ -222,30 +224,33 @@ func (clt *Client) Signal(name string, payload webwire.Payload) error {
return clt.conn.Write(msgBytes)
}

// Session returns information about the current session
func (clt *Client) Session() webwire.Session {
// Session returns an exact copy of the session object or nil if there's no
// session currently assigned to this client
func (clt *Client) Session() *webwire.Session {
clt.sessionLock.RLock()
defer clt.sessionLock.RUnlock()
if clt.session == nil {
return webwire.Session{}
return nil
}
clone := &webwire.Session{
Key: clt.session.Key,
Creation: clt.session.Creation,
}
if clt.session.Info != nil {
clone.Info = clt.session.Info.Copy()
}
return *clt.session
return clone
}

// SessionInfo returns the value of a session info field identified by the given key
// in the form of an empty interface that could be casted to either a string, bool, float64 number
// a map[string]interface{} object or an []interface{} array according to JSON data types.
// Returns nil if either there's no session or if the given field doesn't exist.
func (clt *Client) SessionInfo(key string) interface{} {
// SessionInfo returns a copy of the session info field value
// in the form of an empty interface to be casted to either concrete type
func (clt *Client) SessionInfo(fieldName string) interface{} {
clt.sessionLock.RLock()
defer clt.sessionLock.RUnlock()
if clt.session == nil || clt.session.Info == nil {
return nil
}
if value, exists := clt.session.Info[key]; exists {
return value
}
return nil
return clt.session.Info.Value(fieldName)
}

// PendingRequests returns the number of currently pending requests
Expand Down
122 changes: 122 additions & 0 deletions client/genericSessionInfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package client

import (
"reflect"

webwire "github.com/qbeon/webwire-go"
)

func deepCopy(src interface{}) interface{} {
if src == nil {
return nil
}

// Make the interface a reflect.Value
original := reflect.ValueOf(src)

// Make a copy of the same type as the original.
cpy := reflect.New(original.Type()).Elem()

// Recursively copy the original.
copyRecursive(original, cpy)

// Return the copy as an interface.
return cpy.Interface()
}

func copyRecursive(original, cpy reflect.Value) {
// handle according to original's Kind
switch original.Kind() {
case reflect.Interface:
// If this is a nil, don't do anything
if original.IsNil() {
return
}
// Get the value for the interface, not the pointer.
originalValue := original.Elem()

// Get the value by calling Elem().
copyValue := reflect.New(originalValue.Type()).Elem()
copyRecursive(originalValue, copyValue)
cpy.Set(copyValue)

case reflect.Slice:
if original.IsNil() {
return
}
// Make a new slice and copy each element.
cpy.Set(reflect.MakeSlice(
original.Type(),
original.Len(),
original.Cap(),
))
for i := 0; i < original.Len(); i++ {
copyRecursive(original.Index(i), cpy.Index(i))
}

case reflect.Map:
if original.IsNil() {
return
}
cpy.Set(reflect.MakeMap(original.Type()))
for _, key := range original.MapKeys() {
originalValue := original.MapIndex(key)
copyValue := reflect.New(originalValue.Type()).Elem()
copyRecursive(originalValue, copyValue)
copyKey := deepCopy(key.Interface())
cpy.SetMapIndex(reflect.ValueOf(copyKey), copyValue)
}

default:
cpy.Set(original)
}
}

// GenericSessionInfo defines a default webwire.SessionInfo interface
// implementation type used by the client when no explicit session info
// parser is used
type GenericSessionInfo struct {
data map[string]interface{}
}

// Copy implements the webwire.SessionInfo interface.
// It deep-copies the object and returns it's exact clone
func (sinf *GenericSessionInfo) Copy() webwire.SessionInfo {
return &GenericSessionInfo{
data: deepCopy(sinf.data).(map[string]interface{}),
}
}

// Fields implements the webwire.SessionInfo interface.
// It returns a constant list of the names of all fields of the object
func (sinf *GenericSessionInfo) Fields() []string {
if sinf.data == nil {
return make([]string, 0)
}
names := make([]string, len(sinf.data))
index := 0
for fieldName := range sinf.data {
names[0] = fieldName
index++
}
return names
}

// Value implements the webwire.SessionInfo interface.
// It returns an exact deep copy of a session info field value
func (sinf *GenericSessionInfo) Value(fieldName string) interface{} {
if sinf.data == nil {
return nil
}
if val, exists := sinf.data[fieldName]; exists {
return deepCopy(val)
}
return nil
}

// GenericSessionInfoParser represents a default implementation of a
// session info object parser. It parses the info object into a generic
// session info type implementing the webwire.SessionInfo interface
func GenericSessionInfoParser(data map[string]interface{}) webwire.SessionInfo {
return &GenericSessionInfo{data}
}
24 changes: 19 additions & 5 deletions client/handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,37 @@ package client

import (
"encoding/json"
"time"

webwire "github.com/qbeon/webwire-go"
)

func (clt *Client) handleSessionCreated(msgPayload webwire.Payload) {
// Set new session
var session webwire.Session
var encoded struct {
Key string `json:"k"`
Creation time.Time `json:"c"`
Info map[string]interface{} `json:"i"`
}

if err := json.Unmarshal(msgPayload.Data, &session); err != nil {
if err := json.Unmarshal(msgPayload.Data, &encoded); err != nil {
clt.errorLog.Printf("Failed unmarshalling session object: %s", err)
return
}

// parse attached session info
var parsedSessInfo webwire.SessionInfo
if encoded.Info != nil && clt.sessionInfoParser != nil {
parsedSessInfo = clt.sessionInfoParser(encoded.Info)
}

clt.sessionLock.Lock()
clt.session = &session
clt.session = &webwire.Session{
Key: encoded.Key,
Creation: encoded.Creation,
Info: parsedSessInfo,
}
clt.sessionLock.Unlock()
clt.impl.OnSessionCreated(&session)
clt.impl.OnSessionCreated(clt.session)
}

func (clt *Client) handleSessionClosed() {
Expand Down
4 changes: 4 additions & 0 deletions client/interface.go → client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ type Implementation interface {
// either by the server or the client itself
OnSessionClosed()
}

// SessionInfoParser is invoked during the parsing of a newly assigned
// session, it must return a webwire.SessionInfo compliant object
type SessionInfoParser func(data map[string]interface{}) webwire.SessionInfo
9 changes: 9 additions & 0 deletions client/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"io"
"os"
"time"

webwire "github.com/qbeon/webwire-go"
)

// OptionToggle represents the value of a togglable option
Expand All @@ -22,6 +24,9 @@ const (

// Options represents the options used during the creation a new client instance
type Options struct {
// SessionInfoParser defines the optional session info parser function
SessionInfoParser func(map[string]interface{}) webwire.SessionInfo

// DefaultRequestTimeout defines the default request timeout duration
// used by client.Request and client.RestoreSession
DefaultRequestTimeout time.Duration
Expand Down Expand Up @@ -51,6 +56,10 @@ type Options struct {

// SetDefaults sets default values for undefined required options
func (opts *Options) SetDefaults() {
if opts.SessionInfoParser == nil {
opts.SessionInfoParser = GenericSessionInfoParser
}

if opts.DefaultRequestTimeout < 1 {
opts.DefaultRequestTimeout = 60 * time.Second
}
Expand Down
2 changes: 1 addition & 1 deletion examples/chatroom/client/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
// OnSessionCreated implements the webwireClient.Implementation interface
// it's invoked when a new session is assigned to the client
func (clt *ChatroomClient) OnSessionCreated(newSession *webwire.Session) {
username := newSession.Info["username"].(string)
username := newSession.Info.Value("username").(string)
log.Printf("Authenticated as %s", username)
}

Expand Down
11 changes: 8 additions & 3 deletions examples/chatroom/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"flag"
"time"

"github.com/qbeon/webwire-go/examples/chatroom/shared"

wwrclt "github.com/qbeon/webwire-go/client"
)

Expand All @@ -21,10 +23,13 @@ func NewChatroomClient(serverAddr string) *ChatroomClient {
serverAddr,
newChatroomClient,
wwrclt.Options{
// Address of the webwire server
// Default timeout for timed requests
DefaultRequestTimeout: 10 * time.Second,
ReconnectionInterval: 2 * time.Second,
// Default timeout for timed requests
ReconnectionInterval: 2 * time.Second,

// Session info parser function must override the default one
// for the session info object to be typed as shared.SessionInfo
SessionInfoParser: shared.SessionInfoParser,
},
)

Expand Down
1 change: 1 addition & 0 deletions examples/chatroom/client/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func (clt *ChatroomClient) Start() {
MAINLOOP:
for {
input, _ := reader.ReadString('\n')

// Remove new-line character
input = input[:len(input)-1]

Expand Down
4 changes: 2 additions & 2 deletions examples/chatroom/server/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ func (srv *ChatRoomServer) handleAuth(
}

// Finally create a new session
if err := client.CreateSession(wwr.SessionInfo{
"username": credentials.Name,
if err := client.CreateSession(&shared.SessionInfo{
Username: credentials.Name,
}); err != nil {
return wwr.Payload{}, fmt.Errorf("Couldn't create session: %s", err)
}
Expand Down

0 comments on commit 2b6da64

Please sign in to comment.