jsonrpc

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Dec 22, 2017 License: MIT Imports: 8 Imported by: 0

README

Go Report Card GoDoc GitHub license

JSON-RPC 2.0 Client for golang

A go implementation of an rpc client using json as data format over http. The implementation is based on the JSON-RPC 2.0 specification: http://www.jsonrpc.org/specification

Supports:

  • requests with arbitrary parameters
  • requests with named parameters
  • notifications
  • batch requests
  • convenient response retrieval
  • basic authentication
  • custom headers
  • custom http client

Installation

go get -u github.com/ybbus/jsonrpc

Getting started

Let's say we want to retrieve a person with a specific id using rpc-json over http. Then we want to save this person with new properties. We have to provide basic authentication credentials. (Error handling is omitted here)

type Person struct {
    Id   int `json:"id"`
    Name string `json:"name"`
    Age  int `json:"age"`
}

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    rpcClient.SetBasicAuth("alex", "secret")

    response, _ := rpcClient.Call("getPersonById", 123)

    person := Person{}
    response.getObject(&person)

    person.Age = 33
    rpcClient.Call("updatePerson", person)
}

In detail

Generating rpc-json requests

Let's start by executing a simple json-rpc http call: In production code: Always make sure to check err != nil first!

This calls generate and send a valid rpc-json object. (see: http://www.jsonrpc.org/specification#request_object)

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, _ := rpcClient.Call("getDate")
    // generates body: {"jsonrpc":"2.0","method":"getDate","id":0}
}

Call a function with parameter:

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, _ := rpcClient.Call("addNumbers", 1, 2)
    // generates body: {"jsonrpc":"2.0","method":"addNumbers","params":[1,2],"id":0}
}

Call a function with arbitrary parameters:

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, _ := rpcClient.Call("createPerson", "Alex", 33, "Germany")
    // generates body: {"jsonrpc":"2.0","method":"createPerson","params":["Alex",33,"Germany"],"id":0}
}

Call a function with named parameters:

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
		rpcClient.CallNamed("createPerson", map[string]interface{}{
		"name":      "Bartholomew Allen",
		"nicknames": []string{"Barry", "Flash",},
		"male":      true,
		"age":       28,
		"address":   map[string]interface{}{"street": "Main Street", "city": "Central City"},
	})
    // generates body: {"jsonrpc":"2.0","method":"createPerson","params":
	//	{"name": "Bartholomew Allen", "nicknames": ["Barry", "Flash"], "male": true, "age": 28,
	//	"address": {"street": "Main Street", "city": "Central City"}}
	//	,"id":0}
}

Call a function providing custom data structures as parameters:

type Person struct {
  Name    string `json:"name"`
  Age     int `json:"age"`
  Country string `json:"country"`
}
func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, _ := rpcClient.Call("createPerson", Person{"Alex", 33, "Germany"})
    // generates body: {"jsonrpc":"2.0","method":"createPerson","params":[{"name":"Alex","age":33,"country":"Germany"}],"id":0}
}

Complex example:

type Person struct {
  Name    string `json:"name"`
  Age     int `json:"age"`
  Country string `json:"country"`
}
func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, _ := rpcClient.Call("createPersonsWithRole", []Person{{"Alex", 33, "Germany"}, {"Barney", 38, "Germany"}}, []string{"Admin", "User"})
    // generates body: {"jsonrpc":"2.0","method":"createPersonsWithRole","params":[[{"name":"Alex","age":33,"country":"Germany"},{"name":"Barney","age":38,"country":"Germany"}],["Admin","User"]],"id":0}
}
Notification

A jsonrpc notification is a rpc call to the server without expecting a response. Only an error object is returned in case of networkt / http error. No id field is set in the request json object. (see: http://www.jsonrpc.org/specification#notification)

Execute an simple notification:

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    err := rpcClient.Notification("disconnectClient", 123)
    if err != nil {
        //error handling goes here
    }
}
Batch rpcjson calls

A jsonrpc batch call encapsulates multiple json-rpc requests in a single rpc-service call. It returns an array of results (for all non-notification requests). (see: http://www.jsonrpc.org/specification#batch)

Execute two jsonrpc calls and a single notification as batch:

func main() {
    rpcClient := NewRPCClient(httpServer.URL)

	req1 := rpcClient.NewRPCRequestObject("addNumbers", 1, 2)
	req2 := rpcClient.NewRPCRequestObject("getPersonByName", "alex")
	notify1 := rpcClient.NewRPCNotificationObject("disconnect", true)

	responses, _ := rpcClient.Batch(req1, req2, notify1)

    person := Person{}
    response2, _ := responses.GetResponseOf(req2)
    response2.GetObject(&person)
}

To update the ID of an existing rpcRequest object:

func main() {
    rpcClient := NewRPCClient(httpServer.URL)

	req1 := rpcClient.NewRPCRequestObject("addNumbers", 1, 2)
	req2 := rpcClient.NewRPCRequestObject("getPersonByName", "alex")
	notify1 := rpcClient.NewRPCNotifyObject("disconnect", true)

	responses, _ := rpcClient.Batch(req1, req2, notify1)

    rpcClient.UpdateRequestID(req1) // updates id to the next valid id if autoincrement is enabled
}
Working with rpc-json responses

Before working with the response object, make sure to check err != nil first. This error indicates problems on the network / http level of an error when parsing the json response.

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, err := rpcClient.Call("addNumbers", 1, 2)
    if err != nil {
        //error handling goes here
    }
}

