Skip to content

Commit

Permalink
feat(client): Add Client.Request helper function
Browse files Browse the repository at this point in the history
Some refactors in the handler result in cleaner code. The handler now
returns extra data for its Error Responses. Improved comments.

BREAKING CHANGE: Replace NewInvalidParamsError() with InvalidParams(),
un-export constructed Errors that a user does not ever need.
  • Loading branch information
AdamSLevy committed Mar 21, 2019
1 parent 78b52d9 commit 6c6728c
Show file tree
Hide file tree
Showing 14 changed files with 357 additions and 163 deletions.
84 changes: 84 additions & 0 deletions client.go
@@ -0,0 +1,84 @@
package jsonrpc2

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
)

// Client embeds http.Client and provides a convenient way to make JSON-RPC
// requests.
type Client struct {
http.Client
DebugRequest bool
}

// Request uses c to make a JSON-RPC 2.0 Request to url with the given method
// and params, and then parses the Response using the provided result for
// Response.Result. Thus, result must be a pointer in order for json.Unmarshal
// to populate it. If Request returns nil, then the request and RPC method call
// were successful and result will be populated, if applicable. If the request
// is successful but the RPC method returns an Error Response, then Request
// will return the Error, which can be checked for by attempting a type
// assertion on the returned error.
//
// Request uses a pseudorandom uint32 for the Request.ID.
//
// If c.DebugRequest is true then the Request and Response will be printed to
// stdout.
func (c *Client) Request(url, method string, params, result interface{}) error {
// Generate a random ID for this request.
reqID := rand.Uint32()%200 + 500

// Marshal the JSON RPC Request.
reqJrpc := NewRequest(method, reqID, params)
if c.DebugRequest {
fmt.Println(reqJrpc)
}
reqBytes, err := reqJrpc.MarshalJSON()
if err != nil {
return err
}

// Make the HTTP request.
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBytes))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
res, err := c.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusBadRequest {
return fmt.Errorf("http: %v", res.Status)
}

// Read the HTTP response.
resBytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("ioutil.ReadAll(http.Response.Body): %v", err)
}

// Unmarshal the HTTP response into a JSON RPC response.
var resID uint32
resJrpc := Response{Result: result, ID: &resID}
if err := json.Unmarshal(resBytes, &resJrpc); err != nil {
return fmt.Errorf("json.Unmarshal(%v): %v", string(resBytes), err)
}
if c.DebugRequest {
fmt.Println(resJrpc)
fmt.Println("")
}
if resJrpc.Error != nil {
return *resJrpc.Error
}
if resID != reqID {
return fmt.Errorf("request/response ID mismatch")
}
return nil
}
69 changes: 37 additions & 32 deletions error.go
Expand Up @@ -14,44 +14,49 @@ type Error struct {
Data interface{} `json:"data,omitempty"`
}

// Official JSON-RPC 2.0 Errors
var (
// ParseError is returned to the client if a JSON is not well formed.
ParseError = NewError(ParseErrorCode, ParseErrorMessage, nil)
// InvalidRequest is returned to the client if a request does not
// conform to JSON-RPC 2.0 spec
InvalidRequest = NewError(InvalidRequestCode, InvalidRequestMessage, nil)
// MethodNotFound is returned to the client if a method is called that
// has not been registered with RegisterMethod()
MethodNotFound = NewError(MethodNotFoundCode, MethodNotFoundMessage, nil)
// InvalidParams is returned to the client if a method is called with
// an invalid "params" object. A method's function is responsible for
// detecting and returning this error.
InvalidParams = NewError(InvalidParamsCode, InvalidParamsMessage, nil)
// InternalError is returned to the client if a method function returns
// an invalid response object.
InternalError = NewError(InternalErrorCode, InternalErrorMessage, nil)
)

// NewError returns an Error with the given code, message, and data.
func NewError(code ErrorCode, message string, data interface{}) Error {
return Error{Code: code, Message: message, Data: data}
}

