pjrpc

package module
v2.6.0 Latest Latest
Warning

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

Go to latest
Published: Mar 10, 2024 License: MIT Imports: 13 Imported by: 5

README

pjrpc - Painless-JSON-RPC

Golang JSON-RPC router with code and specs generator (Swagger included).

Features

  • Lightweight - simple and fast router
  • Dependency free - it's required uuid package in generated code only
  • JSON-RPC protocol - support protocol v2.0 + Batch requests + Notifications
  • Go std-lib compatible - net/http, net and json.RawMessage
  • Go way context usage - ctx in your handlers with data
  • Handler middlewares - use your repeatable code as middlewares
  • Boilerplate code generator - unmarshal JSON params to the custom models
  • Go spec - describe your types and methods in Go with genpjrpc generator
  • Swagger spec - genpjrpc can generate openapi spec with methods and types based on Go code.
  • WASM Client - JSON-RPC client with generated files can compile to WASM
  • Websocket - JSON-RPC client/server over Websocket protocol
  • TCP and Async Client - JSON-RPC over async protocols and client (https://gitlab.com/pjrpc/pjrpc/-/tree/master/_examples/tcp)
  • Using as a JSON-RPC router - you can use this router like a simple JSON-RPC router without code-gen

Benchmarks

Golang as a spec

Create your Go types as transport models and use them in the service interface.

See documentation of the genpjrpc for details.

file model/types/types.go:

package types

type Request struct {
	Name string `json:"name"`
}

type Response struct {
	ID int `json:"id"`
}

file model/service/service.go:

package serivce

import "app/model/types"

// Service description...
//go:generate genpjrpc -search.name=Service -print.place.path_swagger_file=../../api/swagger.json
type Service interface {
	Method(types.Request) types.Response
}

Install

go install gitlab.com/pjrpc/pjrpc/cmd/genpjrpc@latest

Run generator:

go generate ./...

Files will be generated by genpjrpc:

# Go wrappers for server register func and real server interface.
./app/model/service/rpcserver/pjrpc_server_service.go

# Go wrappers for client to the rpc serice.
./app/model/service/rpcclient/pjrpc_client_service.go

# OpenAPI spec with rpc methods and every used type.
./app/api/swagger.json

Examples

Server example

package main

import (
	"context"
	"errors"
	"log"
	"net/http"

	"gitlab.com/pjrpc/pjrpc/v2"

	"app/model/service/rpcserver"
	"app/model/types"
)

var errServer = errors.New("server error")

type rpc struct{}

// Method responder.
func (r *rpc) Method(ctx context.Context, in *types.Request) (*types.Response, error) {
	log.Printf("got message from: %s", in.Name)

	switch in.Name {
	case "rpc client": // it's ok, pass

	case "error": // return golang error
		return nil, errServer

	case "panic":
		panic("rpc client sent panic")

	default: // returns json-rpc error
		return nil, jerrs.InvalidParams("wrong name")
	}

	return &types.Response{Name: "rpc server"}, nil
}

func main() {
	srv := pjrpc.NewServerHTTP()
	srv.SetLogger(log.Writer())

	r := &rpc{}

	rpcserver.RegisterServiceServer(srv, r)

	mux := http.NewServeMux()
	mux.Handle("/rpc/", srv)

	log.Println("Starting rpc server on :8080")

	err := http.ListenAndServe(":8080", mux)
	if err != nil {
		log.Fatalf("http.ListenAndServe: %s", err)
	}
}

request:

{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "hello",
  "params": {
    "name": "rpc client"
  }
}

response:

{
  "jsonrpc": "2.0",
  "id": "1",
  "result": {
    "name": "rpc server"
  }
}

Documentation

Overview

Package pjrpc contains router and Servers to build JSON-RPC service. Also it contains generator for boilerplate code looks like a gRPC.

Index

Examples

Constants

View Source
const (
	// ContentTypeHeaderName name of the header Content-Type.
	ContentTypeHeaderName = "Content-Type"
	// ContentTypeHeaderValue value of the header Content-Type.
	ContentTypeHeaderValue = "application/json"
	// JSONRPCVersion is a version of supported JSON-RPC protocol.
	JSONRPCVersion = "2.0"
)

Variables

View Source
var (
	// ErrPanicInHandler returns in the server when handler called panic.
	ErrPanicInHandler = errors.New("panic in handler")

	// ErrBadStatusCode returns in client when http response has code not 200.
	ErrBadStatusCode = errors.New("bad status code")

	// ErrWrongContentType returns in client when http response has content-type not application/json.
	ErrWrongContentType = errors.New("wrong content type")

	// ErrUnsupportedMods returns in client that are not supported request modifications.
	ErrUnsupportedMods = errors.New("mods are not supported")

	// ErrorResponseChannelClosed returns in async client when response listener got closed channel.
	ErrorResponseChannelClosed = errors.New("response channel is closed")
)

Functions

func ContextGetConnection added in v2.3.0

func ContextGetConnection(ctx context.Context) (conn net.Conn, ok bool)

ContextGetConnection returns websocket connection from context.

func ContextSetConnection added in v2.3.0

func ContextSetConnection(ctx context.Context, conn net.Conn) context.Context

ContextSetConnection sets websocket connection to the context.

func ContextSetData

func ContextSetData(ctx context.Context, data *ContextData) context.Context

ContextSetData sets ContextData to the context.

Types

type BatchRequests

type BatchRequests []*Request

BatchRequests client MAY send an Array filled with Request objects. See spec https://www.jsonrpc.org/specification#batch page.

type BatchResponses

type BatchResponses []*Response

BatchResponses the Server should respond with an Array containing the corresponding Response objects, after all of the batch Request objects have been processed.

type ContextData

type ContextData struct {
	// It's raw HTTP request from router.
	HTTPRequest *http.Request
	// It's parsed body of the request.
	JRPCRequest *Request
	// It will be true if this JSON-RPC request is a part of the one request.
	IsBatch bool
}

ContextData router data of the request as context.

func ContextGetData

func ContextGetData(ctx context.Context) (d *ContextData, ok bool)

ContextGetData returns ContextData from the context.

type ErrorResponse

type ErrorResponse struct {
	// A Number that indicates the error type that occurred.
	Code int `json:"code"`
	// A String providing a short description of the error.
	Message string `json:"message"`
	// A Primitive or Structured value that contains additional information about the error.
	Data json.RawMessage `json:"data,omitempty"`
}

ErrorResponse model of the response with error.

Example
package main

import (
	"encoding/json"
	"fmt"

	"gitlab.com/pjrpc/pjrpc/v2"
)

type customErrorData struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

func main() {
	// Specification errors with or without your data.
	jerr := pjrpc.JRPCErrInternalError()
	fmt.Println(jerr)

	jerr = pjrpc.JRPCErrInternalError("some text")
	fmt.Println(jerr)

	jerr = pjrpc.JRPCErrInternalError(nil)
	fmt.Println(jerr)

	data := &customErrorData{
		Code:    123,
		Message: "custom message",
	}

	jerr = pjrpc.JRPCErrInternalError(data)
	fmt.Println(jerr)

	jsonErr, err := json.Marshal(jerr)
	if err != nil {
		return
	}
	fmt.Println(string(jsonErr))

	// Your custom JSON-RPC error.
	jerr = pjrpc.JRPCErrServerError(-32001)
	fmt.Println(jerr)

	jsonErr, err = json.Marshal(jerr)
	if err != nil {
		return
	}
	fmt.Println(string(jsonErr))

	jerr = pjrpc.JRPCErrServerError(-32002, nil)
	fmt.Println(jerr)

	jsonErr, err = json.Marshal(jerr)
	if err != nil {
		return
	}
	fmt.Println(string(jsonErr))

	// Your custom JSON-RPC error with your data.
	data.Code = 321
	jerr = pjrpc.JRPCErrServerError(-32002, data)
	fmt.Println(jerr)

	jsonErr, err = json.Marshal(jerr)
	if err != nil {
		return
	}
	fmt.Println(string(jsonErr))

}
Output:

JSON-RPC Error: [-32603] Internal error
JSON-RPC Error: [-32603] Internal error ("some text")
JSON-RPC Error: [-32603] Internal error (null)
JSON-RPC Error: [-32603] Internal error ({"code":123,"message":"custom message"})
{"code":-32603,"message":"Internal error","data":{"code":123,"message":"custom message"}}
JSON-RPC Error: [-32001] Server error
{"code":-32001,"message":"Server error"}
JSON-RPC Error: [-32002] Server error (null)
{"code":-32002,"message":"Server error","data":null}
JSON-RPC Error: [-32002] Server error ({"code":321,"message":"custom message"})
{"code":-32002,"message":"Server error","data":{"code":321,"message":"custom message"}}

func JRPCErrInternalError

func JRPCErrInternalError(data ...any) *ErrorResponse

JRPCErrInternalError internal JSON-RPC error.

func JRPCErrInvalidParams

func JRPCErrInvalidParams(data ...any) *ErrorResponse

JRPCErrInvalidParams invalid method parameter(s).

func JRPCErrInvalidRequest

func JRPCErrInvalidRequest(data ...any) *ErrorResponse

JRPCErrInvalidRequest the JSON sent is not a valid Request object.

func JRPCErrMethodNotFound

func JRPCErrMethodNotFound(data ...any) *ErrorResponse

JRPCErrMethodNotFound the method does not exist / is not available.

func JRPCErrParseError

func JRPCErrParseError(data ...any) *ErrorResponse

JRPCErrParseError invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.

func JRPCErrServerError

func JRPCErrServerError(code int, data ...any) *ErrorResponse

JRPCErrServerError reserved for implementation-defined server-errors. Codes -32000 to -32099.

func (*ErrorResponse) Error

func (e *ErrorResponse) Error() string

Error implementation error interface.

type Handler

type Handler func(ctx context.Context, params json.RawMessage) (any, error)

Handler request handler takes request context and raw json params. Returns type with result data and error.

type Middleware

type Middleware func(next Handler) Handler

Middleware wraps handler and call before wrapped handler.

type Registrator

type Registrator interface {
	// RegisterMethod saves method handler to router.
	RegisterMethod(methodName string, h Handler)
	// With adds midlewares to the queue. It will be call before methods.
	With(mws ...Middleware)
}

Registrator provides methods to register handler and middlewares.

type Request

type Request struct {
	// A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
	JSONRPC string `json:"jsonrpc"`
	// An identifier established by the Client that MUST contain a String, Number, or NULL value if included.
	ID json.RawMessage `json:"id,omitempty"`
	// A String containing the name of the method to be invoked.
	Method string `json:"method"`
	// A Structured value that holds the parameter values to be used during the invocation of the method.
	Params json.RawMessage `json:"params,omitempty"`
}

Request model of the JSON-RPC request.

func NewRequest added in v2.2.0

func NewRequest(id, method string, params any) (req *Request, err error)

NewRequest returns new Request object with marshaled ID and Params. You can pass ID as empty string if you want to create a Notification request.

func (*Request) GetID

func (r *Request) GetID() string

GetID returns id of the request as a string.

func (*Request) IsNotification added in v2.2.0

func (r *Request) IsNotification() bool

IsNotification returns true when request doesn't have id.

func (*Request) JSON added in v2.2.0

func (r *Request) JSON() json.RawMessage

JSON converts Request object to JSON message.

type Response

type Response struct {
	// A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
	JSONRPC string `json:"jsonrpc"`
	// It MUST be the same as the value of the id member in the Request.
	ID json.RawMessage `json:"id,omitempty"`
	// This member is REQUIRED on success. The value of this member is determined
	// by the method invoked on the Server.
	Result json.RawMessage `json:"result,omitempty"`
	// This member is REQUIRED on error. This member MUST NOT exist if there was no error
	// triggered during invocation.
	Error *ErrorResponse `json:"error,omitempty"`
}

Response model of the response object.

func NewResponseFromError added in v2.2.0

func NewResponseFromError(err error) *Response

NewResponseFromError returns response model based on Error.

func NewResponseFromJSON added in v2.2.0

func NewResponseFromJSON(msg json.RawMessage) (*Response, error)

NewResponseFromJSON parses json message and returns Response object.

func NewResponseFromJSONReader added in v2.2.0

func NewResponseFromJSONReader(r io.Reader) (*Response, error)

NewResponseFromJSONReader reads io.Reader as a JSON data. Returns Response object and parse error.

func NewResponseFromRequest added in v2.2.0

func NewResponseFromRequest(req *Request) *Response

NewResponseFromRequest returns new response model based on Request.

func (*Response) GetID

func (r *Response) GetID() string

GetID returns id of the response as a string.

func (*Response) JSON added in v2.2.0

func (r *Response) JSON() json.RawMessage

JSON converts response model into JSON text.

func (*Response) SetError

func (r *Response) SetError(err error) *Response

SetError sets error to Response model.

func (*Response) SetResult

func (r *Response) SetResult(result any) *Response

SetResult sets result to Response model.

func (*Response) UnmarshalResult added in v2.2.0

func (r *Response) UnmarshalResult(dst any) error

UnmarshalResult parses Result field in the Response object to destination param.

type Router

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

Router contains storage with handlers and common middleware.

func NewRouter

func NewRouter() *Router

NewRouter creates new router with empty handler storage.

func (*Router) Invoke

func (r *Router) Invoke(ctx context.Context, methodName string, params json.RawMessage) (any, error)

Invoke invokes handler by method name. If the router has middleware, it will be called first. After that the registered middleware will be called and then the registered handler.

func (*Router) MethodWith added in v2.1.0

func (r *Router) MethodWith(methodName string, mw Middleware) error

MethodWith adds middleware to exclusive method, method must be registered.

func (*Router) RegisterMethod

func (r *Router) RegisterMethod(methodName string, h Handler)

RegisterMethod saves handler of the method to storage.

func (*Router) With

func (r *Router) With(mws ...Middleware)

With adds middlewares to the router's queue of the middlewares.

type Server added in v2.2.0

type Server struct {
	// Contains handlers for JSON-RPC methods.
	Router *Router

	// Logger is an optional logger as a last chance to alert about error.
	// Also writes panics from handlers.
	Logger *log.Logger

	// Panic handler calls when your rpc handler make panic.
	// There will be default panic handler (DefaultRestoreOnPanic).
	OnPanic func(ctx context.Context, err error) *ErrorResponse

	// OnErrorParseRequest handler calls when server can't parse client request.
	// There will be default handler (DefaultOnErrorParseRequest).
	OnErrorParseRequest func(ctx context.Context, err error) *ErrorResponse
}

Server as an protocol agnostic only JSON-RPC specification.

func NewServer added in v2.2.0

func NewServer() *Server

NewServer returns new protocol agnostic JSON-RPC server.

func (*Server) DefaultOnErrorParseRequest added in v2.2.0

func (*Server) DefaultOnErrorParseRequest(context.Context, error) *ErrorResponse

DefaultOnErrorParseRequest default handler calls when server can't parse client request. Returns JSON-RPC error Invalid Request with static text.

func (*Server) DefaultRestoreOnPanic added in v2.2.0

func (s *Server) DefaultRestoreOnPanic(_ context.Context, err error) *ErrorResponse

DefaultRestoreOnPanic default panic handler. It just prints error in log and sets default internal error in response.

func (*Server) RegisterMethod added in v2.2.0

func (s *Server) RegisterMethod(methodName string, h Handler)

RegisterMethod saves handler of the method to starage.

func (*Server) Serve added in v2.2.0

func (s *Server) Serve(ctx context.Context, body json.RawMessage) json.RawMessage

Serve is a main method of the Server. It parses body of the request and builds response. This method never returns error but puts it in the response.

func (*Server) SetLogger added in v2.2.0

func (s *Server) SetLogger(w io.Writer)

SetLogger sets your io.Writer as an error logger of the pjrpc server. Also you can set your own *log.Logger in Logger field.

func (*Server) With added in v2.2.0

func (s *Server) With(mws ...Middleware)

With adds middlewares to the router's queue of the middlewares.

type ServerHTTP

type ServerHTTP struct {
	*Server

	MaxBodyLimit int64
}

ServerHTTP JSON-RPC server over HTTP protocol.

Example
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"strings"

	"gitlab.com/pjrpc/pjrpc/v2"
)

