Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dyncall] added dyncall module #93

Merged
merged 1 commit into from
Aug 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions dyncall/call.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dyncall

type CallModule interface {
// the call id this module relates to
// e.g. "Messages:Fetch"
CallID() string
// validate the given parameters - might return an error when received invalid parameters
Validate(map[string]interface{}) error
// will handle the call and return the result
Handle(map[string]interface{}) (map[string]interface{}, error)
}
86 changes: 86 additions & 0 deletions dyncall/dyncall_registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package dyncall

import "fmt"

type getCallModule struct {
respChan chan CallModule
id string
}

type Registry struct {
addModuleChan chan CallModule
getModuleChan chan getCallModule
closeChan chan struct{}
}

func New() *Registry {

r := &Registry{
addModuleChan: make(chan CallModule),
getModuleChan: make(chan getCallModule),
closeChan: make(chan struct{}),
}

// state
go func() {
modules := map[string]CallModule{}

for {
select {
case <-r.closeChan:
return
case m := <-r.addModuleChan:
modules[m.CallID()] = m
case getCallMod := <-r.getModuleChan:
mod := modules[getCallMod.id]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably check if map key exists first to avoid any potential panics

The current implementation might still work correctly in practice though, even without any changes

In the case of the key not existing in the modules map, nil will be sent to getCallMod.respChan so as long as it's properly handled, it shouldn't cause major issues

A proposed alternative that feels more safe in terms of potential panics

if mod, exists := modules[getCallMod.id]; exists {
    getCallMod.respChan <- mod
} else {
    getCallMod.respChan <- nil
}

Even though it might not make much of a difference in this use case, if CallModule was a struct with 2 fields, of type int and string, you wouldn't get a nil if the key is missing in the map with the unsafe usage, and you would get {0,"") as this would be corresponding to the default values of int and string type.

It's just that this time the default value of CallModule is nil because its an interface

getCallMod.respChan <- mod
}
}

}()

return r

}

func (r *Registry) Register(m CallModule) error {

// exist if already registered
respChan := make(chan CallModule)
r.getModuleChan <- getCallModule{
id: m.CallID(),
respChan: respChan,
}
if nil != <-respChan {
return fmt.Errorf("a call modules with id %s has already been registered", m.CallID())
}

// add module
r.addModuleChan <- m

return nil

}

func (r *Registry) Call(callID string, payload map[string]interface{}) (map[string]interface{}, error) {

// get call module
respChan := make(chan CallModule)
r.getModuleChan <- getCallModule{
id: callID,
respChan: respChan,
}
callModule := <-respChan
if nil == callModule {
return map[string]interface{}{}, fmt.Errorf("a call module with call id: %s does not exist", callID)
}

// validate the payload
if err := callModule.Validate(payload); err != nil {
return map[string]interface{}{}, err
}

// handle the payload
return callModule.Handle(payload)

}
84 changes: 84 additions & 0 deletions dyncall/dyncall_registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package dyncall

import (
"github.com/kataras/iris/core/errors"
"github.com/stretchr/testify/require"
"testing"
)

type testCallModule struct {
callID string
validate func(map[string]interface{}) error
handle func(map[string]interface{}) (map[string]interface{}, error)
}

func (m *testCallModule) CallID() string {
return m.callID
}

func (m *testCallModule) Validate(payload map[string]interface{}) error {
return m.validate(payload)
}

func (m *testCallModule) Handle(payload map[string]interface{}) (map[string]interface{}, error) {
return m.handle(payload)
}

func TestRegistry_Register(t *testing.T) {

callModule := testCallModule{
callID: "MY:MODULE",
}

reg := New()

// should register without an error
require.Nil(t, reg.Register(&callModule))

// should fail since the same module can't be registered twice
require.EqualError(t, reg.Register(&callModule), "a call modules with id MY:MODULE has already been registered")

}

// make sure call gets validated
func TestRegistry_CallValidate(t *testing.T) {

callModule := testCallModule{
callID: "MY:MODULE",
validate: func(payload map[string]interface{}) error {
return errors.New("invalid payload")
},
}

reg := New()
require.Nil(t, reg.Register(&callModule))

// call should be validated
_, err := reg.Call("MY:MODULE", map[string]interface{}{})
require.EqualError(t, err, "invalid payload")

}

func TestRegistry_Call(t *testing.T) {

callModule := testCallModule{
callID: "MY:MODULE",
validate: func(payload map[string]interface{}) error {
return nil
},
handle: func(payload map[string]interface{}) (map[string]interface{}, error) {
return map[string]interface{}{
"key": "value",
}, nil
},
}

reg := New()
require.Nil(t, reg.Register(&callModule))

// call should be validated
result, err := reg.Call("MY:MODULE", map[string]interface{}{})
require.Nil(t, err)
require.Equal(t, map[string]interface{}{"key": "value"}, result)

}