The next thing you have to check is if an rpc-json protocol error occoured. This is done by checking if the Error field in the rpc-response != nil: (see: http://www.jsonrpc.org/specification#error_object)

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, err := rpcClient.Call("addNumbers", 1, 2)
    if err != nil {
        //error handling goes here
    }

    if response.Error != nil {
        // check response.Error.Code, response.Error.Message, response.Error.Data  here
    }
}

After making sure that no errors occoured you can now examine the RPCResponse object. When executing a json-rpc request, most of the time you will be interested in the "result"-property of the returned json-rpc response object. (see: http://www.jsonrpc.org/specification#response_object) The library provides some helper functions to retrieve the result in the data format you are interested in. Again: check for err != nil here to be sure the expected type was provided in the response and could be parsed.

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, _ := rpcClient.Call("addNumbers", 1, 2)

    result, err := response.GetInt()
    if err != nil {
        // result seems not to be an integer value
    }

    // helpers provided for all primitive types:
    response.GetInt() // int32 or int64 depends on architecture / implementation
    response.GetInt64()
    response.GetFloat64()
    response.GetString()
    response.GetBool()
}

Retrieving arrays and objects is also very simple:

// json annotations are only required to transform the structure back to json
type Person struct {
    Id   int `json:"id"`
    Name string `json:"name"`
    Age  int `json:"age"`
}

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, _ := rpcClient.Call("getPersonById", 123)

    person := Person{}
    err := response.GetObject(&Person) // expects a rpc-object result value like: {"id": 123, "name": "alex", "age": 33}

    fmt.Println(person.Name)
}

Retrieving arrays e.g. of ints:

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, _ := rpcClient.Call("getRandomNumbers", 10)

    rndNumbers := []int{}
    err := response.getObject(&rndNumbers) // expects a rpc-object result value like: [10, 188, 14, 3]

    fmt.Println(rndNumbers[0])
}
Basic authentication

If the rpc-service is running behind a basic authentication you can easily set the credentials:

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    rpcClient.SetBasicAuth("alex", "secret")
    response, _ := rpcClient.Call("addNumbers", 1, 2) // send with Authorization-Header
}
Set custom headers

Setting some custom headers (e.g. when using another authentication) is simple:

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    rpcClient.SetCustomHeader("Authorization", "Bearer abcd1234")
    response, _ := rpcClient.Call("addNumbers", 1, 2) // send with a custom Auth-Header
}
ID auto-increment

Per default the ID of the json-rpc request increments automatically for each request. You can change this behaviour:

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")
    response, _ := rpcClient.Call("addNumbers", 1, 2) // send with ID == 0
    response, _ = rpcClient.Call("addNumbers", 1, 2) // send with ID == 1
    rpcClient.SetNextID(10)
    response, _ = rpcClient.Call("addNumbers", 1, 2) // send with ID == 10
    rpcClient.SetAutoIncrementID(false)
    response, _ = rpcClient.Call("addNumbers", 1, 2) // send with ID == 11
    response, _ = rpcClient.Call("addNumbers", 1, 2) // send with ID == 11
}
Set a custom httpClient

If you have some special needs on the http.Client of the standard go library, just provide your own one. For example to use a proxy when executing json-rpc calls:

func main() {
    rpcClient := NewRPCClient("http://my-rpc-service:8080/rpc")

    proxyURL, _ := url.Parse("http://proxy:8080")
	transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}

	httpClient := &http.Client{
		Transport: transport,
	}

	rpcClient.SetHTTPClient(httpClient)
}

Documentation

Overview

Package jsonrpc provides an jsonrpc 2.0 client that sends jsonrpc requests and receives jsonrpc responses using http.

Example
type Person struct {
	Name    string `json:"name"`
	Age     int    `json:"age"`
	Country string `json:"country"`
}

// create client
rpcClient := NewRPCClient("http://my-rpc-service")

// execute rpc to service
response, _ := rpcClient.Call("getPersonByID", 12345)

// parse result into struct
var person Person
response.GetObject(&person)

// change result and send back using rpc
person.Age = 35
rpcClient.Call("setPersonByID", 12345, person)
Output:

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BatchResponse

