jsonrpc2

package module
v12.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 13, 2019 License: MIT Imports: 9 Imported by: 0

README

jsonrpc2/v12

GoDoc Go Report Card Coverage Status Build Status

Package jsonrpc2 is a complete and strictly conforming implementation of the JSON-RPC 2.0 protocol for both clients and servers.

It strives to conform to the official specification: https://www.jsonrpc.org

Getting started

Please read the official godoc documentation for the most up to date documentation.

Client

Clients use the provided types, optionally along with their own custom data types for making Requests and parsing Responses. The Request and Response types are defined so that they can accept any valid types for "id", "params", and "result".

Clients can use the Request, Response, and Error types with the json and http packages to make HTTP JSON-RPC 2.0 calls and parse their responses.

reqBytes, _ := json.Marshal(jsonrpc2.NewRequest("subtract", 0, []int{5, 1}))
httpResp, _ := http.Post("www.example.com", "application/json",
        bytes.NewReader(reqBytes))
respBytes, _ := ioutil.ReadAll(httpResp.Body)
response := jsonrpc2.Response{Result: &MyCustomResultType{}}
json.Unmarshal(respBytes, &response)
Server

Servers define their own MethodFuncs and associate them with a method name in a MethodMap. Passing the MethodMap to HTTPRequestHandler() will return a corresponding http.Handler which can be used with an http.Server. The http.Handler handles both batch and single requests, catches all protocol errors, and recovers from any panics or invalid return values from the user provided MethodFunc. MethodFuncs only need to catch errors related to their function such as InvalidParams or any user defined errors for the RPC method.

func versionMethod(params json.RawMessage) interface{} {
	if params != nil {
		return jsonrpc2.NewInvalidParamsError("no params accepted")
	}
	return "0.0.0"
}
var methods = jsonrpc2.MethodMap{"version": versionMethod}
func StartServer() {
        http.ListenAndServe(":8080", jsonrpc2.HTTPRequestHandler(methods))
}

Documentation

Overview

Package jsonrpc2 is a complete and strictly conforming implementation of the JSON-RPC 2.0 protocol for both clients and servers.

https://www.jsonrpc.org.

Client

Clients use the provided types, optionally along with their own custom data types for making Requests and parsing Responses. The Request and Response types are defined so that they can accept any valid types for "id", "params", and "result".

Clients can use the Request, Response, and Error types with the json and http packages to make HTTP JSON-RPC 2.0 calls and parse their responses.

reqBytes, _ := json.Marshal(jsonrpc2.NewRequest("subtract", 0, []int{5, 1}))
httpResp, _ := http.Post("www.example.com", "application/json",
        bytes.NewReader(reqBytes))
respBytes, _ := ioutil.ReadAll(httpResp.Body)
response := jsonrpc2.Response{Result: &MyCustomResultType{}}
json.Unmarshal(respBytes, &response)

Server

Servers define their own MethodFuncs and associate them with a method name in a MethodMap. Passing the MethodMap to HTTPRequestHandler() will return a corresponding http.Handler which can be used with an http.Server. The http.Handler handles both batch and single requests, catches all protocol errors, and recovers from any panics or invalid return values from the user provided MethodFunc. MethodFuncs only need to catch errors related to their function such as Invalid Params or any user defined errors for the RPC method.

func versionMethod(params json.RawMessage) interface{} {
	if params != nil {
		return jsonrpc2.InvalidParams("no params accepted")
	}
	return "0.0.0"
}
var methods = jsonrpc2.MethodMap{"version": versionMethod}
func StartServer() {
        http.ListenAndServe(":8080", jsonrpc2.HTTPRequestHandler(methods))
}
Example

This example makes all of the calls from the examples in the JSON-RPC 2.0 specification and prints them in a similar format.

package main

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

	jrpc "github.com/AdamSLevy/jsonrpc2/v12"
)

var endpoint = "http://localhost:18888"

// Functions for making requests and printing the Requests and Responses.
func post(b []byte) []byte {
	httpResp, _ := http.Post(endpoint, "", bytes.NewReader(b))
	respBytes, _ := ioutil.ReadAll(httpResp.Body)
	return respBytes
}
func postNewRequest(method string, id, params interface{}) {
	postRequest(jrpc.NewRequest(method, id, params))
}
func postRequest(request interface{}) {
	fmt.Println(request)
	reqBytes, _ := json.Marshal(request)
	respBytes := post(reqBytes)
	parseResponse(respBytes)
}
func parseResponse(respBytes []byte) {
	var response interface{}
	if len(respBytes) == 0 {
		return
	} else if string(respBytes[0]) == "[" {
		response = &jrpc.BatchResponse{}
	} else {
		response = &jrpc.Response{}
	}
	json.Unmarshal(respBytes, response)
	fmt.Println(response)
	fmt.Println()
}
func postBytes(req string) {
	fmt.Println("-->", req)
	respBytes := post([]byte(req))
	parseResponse(respBytes)
}