type rpc struct{}

type request struct {
	Name string `json:"name"`
}

type response struct {
	Name string `json:"name"`
}

func middleware(next pjrpc.Handler) pjrpc.Handler {
	return func(ctx context.Context, params json.RawMessage) (any, error) {
		fmt.Println("I'm middleware :)")
		return next(ctx, params)
	}
}

func (*rpc) helloHandler(ctx context.Context, params json.RawMessage) (any, error) {
	req := &request{}
	err := json.Unmarshal(params, req)
	if err != nil {
		return nil, pjrpc.JRPCErrInvalidParams(err.Error())
	}

	fmt.Println("request from:", req.Name)
	return &response{Name: "server"}, nil
}

func main() {
	r := &rpc{}
	srv := pjrpc.NewServerHTTP()

	// add your error logger to the server to catch error in write response method and panics.
	srv.SetLogger(log.Writer())

	srv.With(middleware)

	srv.RegisterMethod("hello", r.helloHandler)

	mux := http.NewServeMux()
	mux.Handle("/rpc", srv)

	// In real life you will start listener of the http server...

	rec := httptest.NewRecorder()
	bodyReq := strings.NewReader(`{"jsonrpc":"2.0","id":"1","method":"hello","params":{"name":"client"}}`)
	req := httptest.NewRequest(http.MethodPost, "/rpc", bodyReq)
	req.Header.Set("Content-Type", "application/json")

	mux.ServeHTTP(rec, req)

	fmt.Println("=== text after request ===")
	fmt.Println(rec.Code)
	fmt.Println(rec.Body.String())

}
Output:

