Skip to content

rpcserver: add batched request support (json 2.0) #841

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

Merged
merged 1 commit into from
Feb 13, 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
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