// The RPC methods called in the JSON-RPC 2.0 specification examples.
func subtract(params json.RawMessage) interface{} {
	// Parse either a params array of numbers or named numbers params.
	var a []float64
	if err := json.Unmarshal(params, &a); err == nil {
		if len(a) != 2 {
			return jrpc.InvalidParams("Invalid number of array params")
		}
		return a[0] - a[1]
	}
	var p struct {
		Subtrahend *float64
		Minuend    *float64
	}
	if err := json.Unmarshal(params, &p); err != nil ||
		p.Subtrahend == nil || p.Minuend == nil {
		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.InvalidParams(err)
	}
	sum := float64(0)
	for _, x := range p {
		sum += x
	}
	return sum
}
func notifyHello(_ json.RawMessage) interface{} {
	return ""
}
func getData(_ json.RawMessage) interface{} {
	return []interface{}{"hello", 5}
}

// This example makes all of the calls from the examples in the JSON-RPC 2.0
// specification and prints them in a similar format.
func main() {
	// Start the server.
	go func() {
		// Register RPC methods.
		methods := jrpc.MethodMap{
			"subtract":     subtract,
			"sum":          sum,
			"notify_hello": notifyHello,
			"get_data":     getData,
		}
		handler := jrpc.HTTPRequestHandler(methods)
		jrpc.DebugMethodFunc = true
		http.ListenAndServe(":18888", handler)
	}()

	// Make requests.
	fmt.Println("Syntax:")
	fmt.Println("--> data sent to Server")
	fmt.Println("<-- data sent to Client")
	fmt.Println("")

	fmt.Println("rpc call with positional parameters:")
	postNewRequest("subtract", 1, []int{42, 23})
	postNewRequest("subtract", 2, []int{23, 42})

	fmt.Println("rpc call with named parameters:")
	postNewRequest("subtract", 3, map[string]int{"subtrahend": 23, "minuend": 42})
	postNewRequest("subtract", 4, map[string]int{"minuend": 42, "subtrahend": 23})

	fmt.Println("a Notification:")
	postNewRequest("update", nil, []int{1, 2, 3, 4, 5})
	postNewRequest("foobar", nil, nil)
	fmt.Println()

	fmt.Println("rpc call of non-existent method:")
	postNewRequest("foobar", "1", nil)

	fmt.Println("rpc call with invalid JSON:")
	postBytes(`{"jsonrpc":"2.0","method":"foobar,"params":"bar","baz]`)

	fmt.Println("rpc call with invalid Request object:")
	postBytes(`{"jsonrpc":"2.0","method":1,"params":"bar"}`)

	fmt.Println("rpc call Batch, invalid JSON:")
	postBytes(
		`[
  {"jsonrpc":"2.0","method":"sum","params":[1,2,4],"id":"1"},
  {"jsonrpc":"2.0","method"
]`)

	fmt.Println("rpc call with an empty Array:")
	postBytes(`[]`)

	fmt.Println("rpc call with an invalid Batch (but not empty):")
	postBytes(`[1]`)

	fmt.Println("rpc call with invalid Batch:")
	postBytes(`[1,2,3]`)

	fmt.Println("rpc call Batch:")
	postBytes(`[
  {"jsonrpc":"2.0","method":"sum","params":[1,2,4],"id":"1"},
  {"jsonrpc":"2.0","method":"notify_hello","params":[7]},
  {"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":"2"},
  {"foo":"boo"},
  {"jsonrpc":"2.0","method":"foo.get","params":{"name":"myself"},"id":"5"},
  {"jsonrpc":"2.0","method":"get_data","id":"9"}
]`)
	fmt.Println("rpc call Batch (all notifications):")
	postRequest(jrpc.BatchRequest{
		jrpc.NewRequest("notify_sum", nil, []int{1, 2, 4}),
		jrpc.NewRequest("notify_hello", nil, []int{7}),
	})
	fmt.Println("<-- //Nothing is returned for all notification batches")

}
Output:

Syntax:
--> data sent to Server
<-- data sent to Client

