Skip to content

Commit

Permalink
Export the ability of custom ConnHandlers and a lot of improvements t…
Browse files Browse the repository at this point in the history
…hat allow externals package to interact with neffos structs-to-events feature such as Iris' new v11.2+ fully featured MVC websocket controller
  • Loading branch information
kataras committed Jul 9, 2019
1 parent c2363a4 commit 26005b6
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 64 deletions.
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func Dial(ctx context.Context, dial Dialer, url string, connHandler ConnHandler)
connHandler = Namespaces{}
}

c := newConn(underline, connHandler.getNamespaces())
c := newConn(underline, connHandler.GetNamespaces())
readTimeout, writeTimeout := getTimeouts(connHandler)
c.readTimeout = readTimeout
c.writeTimeout = writeTimeout
Expand Down
70 changes: 53 additions & 17 deletions conn_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (
"time"
)

// ConnHandler is the interface which
// `Events`, `Namespaces` and `WithTimeout` are implement.
// It's exported just to be used on the `Dial`(client) and `New` (server) functions.
// ConnHandler is the interface which namespaces and events can be retrieved through.
// Built-in ConnHandlers are the`Events`, `Namespaces`, `WithTimeout` and `NewStruct`.
// Users of this are the `Dial`(client) and `New` (server) functions.
type ConnHandler interface {
getNamespaces() Namespaces
GetNamespaces() Namespaces
}