// NewInvalidParamsError returns an InvalidParams Error with the given data.
func NewInvalidParamsError(data interface{}) Error {
err := InvalidParams
err.Data = data
return err
func NewError(code ErrorCode, message string, data interface{}) *Error {
return &Error{Code: code, Message: message, Data: data}
}

// Error implements the error interface.
func (e Error) Error() string {
s := fmt.Sprintf("jsonrpc2.Error{Code:%v, Message:%#v", e.Code, e.Message)
if e.Data != nil {
s += fmt.Sprintf(", Data:%#v}", e.Data)
} else {
s += "}"
s += fmt.Sprintf(", Data:%#v", e.Data)
}
return s
return s + "}"
}

// Official JSON-RPC 2.0 Errors

// InvalidParams returns a pointer to a new Error initialized with the
// InvalidParamsCode and InvalidParamsMessage and the user provided data.
// MethodFuncs are responsible for detecting and returning this error.
func InvalidParams(data interface{}) *Error {
return NewError(InvalidParamsCode, InvalidParamsMessage, data)
}

// internalError returns a pointer to a new Error initialized with the
// InternalErrorCode and InternalErrorMessage and the user provided data.
func internalError(data interface{}) *Error {
return NewError(InternalErrorCode, InternalErrorMessage, data)
}

// parseError returns a pointer to a new Error initialized with the
// ParseErrorCode and ParseErrorMessage and the user provided data.
func parseError(data interface{}) *Error {
return NewError(ParseErrorCode, ParseErrorMessage, data)
}

// invalidRequest returns a pointer to a new Error initialized with the
// InvalidRequestCode and InvalidRequestMessage and the user provided data.
func invalidRequest(data interface{}) *Error {
return NewError(InvalidRequestCode, InvalidRequestMessage, data)
}

// methodNotFound returns a pointer to a new Error initialized with the
// MethodNotFoundCode and MethodNotFoundMessage and the user provided data.
func methodNotFound(data interface{}) *Error {
return NewError(MethodNotFoundCode, MethodNotFoundMessage, data)
}
4 changes: 2 additions & 2 deletions error_test.go
Expand Up @@ -10,9 +10,9 @@ func TestErrorCodeIsReserved(t *testing.T) {
assert := assert.New(t)
var c ErrorCode
assert.False(c.IsReserved())
c = LowestReservedErrorCode
c = MinReservedErrorCode
assert.True(c.IsReserved())
c = HighestReservedErrorCode
c = MaxReservedErrorCode
assert.True(c.IsReserved())
}

Expand Down
53 changes: 39 additions & 14 deletions errorcode.go
@@ -1,27 +1,52 @@
// Copyright 2018 Adam S Levy. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.

package jsonrpc2

// ErrorCode represent the int JSON RPC 2.0 error code.
type ErrorCode int

// Official JSON-RPC 2.0 Spec Error Codes and Messages
const (
LowestReservedErrorCode ErrorCode = -32768
ParseErrorCode ErrorCode = -32700
InvalidRequestCode ErrorCode = -32600
MethodNotFoundCode ErrorCode = -32601
InvalidParamsCode ErrorCode = -32602
InternalErrorCode ErrorCode = -32603
HighestReservedErrorCode ErrorCode = -32000

ParseErrorMessage = "Parse error"
InvalidRequestMessage = "Invalid Request"
MethodNotFoundMessage = "Method not found"
InvalidParamsMessage = "Invalid params"
InternalErrorMessage = "Internal error"
// MinReservedErrorCode is the minimum reserved error code. Method
// defined errors may be less than this value.
MinReservedErrorCode ErrorCode = -32768

// ParseErrorCode is returned to the client when invalid JSON was
// received by the server. An error occurred on the server while
// parsing the JSON text.
ParseErrorCode ErrorCode = -32700
ParseErrorMessage = "Parse error"

// InvalidRequestCode is returned to the client when the JSON sent is
// not a valid Request object.
InvalidRequestCode ErrorCode = -32600
InvalidRequestMessage = "Invalid Request"

// MethodNotFoundCode is returned to the client when the method does
// not exist / is not available.
MethodNotFoundCode ErrorCode = -32601
MethodNotFoundMessage = "Method not found"

// InvalidParamsCode is returned to the client if a method is called
// with invalid method parameter(s). MethodFuncs are responsible for
// detecting and returning this error.
InvalidParamsCode ErrorCode = -32602
InvalidParamsMessage = "Invalid params"

// InternalErrorCode is returned to the client if an internal error
// occurs such as a MethodFunc panic.
InternalErrorCode ErrorCode = -32603
InternalErrorMessage = "Internal error"

// MaxReservedErrorCode is the maximum reserved error code. Method
// defined errors may be greater than this value.
MaxReservedErrorCode ErrorCode = -32000
)

// IsReserved returns true if c is within the reserved error code range:
// [LowestReservedErrorCode, HighestReservedErrorCode].
func (c ErrorCode) IsReserved() bool {
return LowestReservedErrorCode <= c && c <= HighestReservedErrorCode
return MinReservedErrorCode <= c && c <= MaxReservedErrorCode
}
26 changes: 13 additions & 13 deletions example_test.go
Expand Up @@ -11,7 +11,7 @@ import (
"io/ioutil"
"net/http"

jrpc "github.com/AdamSLevy/jsonrpc2/v10"
jrpc "github.com/AdamSLevy/jsonrpc2/v11"
)

var endpoint = "http://localhost:18888"
Expand Down Expand Up @@ -56,7 +56,7 @@ func subtract(params json.RawMessage) interface{} {
var a []float64
if err := json.Unmarshal(params, &a); err == nil {
if len(a) != 2 {
return jrpc.NewInvalidParamsError("Invalid number of array params")
return jrpc.InvalidParams("Invalid number of array params")
}
return a[0] - a[1]
}
Expand All @@ -66,15 +66,15 @@ func subtract(params json.RawMessage) interface{} {
}
if err := json.Unmarshal(params, &p); err != nil ||
p.Subtrahend == nil || p.Minuend == nil {
return jrpc.NewInvalidParamsError(`Required fields "subtrahend" and ` +
return jrpc.InvalidParams(`Required fields "subtrahend" and ` +
`"minuend" must be valid numbers.`)
}
return *p.Minuend - *p.Subtrahend
}
func sum(params json.RawMessage) interface{} {
var p []float64
if err := json.Unmarshal(params, &p); err != nil {
return jrpc.NewInvalidParamsError(err)
return jrpc.InvalidParams(err)
}
sum := float64(0)
for _, x := range p {
Expand Down Expand Up @@ -191,15 +191,15 @@ func Example() {
//
// rpc call of non-existent method:
// --> {"jsonrpc":"2.0","method":"foobar","id":"1"}
// <-- {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":"1"}
// <-- {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found","data":{"method":"foobar"}},"id":"1"}
//
// rpc call with invalid JSON:
// --> {"jsonrpc":"2.0","method":"foobar,"params":"bar","baz]
// <-- {"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"},"id":null}
//
// rpc call with invalid Request object:
// --> {"jsonrpc":"2.0","method":1,"params":"bar"}
// <-- {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null}
// <-- {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request","data":"json: cannot unmarshal number into Go struct field Request.method of type string"},"id":null}
//
// rpc call Batch, invalid JSON:
// --> [
Expand All @@ -210,20 +210,20 @@ func Example() {
//
// rpc call with an empty Array:
// --> []
// <-- {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null}
// <-- {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request","data":"empty batch request"},"id":null}
//
// rpc call with an invalid Batch (but not empty):
// --> [1]
// <-- [
// {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null}
// {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request","data":"json: cannot unmarshal number into Go value of type jsonrpc2.Request"},"id":null}
// ]
//
// rpc call with invalid Batch:
// --> [1,2,3]
// <-- [
// {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null},
// {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null},
// {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null}
// {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request","data":"json: cannot unmarshal number into Go value of type jsonrpc2.Request"},"id":null},
// {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request","data":"json: cannot unmarshal number into Go value of type jsonrpc2.Request"},"id":null},
// {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request","data":"json: cannot unmarshal number into Go value of type jsonrpc2.Request"},"id":null}
// ]
//
// rpc call Batch:
Expand All @@ -238,8 +238,8 @@ func Example() {
// <-- [
// {"jsonrpc":"2.0","result":7,"id":"1"},
// {"jsonrpc":"2.0","result":19,"id":"2"},
// {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null},
// {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":"5"},
// {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request","data":"json: unknown field \"foo\""},"id":null},
// {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found","data":{"method":"foo.get"}},"id":"5"},
// {"jsonrpc":"2.0","result":["hello",5],"id":"9"}
// ]
//
Expand Down
2 changes: 1 addition & 1 deletion examplefuncs_test.go
Expand Up @@ -9,7 +9,7 @@ import (
"io/ioutil"
"net/http"

"github.com/AdamSLevy/jsonrpc2/v10"
"github.com/AdamSLevy/jsonrpc2/v11"
)

// Use the http and json packages to send a Request object.
Expand Down
10 changes: 4 additions & 6 deletions go.mod
@@ -1,7 +1,5 @@
module github.com/AdamSLevy/jsonrpc2/v10
module github.com/AdamSLevy/jsonrpc2/v11

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2
)
go 1.12

require github.com/stretchr/testify v1.3.0
9 changes: 5 additions & 4 deletions go.sum
@@ -1,6 +1,7 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

0 comments on commit 6c6728c

Please sign in to comment.