rpc call with positional parameters:
--> {"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":1}
<-- {"jsonrpc":"2.0","result":19,"id":1}

--> {"jsonrpc":"2.0","method":"subtract","params":[23,42],"id":2}
<-- {"jsonrpc":"2.0","result":-19,"id":2}

rpc call with named parameters:
--> {"jsonrpc":"2.0","method":"subtract","params":{"minuend":42,"subtrahend":23},"id":3}
<-- {"jsonrpc":"2.0","result":19,"id":3}

--> {"jsonrpc":"2.0","method":"subtract","params":{"minuend":42,"subtrahend":23},"id":4}
<-- {"jsonrpc":"2.0","result":19,"id":4}

a Notification:
--> {"jsonrpc":"2.0","method":"update","params":[1,2,3,4,5]}
--> {"jsonrpc":"2.0","method":"foobar"}

rpc call of non-existent method:
--> {"jsonrpc":"2.0","method":"foobar","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","data":"json: cannot unmarshal number into Go struct field Request.method of type string"},"id":null}

rpc call Batch, invalid JSON:
--> [
  {"jsonrpc":"2.0","method":"sum","params":[1,2,4],"id":"1"},
  {"jsonrpc":"2.0","method"
]
<-- {"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"},"id":null}

rpc call with an empty Array:
--> []
<-- {"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","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","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:
--> [
  {"jsonrpc":"2.0","method":"sum","params":[1,2,4],"id":"1"},
  {"jsonrpc":"2.0","method":"notify_hello","params":[7]},
  {"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":"2"},
  {"foo":"boo"},
  {"jsonrpc":"2.0","method":"foo.get","params":{"name":"myself"},"id":"5"},
  {"jsonrpc":"2.0","method":"get_data","id":"9"}
]
<-- [
  {"jsonrpc":"2.0","result":7,"id":"1"},
  {"jsonrpc":"2.0","result":19,"id":"2"},
  {"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"}
]

rpc call Batch (all notifications):
--> [
  {"jsonrpc":"2.0","method":"notify_sum","params":[1,2,4]},
  {"jsonrpc":"2.0","method":"notify_hello","params":[7]}
]
<-- //Nothing is returned for all notification batches

Index

Examples

Constants

View Source
const (
	// 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
)

Official JSON-RPC 2.0 Spec Error Codes and Messages

View Source
const Version = "2.0"

Version is the valid version string for the "jsonrpc" field required in all JSON RPC 2.0 objects.

Variables

View Source
var DebugMethodFunc = false

DebugMethodFunc controls whether additional debug information will be printed to stdout in the event of an InternalError when a MethodFunc is called. This can be helpful when troubleshooting MethodFuncs.

Functions

func HTTPRequestHandler

func HTTPRequestHandler(methods MethodMap) http.HandlerFunc

HTTPRequestHandler returns an http.HandlerFunc for the given methods.

Types

type BatchRequest

type BatchRequest []Request

BatchRequest is a type that implements fmt.Stringer for a slice of Requests.

func (BatchRequest) String

func (br BatchRequest) String() string

String returns a string of the JSON array with "--> " prefixed to represent a BatchRequest object.

type BatchResponse

type BatchResponse []Response

BatchResponse is a type that implements fmt.Stringer for a slice of Responses.

func (BatchResponse) String

func (br BatchResponse) String() string

String returns a string of the JSON array with "<-- " prefixed to represent a BatchResponse object.

type Client

type Client struct {
	RequestDoer
	DebugRequest bool
	Log          Logger

	BasicAuth bool
	User      string
	Password  string
	Header    http.Header
}

Client embeds http.Client and provides a convenient way to make JSON-RPC requests.

func NewClient

func NewClient(doer RequestDoer) *Client

NewClient returns a newly initialized Client. If doer is nil, then &http.Client{} is used.

func (*Client) Request

func (c *Client) Request(url, method string, params, result interface{}) error

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.

Requests will have the "Content-Type":"application/json" header added.

Any populated c.Header will then be added to the http.Request, so you may override the "Content-Type" header with your own.

If c.BasicAuth is true then http.Request.SetBasicAuth(c.User, c.Password) will be called. This will override the same header in c.Header.

If c.DebugRequest is true then the Request and Response will be printed to stdout.

type Error

type Error struct {
	Code    ErrorCode   `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

Error represents a JSON-RPC 2.0 Error object, which is used in the Response object.

func InvalidParams

func InvalidParams(data interface{}) *Error

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 NewError

func NewError(code ErrorCode, message string, data interface{}) *Error

NewError returns an Error with the given code, message, and data.

func (Error) Error

func (e Error) Error() string

Error implements the error interface.

type ErrorCode

type ErrorCode int

ErrorCode represent the int JSON RPC 2.0 error code.

func (ErrorCode) IsReserved

func (c ErrorCode) IsReserved() bool

IsReserved returns true if c is within the reserved error code range: [LowestReservedErrorCode, HighestReservedErrorCode].

type Logger

type Logger interface {
	Println(...interface{})
}

Logger allows custom log types to be used with the Client when Client.DebugRequest is true.

type MethodFunc

type MethodFunc func(params json.RawMessage) interface{}

MethodFunc is the function signature used for RPC methods. The raw JSON of the params of a valid Request is passed to the MethodFunc for further application specific unmarshaling and validation. When a MethodFunc is invoked by the handler, the params json.RawMessage, if not nil, is guaranteed to be valid JSON representing a structured JSON type. If the "params" field was omitted or was null, then nil is passed to the MethodFunc.

A valid MethodFunc must return a not-nil interface{} that will not cause an error when passed to json.Marshal. If the underlying type of the returned interface{} is Error or *Error, then an Error Response will be returned to the client. If the underlying type of the returned interface{} is any other generic error, than an Internal Error will be returned to the client. Any return value that is not an error will be used as the "result" field.

If the MethodFunc returns an Error or *Error, then the Error must either use the InvalidParamsCode, or it must use an Error.Code that is outside of the reserved error code range. See ErrorCode.IsReserved() for more information.

An invalid MethodFunc will result in an Internal Error to be returned to the client without revealing any information about the error. If you are getting InternalErrors from your MethodFunc, set DebugMethodFunc to true for additional debug output about the cause of the Internal Error.

Example (Panic)

Any panic will return InternalError to the user if the call was a request and not a Notification.

var _ jsonrpc2.MethodFunc = func(params json.RawMessage) interface{} {
	panic("don't worry, jsonrpc2 will recover you and return an internal error")
}
Output:

type MethodMap

type MethodMap map[string]MethodFunc

MethodMap associates method names with MethodFuncs and is passed to HTTPRequestHandler() to generate a corresponding http.HandlerFunc.

type Request

type Request struct {
	JSONRPC string      `json:"jsonrpc"`
	Method  string      `json:"method"`
	Params  interface{} `json:"params,omitempty"`
	ID      interface{} `json:"id,omitempty"`
}

Request represents a JSON-RPC 2.0 Request or Notification object.

Valid Requests must use a numeric or string type for the ID, and a structured type such as a slice, array, map, or struct for the Params.

Example

Use the http and json packages to send a Request object.

reqBytes, _ := json.Marshal(jsonrpc2.NewRequest("subtract", 0, []int{5, 1}))
httpResp, _ := http.Post("http://localhost:8888", "application/json", bytes.NewReader(reqBytes))
respBytes, _ := ioutil.ReadAll(httpResp.Body)
response := jsonrpc2.Response{}
json.Unmarshal(respBytes, &response)
Output:

func NewRequest

func NewRequest(method string, id, params interface{}) Request

NewRequest returns a new Request with the given method, id, and params. If nil id is provided, it is by definition a Notification object and will not receive a response.

func (Request) MarshalJSON

func (r Request) MarshalJSON() ([]byte, error)

MarshalJSON outputs a JSON RPC Request object with the "jsonrpc" field populated to Version ("2.0").

func (Request) String

func (r Request) String() string

String returns a JSON string with "--> " prefixed to represent a Request object.

type RequestDoer

type RequestDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

RequestDoer is implemented by *http.Client and many other Client types are easily adapted to match this interface. This allows a custom HTTP Client type to be used with Client.

type Response

type Response struct {
	JSONRPC string      `json:"jsonrpc"`
	Result  interface{} `json:"result,omitempty"`
	Error   *Error      `json:"error,omitempty"`
	ID      interface{} `json:"id"`
}

Response represents a JSON-RPC 2.0 Response object.

func (Response) IsValid

func (r Response) IsValid() bool

IsValid returns true if JSONRPC is equal to the Version ("2.0") and one of Response.Result or Response.Error is not nil, but not both.

func (Response) MarshalJSON

func (r Response) MarshalJSON() ([]byte, error)

MarshalJSON outputs a JSON RPC Response object with the "jsonrpc" field populated to Version ("2.0").

func (Response) String

func (r Response) String() string

String returns a string of the JSON with "<-- " prefixed to represent a Response object.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL