Skip to content

Commit

Permalink
rpcserver: add batched request support (json 2.0)
Browse files Browse the repository at this point in the history
  • Loading branch information
dnldd committed Feb 1, 2018
1 parent 31f11b2 commit 406d873
Show file tree
Hide file tree
Showing 21 changed files with 779 additions and 269 deletions.
2 changes: 1 addition & 1 deletion cmd/dcrctl/dcrctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func main() {

// Marshal the command into a JSON-RPC byte slice in preparation for
// sending it to the RPC server.
marshalledJSON, err := dcrjson.MarshalCmd(1, cmd)
marshalledJSON, err := dcrjson.MarshalCmd("1.0", 1, cmd)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
Expand Down
4 changes: 2 additions & 2 deletions dcrjson/btcdextcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func TestBtcdExtCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := dcrjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := dcrjson.MarshalCmd("1.0", testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -166,7 +166,7 @@ func TestBtcdExtCmds(t *testing.T) {

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = dcrjson.MarshalCmd(testID, cmd)
marshalled, err = dcrjson.MarshalCmd("1.0", testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions dcrjson/btcwalletextcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func TestBtcWalletExtCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := dcrjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := dcrjson.MarshalCmd("1.0", testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -143,7 +143,7 @@ func TestBtcWalletExtCmds(t *testing.T) {

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = dcrjson.MarshalCmd(testID, cmd)
marshalled, err = dcrjson.MarshalCmd("1.0", testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions dcrjson/chainsvrcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,7 @@ func TestChainSvrCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := dcrjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := dcrjson.MarshalCmd("1.0", testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -1008,7 +1008,7 @@ func TestChainSvrCmds(t *testing.T) {

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = dcrjson.MarshalCmd(testID, cmd)
marshalled, err = dcrjson.MarshalCmd("1.0", testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions dcrjson/chainsvrwscmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func TestChainSvrWsCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := dcrjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := dcrjson.MarshalCmd("1.0", testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -187,7 +187,7 @@ func TestChainSvrWsCmds(t *testing.T) {

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = dcrjson.MarshalCmd(testID, cmd)
marshalled, err = dcrjson.MarshalCmd("1.0", testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions dcrjson/chainsvrwsntfns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func TestChainSvrWsNtfns(t *testing.T) {
for i, test := range tests {
// Marshal the notification as created by the new static
// creation function. The ID is nil for notifications.
marshalled, err := dcrjson.MarshalCmd(nil, test.staticNtfn())
marshalled, err := dcrjson.MarshalCmd("1.0", nil, test.staticNtfn())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -144,7 +144,7 @@ func TestChainSvrWsNtfns(t *testing.T) {
// Marshal the notification as created by the generic new
// notification creation function. The ID is nil for
// notifications.
marshalled, err = dcrjson.MarshalCmd(nil, cmd)
marshalled, err = dcrjson.MarshalCmd("1.0", nil, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions dcrjson/cmdparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func makeParams(rt reflect.Type, rv reflect.Value) []interface{} {
// is suitable for transmission to an RPC server. The provided command type
// must be a registered type. All commands provided by this package are
// registered by default.
func MarshalCmd(id interface{}, cmd interface{}) ([]byte, error) {
func MarshalCmd(rpcVersion string, id interface{}, cmd interface{}) ([]byte, error) {
// Look up the cmd type and error out if not registered.
rt := reflect.TypeOf(cmd)
registerLock.RLock()
Expand All @@ -60,7 +60,7 @@ func MarshalCmd(id interface{}, cmd interface{}) ([]byte, error) {
params := makeParams(rt.Elem(), rv.Elem())

// Generate and marshal the final JSON-RPC request.
rawCmd, err := NewRequest(id, method, params)
rawCmd, err := NewRequest(rpcVersion, id, method, params)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion dcrjson/cmdparse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ func TestMarshalCmdErrors(t *testing.T) {

t.Logf("Running %d tests", len(tests))
for i, test := range tests {
_, err := dcrjson.MarshalCmd(test.id, test.cmd)
_, err := dcrjson.MarshalCmd("1.0", test.id, test.cmd)
if reflect.TypeOf(err) != reflect.TypeOf(test.err) {
t.Errorf("Test #%d (%s) wrong error type - got `%T` (%v), want `%T`",
i, test.name, err, err, test.err)
Expand Down
4 changes: 2 additions & 2 deletions dcrjson/dcrdcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestDcrdCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := dcrjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := dcrjson.MarshalCmd("1.0", testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -99,7 +99,7 @@ func TestDcrdCmds(t *testing.T) {

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = dcrjson.MarshalCmd(testID, cmd)
marshalled, err = dcrjson.MarshalCmd("1.0", testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions dcrjson/dcrwalletextcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestDcrWalletExtCmds(t *testing.T) {
for i, test := range tests {
// Marshal the command as created by the new static command
// creation function.
marshalled, err := dcrjson.MarshalCmd(testID, test.staticCmd())
marshalled, err := dcrjson.MarshalCmd("1.0", testID, test.staticCmd())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -58,7 +58,7 @@ func TestDcrWalletExtCmds(t *testing.T) {

// Marshal the command as created by the generic new command
// creation function.
marshalled, err = dcrjson.MarshalCmd(testID, cmd)
marshalled, err = dcrjson.MarshalCmd("1.0", testID, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
4 changes: 2 additions & 2 deletions dcrjson/dcrwalletextwsntfns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func TestDcrwalletChainSvrWsNtfns(t *testing.T) {
for i, test := range tests {
// Marshal the notification as created by the new static
// creation function. The ID is nil for notifications.
marshalled, err := dcrjson.MarshalCmd(nil, test.staticNtfn())
marshalled, err := dcrjson.MarshalCmd("1.0", nil, test.staticNtfn())
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand All @@ -151,7 +151,7 @@ func TestDcrwalletChainSvrWsNtfns(t *testing.T) {
// Marshal the notification as created by the generic new
// notification creation function. The ID is nil for
// notifications.
marshalled, err = dcrjson.MarshalCmd(nil, cmd)
marshalled, err = dcrjson.MarshalCmd("1.0", nil, cmd)
if err != nil {
t.Errorf("MarshalCmd #%d (%s) unexpected error: %v", i,
test.name, err)
Expand Down
6 changes: 3 additions & 3 deletions dcrjson/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func ExampleMarshalCmd() {
// server. Typically the client would increment the id here which is
// request so the response can be identified.
id := 1
marshalledBytes, err := dcrjson.MarshalCmd(id, gbCmd)
marshalledBytes, err := dcrjson.MarshalCmd("1.0", id, gbCmd)
if err != nil {
fmt.Println(err)
return
Expand Down Expand Up @@ -98,7 +98,7 @@ func ExampleUnmarshalCmd() {
func ExampleMarshalResponse() {
// Marshal a new JSON-RPC response. For example, this is a response
// to a getblockheight request.
marshalledBytes, err := dcrjson.MarshalResponse(1, 350001, nil)
marshalledBytes, err := dcrjson.MarshalResponse("1.0", 1, 350001, nil)
if err != nil {
fmt.Println(err)
return
Expand All @@ -110,7 +110,7 @@ func ExampleMarshalResponse() {
fmt.Printf("%s\n", marshalledBytes)

// Output:
// {"result":350001,"error":null,"id":1}
// {"jsonrpc":"1.0","result":350001,"error":null,"id":1}
}

// This example demonstrates how to unmarshal a JSON-RPC response and then
Expand Down
128 changes: 94 additions & 34 deletions dcrjson/jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,78 @@ func IsValidIDType(id interface{}) bool {
}
}

// Request is a type for raw JSON-RPC 1.0 requests. The Method field identifies
// the specific command type which in turns leads to different parameters.
// Callers typically will not use this directly since this package provides a
// statically typed command infrastructure which handles creation of these
// requests, however this struct it being exported in case the caller wants to
// construct raw requests for some reason.
// Request represents raw JSON-RPC requests. The Method field identifies the
// specific command type which in turn leads to different parameters. Callers
// typically will not use this directly since this package provides a statically
// typed command infrastructure which handles creation of these requests,
// however this struct is being exported in case the caller wants to construct
// raw requests for some reason.
type Request struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
ID interface{} `json:"id"`
}

// NewRequest returns a new JSON-RPC 1.0 request object given the provided id,
// method, and parameters. The parameters are marshalled into a json.RawMessage
// for the Params field of the returned request object. This function is only
// provided in case the caller wants to construct raw requests for some reason.
//
// Typically callers will instead want to create a registered concrete command
// type with the NewCmd or New<Foo>Cmd functions and call the MarshalCmd
// function with that command to generate the marshalled JSON-RPC request.
func NewRequest(id interface{}, method string, params []interface{}) (*Request, error) {
// UnmarshalJSON is a custom unmarshal func for the Request struct. The param
// field defaults to an empty json.RawMessage array it is omitted by the request
// or nil if the supplied value is invalid.
func (request *Request) UnmarshalJSON(b []byte) error {
var data map[string]interface{}
err := json.Unmarshal(b, &data)
if err != nil {
return err
}

request.ID = data["id"]
methodValue, hasMethod := data["method"]
if hasMethod {
request.Method = methodValue.(string)
}
jsonrpcValue, hasJsonrpc := data["jsonrpc"]
if hasJsonrpc {
request.Jsonrpc = jsonrpcValue.(string)
}
paramsValue, hasParams := data["params"]
if !hasParams {
// set the request param to an empty array if it is ommited in the request
request.Params = []json.RawMessage{}
}
if hasParams {
// assert the request params is an array of data
params, paramsOk := paramsValue.([]interface{})
if paramsOk {
rawParams := make([]json.RawMessage, 0, len(params))
for _, param := range params {
marshalledParam, err := json.Marshal(param)
if err != nil {
return err
}
rawMessage := json.RawMessage(marshalledParam)
rawParams = append(rawParams, rawMessage)
}

request.Params = rawParams
}
}

return nil
}

// NewRequest returns a new JSON-RPC request object given the provided rpc
// version, id, method, and parameters. The parameters are marshalled into a
// json.RawMessage for the Params field of the returned request object. This
// function is only provided in case the caller wants to construct raw requests
// for some reason. Typically callers will instead want to create a registered
// concrete command type with the NewCmd or New<Foo>Cmd functions and call the
// MarshalCmd function with that command to generate the marshalled JSON-RPC
// request.
func NewRequest(rpcVersion string, id interface{}, method string, params []interface{}) (*Request, error) {
// default to JSON-RPC 1.0 if RPC type is not specified
if rpcVersion != "2.0" && rpcVersion != "1.0" {
rpcVersion = "1.0"
}

if !IsValidIDType(id) {
str := fmt.Sprintf("the id of type '%T' is invalid", id)
return nil, makeError(ErrInvalidType, str)
Expand All @@ -99,51 +149,61 @@ func NewRequest(id interface{}, method string, params []interface{}) (*Request,
}

return &Request{
Jsonrpc: "1.0",
Jsonrpc: rpcVersion,
ID: id,
Method: method,
Params: rawParams,
}, nil
}

// Response is the general form of a JSON-RPC response. The type of the Result
// field varies from one command to the next, so it is implemented as an
// interface. The ID field has to be a pointer for Go to put a null in it when
// Response is the general form of a JSON-RPC response. The type of the
// Result field varies from one command to the next, so it is implemented as an
// interface. The ID field has to be a pointer to allow for a nil value when
// empty.
type Response struct {
Result json.RawMessage `json:"result"`
Error *RPCError `json:"error"`
ID *interface{} `json:"id"`
Jsonrpc string `json:"jsonrpc"`
Result json.RawMessage `json:"result"`
Error *RPCError `json:"error"`
ID *interface{} `json:"id"`
}

// NewResponse returns a new JSON-RPC response object given the provided id,
// marshalled result, and RPC error. This function is only provided in case the
// caller wants to construct raw responses for some reason.
//
// NewResponse returns a new JSON-RPC response object given the provided rpc
// version, id, marshalled result, and RPC error. This function is only
// provided in case the caller wants to construct raw responses for some reason.
// Typically callers will instead want to create the fully marshalled JSON-RPC
// response to send over the wire with the MarshalResponse function.
func NewResponse(id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Response, error) {
func NewResponse(rpcVersion string, id interface{}, marshalledResult []byte, rpcErr *RPCError) (*Response, error) {
if rpcVersion != "2.0" && rpcVersion != "1.0" {
rpcVersion = "1.0"
}

if !IsValidIDType(id) {
str := fmt.Sprintf("the id of type '%T' is invalid", id)
return nil, makeError(ErrInvalidType, str)
}

pid := &id
return &Response{
Result: marshalledResult,
Error: rpcErr,
ID: pid,
Jsonrpc: rpcVersion,
Result: marshalledResult,
Error: rpcErr,
ID: pid,
}, nil
}

// MarshalResponse marshals the passed id, result, and RPCError to a JSON-RPC
// response byte slice that is suitable for transmission to a JSON-RPC client.
func MarshalResponse(id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) {
// MarshalResponse marshals the passed rpc version, id, result, and RPCError to
// a JSON-RPC response byte slice that is suitable for transmission to a
// JSON-RPC client.
func MarshalResponse(rpcVersion string, id interface{}, result interface{}, rpcErr *RPCError) ([]byte, error) {
if rpcVersion != "2.0" && rpcVersion != "1.0" {
rpcVersion = "1.0"
}

marshalledResult, err := json.Marshal(result)
if err != nil {
return nil, err
}
response, err := NewResponse(id, marshalledResult, rpcErr)
response, err := NewResponse(rpcVersion, id, marshalledResult, rpcErr)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 406d873

Please sign in to comment.