type BatchResponse struct {
	// contains filtered or unexported fields
}

BatchResponse a list of jsonrpc response objects as a result of a batch request

if you are interested in the response of a specific request use: GetResponseOf(request)

func (*BatchResponse) GetResponseOf

func (batchResponse *BatchResponse) GetResponseOf(request *RPCRequest) (*RPCResponse, error)

GetResponseOf returns the rpc response of the corresponding request by matching the id.

For this method to work, autoincrementID should be set to true (default).

type RPCClient

type RPCClient struct {
	// contains filtered or unexported fields
}

RPCClient sends jsonrpc requests over http to the provided rpc backend. RPCClient is created using the factory function NewRPCClient().

func NewRPCClient

func NewRPCClient(endpoint string) *RPCClient

NewRPCClient returns a new RPCClient instance with default configuration (no custom headers, default http.Client, autoincrement ids). Endpoint is the rpc-service url to which the rpc requests are sent.

func (*RPCClient) Batch

func (client *RPCClient) Batch(requests ...interface{}) (*BatchResponse, error)

Batch sends a jsonrpc batch request to the rpc-service. The parameter is a list of requests the could be one of:

RPCRequest
RPCNotification.

The batch requests returns a list of RPCResponse structs.

Example
rpcClient := NewRPCClient(httpServer.URL)

req1 := rpcClient.NewRPCRequestObject("addNumbers", 1, 2, 3)
req2 := rpcClient.NewRPCRequestObject("getTimestamp")
responses, _ := rpcClient.Batch(req1, req2)

response, _ := responses.GetResponseOf(req2)
timestamp, _ := response.GetInt()

fmt.Println(timestamp)
Output:

func (*RPCClient) Call

func (client *RPCClient) Call(method string, params ...interface{}) (*RPCResponse, error)

Call sends an jsonrpc request over http to the rpc-service url that was provided on client creation.

If something went wrong on the network / http level or if json parsing failed it returns an error.

If something went wrong on the rpc-service / protocol level the Error field of the returned RPCResponse is set and contains information about the error.

If the request was successful the Error field is nil and the Result field of the RPCRespnse struct contains the rpc result.

Example
rpcClient := NewRPCClient("http://my-rpc-service")

// result processing omitted, see: RPCResponse methods
rpcClient.Call("getTimestamp")

rpcClient.Call("getPerson", 1234)

rpcClient.Call("addNumbers", 5, 2, 3)

rpcClient.Call("strangeFunction", 1, true, "alex", 3.4)

type Person struct {
	Name    string `json:"name"`
	Age     int    `json:"age"`
	Country string `json:"country"`
}

person := Person{
	Name:    "alex",
	Age:     33,
	Country: "germany",
}

rpcClient.Call("setPersonByID", 123, person)
Output:

func (*RPCClient) CallNamed

func (client *RPCClient) CallNamed(method string, params map[string]interface{}) (*RPCResponse, error)

CallNamed sends an jsonrpc request over http to the rpc-service url that was provided on client creation. This differs from Call() by sending named, rather than positional, arguments.

If something went wrong on the network / http level or if json parsing failed it returns an error.

If something went wrong on the rpc-service / protocol level the Error field of the returned RPCResponse is set and contains information about the error.

If the request was successful the Error field is nil and the Result field of the RPCRespnse struct contains the rpc result.

Example
rpcClient := NewRPCClient("http://my-rpc-service")

// result processing omitted, see: RPCResponse methods
rpcClient.CallNamed("createPerson", map[string]interface{}{
	"name":      "Bartholomew Allen",
	"nicknames": []string{"Barry", "Flash"},
	"male":      true,
	"age":       28,
	"address":   map[string]interface{}{"street": "Main Street", "city": "Central City"},
})
Output:

func (*RPCClient) NewRPCNotificationObject

func (client *RPCClient) NewRPCNotificationObject(method string, params ...interface{}) *RPCNotification

NewRPCNotificationObject creates and returns a raw RPCNotification structure. It is mainly used when building batch requests. For single notifications use RPCClient.Notification(). NewRPCNotificationObject struct can also be created directly, but this function sets the ID and the jsonrpc field to the correct values.

func (*RPCClient) NewRPCRequestObject

func (client *RPCClient) NewRPCRequestObject(method string, params ...interface{}) *RPCRequest

NewRPCRequestObject creates and returns a raw RPCRequest structure. It is mainly used when building batch requests. For single requests use RPCClient.Call(). RPCRequest struct can also be created directly, but this function sets the ID and the jsonrpc field to the correct values.

func (*RPCClient) Notification

func (client *RPCClient) Notification(method string, params ...interface{}) error

Notification sends a jsonrpc request to the rpc-service. The difference to Call() is that this request does not expect a response. The ID field of the request is omitted.

func (*RPCClient) SetAutoIncrementID

