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

Open Finance Protocol #1

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
48 changes: 41 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# RANGO

Rango is a general purpose websocket server which dispatch public and private messages.
Rango is a general purpose websocket server which dispatches public and private messages.
It's using AMQP (RabbitMQ) as source of messages.

Rango is made as a drop-in replacement of ranger built in ruby.

## Build

```bash
Expand All @@ -29,16 +27,52 @@ wscat --connect localhost:8080/public
wscat --connect localhost:8080/private --header "Authorization: Bearer $(go run ./tools/jwt)"
```

## Messages
## RPC Methods

Every request and responses are formated in a array the following way:

```json
[0, 42, "method", ["any", "arguments", 51]]
```

- The first argument is 0 for requests and 1 for responses.

- The second argument is the request ID, the client can set the value he wants and the server will include it in the response. This helps to keep track of which response stands for which request. We use often 42 in our examples, you need to change this value by your own according to your implementation.

- Third is the method name

- Last is a list of arguments for the method

### Subscribe to public streams

Request:

### Subscribe to a stream list
```
[0,42,"subscribe",["public",["eurusd.trades","eurusd.ob-inc"]]]
[0,43,"subscribe",["private",["trades","orders"]]]
```

Response:

```
{"event":"subscribe","streams":["eurusd.trades","eurusd.ob-inc"]}
[1,42,"subscribed",["public",["eurusd.trades","eurusd.ob-inc"]]]
[1,43,"subscribed",["private",["trades","orders"]]]
```

### Unsubscribe to one or several streams

Request:

```
[0,42,"unsubscribe",["public",["eurusd.trades","eurusd.ob-inc"]]]
```

Response:

```
{"event":"unsubscribe","streams":["eurusd.trades"]}
[1,42,"unsubscribe",["public",["eurusd.trades","eurusd.ob-inc"]]]
```

## RPC Responses

### Authentication notification
26 changes: 0 additions & 26 deletions pkg/message/msg.go

This file was deleted.

48 changes: 0 additions & 48 deletions pkg/message/msg_test.go

This file was deleted.

51 changes: 0 additions & 51 deletions pkg/message/parser.go

This file was deleted.

66 changes: 66 additions & 0 deletions pkg/msg/msg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package msg

import (
"encoding/json"
)

const (
// Request type code
Request = 1

// Response type code
Response = 2

// Event type code
Event = 3
)

// Msg represent websocket messages, it could be either a request, a response or an event
type Msg struct {
Type uint8
ReqID uint64
Method string
Args []interface{}
}

// NewResponse build a response object
func NewResponse(req *Msg, method string, args []interface{}) *Msg {
return &Msg{
Type: Response,
ReqID: req.ReqID,
Method: method,
Args: args,
}
}

// Encode msg into json
func (m *Msg) Encode() []byte {
s, err := json.Marshal([]interface{}{
m.Type,
m.ReqID,
m.Method,
m.Args,
})
if err != nil {
return []byte{}
}
return s
}

// Convss2is converts a string slice to interface slice more details: https://golang.org/doc/faq#convert_slice_of_interface)
func Convss2is(a []string) []interface{} {
s := make([]interface{}, len(a))
for i, v := range a {
s[i] = v
}
return s
}

func Contains(haystack []interface{}, niddle interface{}) bool {
for _, el := range haystack {
if el == niddle {
return true
}
}
return false
}
18 changes: 18 additions & 0 deletions pkg/msg/msg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package msg

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestEncoding(t *testing.T) {
msg := Msg{
Type: Request,
ReqID: 42,
Method: "test",
Args: []interface{}{"hello", "there"},
}

assert.Equal(t, `[1,42,"test",["hello","there"]]`, string(msg.Encode()))
}
115 changes: 115 additions & 0 deletions pkg/msg/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package msg

import (
"encoding/json"
"errors"
"fmt"
"math"
"reflect"
)

func ParseUint64(t interface{}) (uint64, error) {
vf, ok := t.(float64)

if !ok {
return 0, errors.New("expected uint64 got: " + reflect.TypeOf(t).String())
}

vu := uint64(vf)
if float64(vu) != vf {
return 0, errors.New("expected unsigned integer got: float")
}
return vu, nil
}

func ParseUint8(t interface{}) (uint8, error) {
vf, ok := t.(float64)

if !ok {
return 0, errors.New("expected uint8 got: " + reflect.TypeOf(t).String())
}

if math.Trunc(vf) != vf {
return 0, errors.New("expected unsigned integer got: float")
}
return uint8(vf), nil
}

func ParseString(t interface{}) (string, error) {
s, ok := t.(string)

if !ok {
return "", errors.New("expected string got: " + reflect.TypeOf(t).String())
}
return s, nil
}

func ParseSlice(t interface{}) ([]interface{}, error) {
s, ok := t.([]interface{})

if !ok {
return nil, errors.New("expected array got: " + reflect.TypeOf(t).String())
}
return s, nil
}

func ParseSliceOfStrings(t interface{}) ([]string, error) {
s, err := ParseSlice(t)
if err != nil {
return nil, err
}

a := make([]string, len(s))
for i, istr := range s {
str, ok := istr.(string)
a[i] = str

if !ok {
return nil, errors.New("expected array of string, got unexpected " + reflect.TypeOf(istr).String())
}
}
return a, nil
}

func Parse(msg []byte) (*Msg, error) {
req := Msg{}

var v []interface{}
if err := json.Unmarshal(msg, &v); err != nil {
return nil, fmt.Errorf("Could not parse message: %w", err)
}

if len(v) != 4 {
return nil, errors.New("message must contains 4 elements")
}

t, err := ParseUint8(v[0])
if err != nil {
return nil, fmt.Errorf("failed to parse type: %w", err)
}
if t != Request && t != Response && t != Event {
return nil, errors.New("message type must be 1, 2 or 3")
}

reqID, err := ParseUint64(v[1])
if err != nil {
return nil, fmt.Errorf("failed to parse request ID: %w", err)
}

method, err := ParseString(v[2])
if err != nil {
return nil, fmt.Errorf("failed to parse method: %w", err)
}

args, err := ParseSlice(v[3])
if err != nil {
return nil, fmt.Errorf("failed to parse arguments: %w", err)
}

req.Type = t
req.ReqID = reqID
req.Method = method
req.Args = args

return &req, nil
}
Loading