I'm middleware :)
request from: client
=== text after request ===
200
{"jsonrpc":"2.0","id":"1","result":{"name":"server"}}

func NewServerHTTP

func NewServerHTTP(options ...ServerHTTPOption) *ServerHTTP

NewServerHTTP creates new server with default error handlers and empty router.

func (*ServerHTTP) ServeHTTP

func (s *ServerHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements interface of handler in http package.

type ServerHTTPOption added in v2.5.0

type ServerHTTPOption func(s *ServerHTTP)

ServerHTTPOption options to modify parameters of the HTTP server.

func ServerHTTPOptionMaxBodyLimit added in v2.5.0

func ServerHTTPOptionMaxBodyLimit(limit int64) ServerHTTPOption

ServerHTTPOptionMaxBodyLimit sets limit of body reader.

type ServerListener added in v2.3.0

type ServerListener struct {
	*Server

	Listener net.Listener

	// OnErrorAccept takes error after attempt to accept a new connection.
	// If you don't want to stop the listener you can return nil here.
	OnErrorAccept func(err error) error

	// OnNewConnection it called on each new accepted connection.
	// You can return false here if you want to refuse the connection.
	OnNewConnection func(ctx context.Context, conn net.Conn) (accept bool)

	// OnCloseConnection it called before each closure of the connection.
	// If you refused the connection in the OnNewConnection method,
	// you will not get that connection here.
	OnCloseConnection func(ctx context.Context, conn net.Conn)

	// OnErrorReceive it called when Handler gets reading error from the connection.
	// By default the connection will be closed.
	OnErrorReceive func(ctx context.Context, conn net.Conn, err error) (needClose bool)

	// OnErrorSend it called when Handler gets writing error from the connection.
	// By default the connection will be closed.
	OnErrorSend func(ctx context.Context, conn net.Conn, err error) (needClose bool)
}

ServerListener JSON-RPC server based on net.Listener. You can use it as JSON-RPC server over TCP or other protocols. Look for "On..." method of the server to set your own callbacks.

Example
listner, err := net.Listen("tcp", ":35396")
if err != nil {
	fmt.Println("net.Listen:", err)
	return
}

srv := pjrpc.NewServerListener(listner)

srv.RegisterMethod("method", func(ctx context.Context, params json.RawMessage) (any, error) {
	fmt.Println("request:", string(params))
	return "response", nil
})

go func() {
	if errListen := srv.Listen(); errListen != nil {
		fmt.Println("srv.Listen:", errListen)
	}
}()

conn, err := net.Dial("tcp", ":35396")
if err != nil {
	fmt.Println("net.Dial:", err)
	return
}

cl := client.NewAsyncClient(conn)

go func() {
	if errListen := cl.Listen(); errListen != nil {
		fmt.Println("cl.Listen:", errListen)
	}
}()

var res string
err = cl.Invoke(context.Background(), "1", "method", "params", &res)
if err != nil {
	fmt.Println("cl.Invoke:", err)
	return
}

if err = cl.Close(); err != nil {
	fmt.Println("cl.Close:", err)
	return
}

if err = srv.Close(); err != nil {
	fmt.Println("cl.Close:", err)
	return
}

fmt.Println("response:", res)
Output:


request: "params"
response: response

func NewServerListener added in v2.3.0

func NewServerListener(listener net.Listener) *ServerListener

NewServerListener returns a new Server with default handler's callbacks.

func (*ServerListener) Close added in v2.3.0

func (s *ServerListener) Close() error

Close closes the net.Listener of the server.

func (*ServerListener) Handler added in v2.3.0

func (s *ServerListener) Handler(ctx context.Context, conn net.Conn)

Handler takes enriched context and connection from the network. It calls handler callbacks and listens the connection for messages.

func (*ServerListener) Listen added in v2.3.0

func (s *ServerListener) Listen() error

Listen starts to listen the net.Listener for a new connections from the network.

Directories

Path Synopsis
Package client is a JSON-RPC client.
Package client is a JSON-RPC client.
Package pjson implements json marshal, unmarshal methods and encoder, decoder.
Package pjson implements json marshal, unmarshal methods and encoder, decoder.
Package storage contains storage of the internal handlers.
Package storage contains storage of the internal handlers.

Jump to

Keyboard shortcuts

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