func (client *RPCClient) SetAutoIncrementID(flag bool)

SetAutoIncrementID if set to true, the id field of an rpcjson request will be incremented automatically

func (*RPCClient) SetBasicAuth

func (client *RPCClient) SetBasicAuth(username string, password string)

SetBasicAuth is a helper function that sets the header for the given basic authentication credentials. To reset / disable authentication just set username or password to an empty string value.

func (*RPCClient) SetCustomHeader

func (client *RPCClient) SetCustomHeader(key string, value string)

SetCustomHeader is used to set a custom header for each rpc request. You could for example set the Authorization Bearer here.

func (*RPCClient) SetHTTPClient

func (client *RPCClient) SetHTTPClient(httpClient *http.Client)

SetHTTPClient can be used to set a custom http.Client. This can be useful for example if you want to customize the http.Client behaviour (e.g. proxy settings)

func (*RPCClient) SetNextID

func (client *RPCClient) SetNextID(id uint)

SetNextID can be used to manually set the next id / reset the id.

func (*RPCClient) UnsetCustomHeader

func (client *RPCClient) UnsetCustomHeader(key string)

UnsetCustomHeader is used to removes a custom header that was added before.

func (*RPCClient) UpdateRequestID

func (client *RPCClient) UpdateRequestID(rpcRequest *RPCRequest)

UpdateRequestID updates the ID of an RPCRequest structure.

This is used if a request is sent another time and the request should get an updated id.

This does only make sense when used on with Batch() since Call() and Notififcation() do update the id automatically.

type RPCError

type RPCError struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

RPCError represents a jsonrpc error object if an rpc error occurred.

See: http://www.jsonrpc.org/specification#error_object

func (*RPCError) Error added in v1.0.1

func (e *RPCError) Error() string

type RPCNotification

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

RPCNotification represents a jsonrpc notification object. A notification object omits the id field since there will be no server response.

See: http://www.jsonrpc.org/specification#notification

type RPCRequest

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

RPCRequest represents a jsonrpc request object.

See: http://www.jsonrpc.org/specification#request_object

type RPCResponse

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

RPCResponse represents a jsonrpc response object. If no rpc specific error occurred Error field is nil.

See: http://www.jsonrpc.org/specification#response_object

Example
rpcClient := NewRPCClient("http://my-rpc-service")

response, _ := rpcClient.Call("addNumbers", 1, 2, 3)
sum, _ := response.GetInt()
fmt.Println(sum)

response, _ = rpcClient.Call("isValidEmail", "my@ema.il")
valid, _ := response.GetBool()
fmt.Println(valid)

response, _ = rpcClient.Call("getJoke")
joke, _ := response.GetString()
fmt.Println(joke)

response, _ = rpcClient.Call("getPi", 10)
piRounded, _ := response.GetFloat64()
fmt.Println(piRounded)

var rndNumbers []int
response, _ = rpcClient.Call("getRndIntNumbers", 10)
response.GetObject(&rndNumbers)
fmt.Println(rndNumbers[0])

type Person struct {
	Name    string `json:"name"`
	Age     int    `json:"age"`
	Country string `json:"country"`
}

var p Person
response, _ = rpcClient.Call("getPersonByID", 1234)
response.GetObject(&p)
fmt.Println(p.Name)
Output:

func (*RPCResponse) GetBool

func (rpcResponse *RPCResponse) GetBool() (bool, error)

GetBool converts the rpc response to a bool and returns it.

If result was not a bool an error is returned.

func (*RPCResponse) GetFloat64

func (rpcResponse *RPCResponse) GetFloat64() (float64, error)

GetFloat64 converts the rpc response to an float64 and returns it.

If result was not an float64 an error is returned.

func (*RPCResponse) GetInt

func (rpcResponse *RPCResponse) GetInt() (int, error)

GetInt converts the rpc response to an int and returns it.

This is a convenient function. Int could be 32 or 64 bit, depending on the architecture the code is running on. For a deterministic result use GetInt64().

If result was not an integer an error is returned.

func (*RPCResponse) GetInt64

func (rpcResponse *RPCResponse) GetInt64() (int64, error)

GetInt64 converts the rpc response to an int64 and returns it.

If result was not an integer an error is returned.

func (*RPCResponse) GetObject

func (rpcResponse *RPCResponse) GetObject(toType interface{}) error

GetObject converts the rpc response to an object (e.g. a struct) and returns it. The parameter should be a structure that can hold the data of the response object.

For example if the following json return value is expected: {"name": "alex", age: 33, "country": "Germany"} the struct should look like

type Person struct {
  Name string
  Age int
  Country string
}

func (*RPCResponse) GetString

func (rpcResponse *RPCResponse) GetString() (string, error)

GetString converts the rpc response to a string and returns it.

If result was not a string an error is returned.

Jump to

Keyboard shortcuts

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