var (
Expand All @@ -31,7 +31,8 @@ var (
// See `Namespaces`, `New` and `Dial` too.
type Events map[string]MessageHandlerFunc

func (e Events) getNamespaces() Namespaces {
// GetNamespaces returns an empty namespace with the "e" Events.
func (e Events) GetNamespaces() Namespaces {
return Namespaces{"": e}
}

Expand All @@ -55,7 +56,8 @@ func (e Events) fireEvent(c *NSConn, msg Message) error {
// See `WithTimeout`, `New` and `Dial` too.
type Namespaces map[string]Events

func (nss Namespaces) getNamespaces() Namespaces { return nss }
// GetNamespaces just returns the "nss" namespaces.
func (nss Namespaces) GetNamespaces() Namespaces { return nss }

// WithTimeout completes the `ConnHandler` interface.
// Can be used to register namespaces and events or just events on an empty namespace
Expand All @@ -70,8 +72,10 @@ type WithTimeout struct {
Events Events
}

func (t WithTimeout) getNamespaces() Namespaces {
return JoinConnHandlers(t.Namespaces, t.Events).getNamespaces()
// GetNamespaces returns combined namespaces from "Namespaces" and "Events" fields
// with read and write timeouts from "ReadTimeout" and "WriteTimeout" fields of "t".
func (t WithTimeout) GetNamespaces() Namespaces {
return JoinConnHandlers(t.Namespaces, t.Events).GetNamespaces()
}

func getTimeouts(h ConnHandler) (readTimeout time.Duration, writeTimeout time.Duration) {
Expand Down Expand Up @@ -103,6 +107,9 @@ type Struct struct {
eventMatcher EventMatcherFunc
readTimeout, writeTimeout time.Duration

// This field is set when external dependency injection system is used.
injector StructInjector

events Events
}

Expand Down Expand Up @@ -157,6 +164,18 @@ func (s *Struct) SetTimeouts(read, write time.Duration) *Struct {
return s
}

// SetInjector sets a custom injector and overrides the neffos default behavior
// on dynamic structs.
// The "fn" should handle to fill static fields and the NSConn.
// This "fn" will only be called when dynamic struct "ptr" is passed
// on the `NewStruct`.
// The caller should return a
// valid type of "ptr" reflect.Value.
func (s *Struct) SetInjector(fn StructInjector) *Struct {
s.injector = fn
return s
}

// NewStruct returns a new Struct value instance type of ConnHandler.
// The "ptr" should be a pointer to a struct.
// This function is used when you want to convert a structure to
Expand All @@ -173,7 +192,27 @@ func (s *Struct) SetTimeouts(read, write time.Duration) *Struct {
//
// Note that this method has a tiny performance cost when an event's callback's logic has small footprint.
func NewStruct(ptr interface{}) *Struct {
typ := reflect.TypeOf(ptr) // use for methods with receiver Ptr.
if ptr == nil {
panic("NewStruct: value is nil")
}

if s, ok := ptr.(*Struct); ok { // if it's already a *Struct then just return it.
return s
}

var v reflect.Value // use for methods with receiver Ptr.
if rValue, ok := ptr.(reflect.Value); ok {
v = rValue
} else {
v = reflect.ValueOf(ptr)
}

if !v.IsValid() {
panic("NewStruct: value is not a valid one")
}

typ := v.Type() // use for methods with receiver Ptr.

if typ.Kind() != reflect.Ptr {
panic("NewStruct: value should be a pointer")
}
Expand All @@ -186,11 +225,6 @@ func NewStruct(ptr interface{}) *Struct {
panic("NewStruct: value does not points to a struct")
}

v := reflect.ValueOf(ptr) // use for methods with receiver Ptr.
if !v.IsValid() {
panic("NewStruct: value is not a valid one")
}

n := typ.NumMethod()
_, hasNamespaceMethod := typ.MethodByName("Namespace")
if n == 0 || (n == 1 && hasNamespaceMethod) {
Expand All @@ -212,11 +246,13 @@ func (s *Struct) Events() Events {
return s.events
}

s.events = makeEventsFromStruct(s.ptr, s.eventMatcher)
s.events = makeEventsFromStruct(s.ptr, s.eventMatcher, s.injector)
return s.events
}

func (s *Struct) getNamespaces() Namespaces { // completes the `ConnHandler` interface.
// GetNamespaces creates and returns Namespaces based on the
// pointer to struct value provided by the "s".
func (s *Struct) GetNamespaces() Namespaces { // completes the `ConnHandler` interface.
if s.namespace == "" {
s.namespace, _ = resolveStructNamespace(s.ptr)
}
Expand All @@ -233,7 +269,7 @@ func JoinConnHandlers(connHandlers ...ConnHandler) ConnHandler {
namespaces := Namespaces{}

for _, h := range connHandlers {
nss := h.getNamespaces()
nss := h.GetNamespaces()
if len(nss) > 0 {
for namespace, events := range nss {
if events == nil {
Expand Down
6 changes: 3 additions & 3 deletions conn_handler_struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestConnHandlerStructStatic(t *testing.T) {
v := new(testStructStatic)
v.Err = fmt.Errorf("from static")
s := NewStruct(v)
nss := s.getNamespaces()
nss := s.GetNamespaces()

if expected, got := v.Namespace(), s.namespace; expected != got {
t.Fatalf("expected namespace to be: %s but got: %s", expected, got)
Expand Down Expand Up @@ -56,7 +56,7 @@ func TestConnHandlerStructDynamic(t *testing.T) {
StaticFieldErr: fmt.Errorf("a static field which should be set on each new testStructDynamic"),
}
s := NewStruct(v)
nss := s.getNamespaces()
nss := s.GetNamespaces()

nsConn := &NSConn{namespace: s.namespace}
nss[s.namespace][OnNamespaceConnect](nsConn, Message{Namespace: s.namespace})
Expand Down Expand Up @@ -84,7 +84,7 @@ func TestConnHandlerStructDynamicEmbedded(t *testing.T) {
v := new(testStructDynamicEmbedded)
s := NewStruct(v)
s.namespace = "default"
nss := s.getNamespaces()
nss := s.GetNamespaces()

nsConn := &NSConn{namespace: s.namespace}
nss[s.namespace][OnNamespaceConnect](nsConn, Message{Namespace: s.namespace})
Expand Down
6 changes: 3 additions & 3 deletions conn_namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package neffos

import (
"context"
"reflect"
"sync"
"sync/atomic"
)

// NSConn describes a connection connected to a specific namespace,
Expand All @@ -25,9 +25,9 @@ type NSConn struct {
rooms map[string]*Room
roomsMutex sync.RWMutex

// Value is just a temporarily atomic value.
// value is just a temporarily value.
// Storage across event callbacks for this namespace.
Value atomic.Value
value reflect.Value
}

func newNSConn(c *Conn, namespace string, events Events) *NSConn {
Expand Down
5 changes: 3 additions & 2 deletions debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ func EnableDebug(printer interface{}) {
}

if printer == nil {
printer = log.New(os.Stderr, "| neffos | ", 0)
logger := log.New(os.Stderr, "| neffos | ", 0)
printer = logger
logger.Println("debug mode is set")
}

debugPrinter = printer
debugf("debug mode is set")
}

type (
Expand Down
86 changes: 49 additions & 37 deletions reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package neffos

import (
"reflect"
"strings"
)

func indirectType(typ reflect.Type) reflect.Type {
Expand Down Expand Up @@ -109,8 +110,8 @@ func resolveStructNamespace(v reflect.Value) (string, bool) {
if ok {
if getNamespace, ok := v.Method(method.Index).Interface().(func() string); ok {
namespace := getNamespace()
debugf("set namespace [%s] from method [%s.%s]", func() dargs {
return dargs{namespace, indirectType(typ).Name(), method.Name}
debugf("Set namespace [\"%s\"] from method [%s.%s]", func() dargs {
return dargs{namespace, nameOf(typ), method.Name}
})

return namespace, true
Expand All @@ -123,8 +124,8 @@ func resolveStructNamespace(v reflect.Value) (string, bool) {
if f, ok := typ.FieldByNameFunc(func(s string) bool { return s == "Namespace" }); ok {
if f.Type.Kind() == reflect.String {
namespace := v.Field(f.Index[0]).String()
debugf("set namespace [%s] from field [%s.%s]", func() dargs {
return dargs{namespace, typ.Name(), f.Name}
debugf("Set namespace [\"%s\"] from field [%s.%s]", func() dargs {
return dargs{namespace, nameOf(typ), f.Name}
})
return namespace, true
}
Expand Down Expand Up @@ -204,15 +205,27 @@ func makeEventFromMethod(v reflect.Value, method reflect.Method, eventMatcher Ev
// the NSConn exists on the "controller" itself which is set dynamically.
cb = func(c *NSConn, msg Message) error {
// load an existing instance which contains the same "c".
cachePtr := c.Value.Load().(reflect.Value)
return cachePtr.Method(method.Index).Interface().(func(Message) error)(msg)
return c.value.Method(method.Index).Interface().(func(Message) error)(msg)
}
}

return
}

func makeEventsFromStruct(v reflect.Value, eventMatcher EventMatcherFunc) Events {
// StructInjector is a type which injects a dynamic struct value.
// See `Struct.SetInjector` for more.
type StructInjector func(structType reflect.Type, nsConn *NSConn) (structValue reflect.Value)

func nameOf(structType reflect.Type) string {
structType = indirectType(structType)

typName := structType.Name()
pkgPath := structType.PkgPath()
fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName
return fullname
}

func makeEventsFromStruct(v reflect.Value, eventMatcher EventMatcherFunc, injector StructInjector) Events {
events := make(Events, 0)

typ := v.Type()
Expand All @@ -233,60 +246,59 @@ func makeEventsFromStruct(v reflect.Value, eventMatcher EventMatcherFunc) Events
continue
}

debugf("event [%s] is handled by [%s.%s]", func() dargs {
return dargs{eventName, indirectType(typ).Name(), method.Name}
debugf("Event [\"%s\"] is handled by [%s.%s] method", func() dargs {
return dargs{eventName, nameOf(typ), method.Name}
})

events[eventName] = cb
}

if nsConnFieldIndex != -1 {
// if it's a dynamic.
typ = indirectType(typ)

cb, hasNamespaceConnect := events[OnNamespaceConnect]
staticFields := getNonZeroFields(v)

// debugf("field [%s.%s] will be automatically re-filled with [%T(%s)]", func(fidx int, f reflect.Value) dargs {
// fval := f.Interface()
// return dargs{typ.Name(), typ.Field(fidx).Name, fval, fval}
// }).forEach(staticFields)

debugEach(staticFields, func(idx int, f reflect.Value) {
fval := f.Interface()
fname := typ.Field(idx).Name
if fname == "Namespace" {
return // let's no log this as user field because
// it's optionally used to provide a namespace on NewStruct.getNamespaces().
}
var staticFields map[int]reflect.Value

debugf("field [%s.%s] marked as static on value [%v]", typ.Name(), fname, fval)
})
if injector == nil {
// maybe this should be added no matter what, I have to check
// some things in our company's production server first.
staticFields = getNonZeroFields(v)

debugEach(staticFields, func(idx int, f reflect.Value) {
fval := f.Interface()
fname := typ.Field(idx).Name
if fname == "Namespace" {
// let's no log this as user field because
// it's optionally used to provide a namespace on NewStruct.GetNamespaces().
return
}

debugf("Field [%s.%s] marked as static on value [%v]", nameOf(typ), fname, fval)
})

injector = func(typ reflect.Type, nsConn *NSConn) reflect.Value {
return reflect.New(typ)
}
}

// if debugEnabled() {
// for fidx, f := range staticFields {
// fval := f.Interface()
// debugf(, typ.Name(), typ.Field(fidx).Name, fval, fval)
// }
// }
cb, hasNamespaceConnect := events[OnNamespaceConnect]

events[OnNamespaceConnect] = func(c *NSConn, msg Message) error {
// create a new elem instance.
cachePtr := reflect.New(typ)
cachePtr := injector(typ, c)
cacheElem := cachePtr.Elem()

// set the NSConn dynamic field.
cacheElem.Field(nsConnFieldIndex).Set(reflect.ValueOf(c))

if staticFields != nil {
// set any static fields.
// set any static fields if default injector (see above).
for findex, fvalue := range staticFields {
cacheElem.Field(findex).Set(fvalue)
}
}

// Store it for the rest of the events inside
// this namespace of that specific connection.
c.Value.Store(cachePtr)
c.value = cachePtr

if hasNamespaceConnect {
return cb(c, msg)
Expand Down
2 changes: 1 addition & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func New(upgrader Upgrader, connHandler ConnHandler) *Server {

s := &Server{
upgrader: upgrader,
namespaces: connHandler.getNamespaces(),
namespaces: connHandler.GetNamespaces(),
readTimeout: readTimeout,
writeTimeout: writeTimeout,
connections: make(map[*Conn]struct{}),
Expand Down

0 comments on commit 26005b6

Please sign in to comment.