This commit is contained in:
Alexander Kiryukhin 2022-01-31 02:31:43 +03:00
commit 440f3f4604
No known key found for this signature in database
GPG Key ID: 6DF7A2910D0699E9
8 changed files with 393 additions and 0 deletions

74
README.md Normal file
View File

@ -0,0 +1,74 @@
# JSON-RPC 2.0
Golang implementation of JSON-RPC 2.0 server with generics.
Go 1.18+ required
## Features:
- [x] Batch request and responses
- [ ] WebSockets
## Usage
1. Create JSON-RPC 2.0 server:
```go
s := jsonrpc2.New()
```
2. Write handler:
```go
func Multiply(ctx context.Context, args *Args) (int, error) {
return args.A * args.B, nil
}
```
Handler must have exact two arguments (context and input of any json serializable type) and exact two return values (output of any json serializable type and error)
3. Wrap handler with `jsonrpc2.Wrap` method and register it in server:
```go
s.Register("multiply", jsonrpc2.Wrap(Multiply))
```
4. Use server as common http handler:
```go
http.ListenAndServe(":8000", s)
```
## Complete example
[Full code](/examples/http)
```go
package main
import (
"context"
"net/http"
"github.com/neonxp/rpc"
)
func main() {
s := jsonrpc2.New()
s.Register("multiply", jsonrpc2.Wrap(Multiply)) // Register handlers
s.Register("divide", jsonrpc2.Wrap(Divide))
http.ListenAndServe(":8000", s)
}
func Multiply(ctx context.Context, args *Args) (int, error) {
//...
}
func Divide(ctx context.Context, args *Args) (*Quotient, error) {
//...
}
type Args struct {
A int `json:"a"`
B int `json:"b"`
}
type Quotient struct {
Quo int `json:"quo"`
Rem int `json:"rem"`
}
```

42
errors.go Normal file
View File

@ -0,0 +1,42 @@
package jsonrpc2
import "fmt"
const (
ErrCodeParseError = -32700
ErrCodeInvalidRequest = -32600
ErrCodeMethodNotFound = -32601
ErrCodeInvalidParams = -32602
ErrCodeInternalError = -32603
ErrUser = -32000
)
var errorMap = map[int]string{
-32700: "Parse error", // Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.
-32600: "Invalid Request", // The JSON sent is not a valid Request object.
-32601: "Method not found", // The method does not exist / is not available.
-32602: "Invalid params", // Invalid method parameter(s).
-32603: "Internal error", // Internal JSON-RPC error.
-32000: "Other error",
}
//-32000 to -32099 Server error Reserved for implementation-defined server-errors.
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e Error) Error() string {
return fmt.Sprintf("jsonrpc2 error: code: %d message: %s", e.Code, e.Message)
}
func NewError(code int) Error {
if _, ok := errorMap[code]; ok {
return Error{
Code: code,
Message: errorMap[code],
}
}
return Error{Code: code}
}

41
examples/http/main.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"context"
"errors"
"net/http"
"github.com/neonxp/rpc"
)
func main() {
s := jsonrpc2.New()
s.Register("multiply", jsonrpc2.Wrap(Multiply))
s.Register("divide", jsonrpc2.Wrap(Divide))
http.ListenAndServe(":8000", s)
}
func Multiply(ctx context.Context, args *Args) (int, error) {
return args.A * args.B, nil
}
func Divide(ctx context.Context, args *Args) (*Quotient, error) {
if args.B == 0 {
return nil, errors.New("divide by zero")
}
quo := new(Quotient)
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return quo, nil
}
type Args struct {
A int `json:"a"`
B int `json:"b"`
}
type Quotient struct {
Quo int `json:"quo"`
Rem int `json:"rem"`
}

42
examples/http/test.http Normal file
View File

@ -0,0 +1,42 @@
POST http://localhost:8000/
Content-Type: application/json
{
"jsonrpc": "2.0",
"method": "multiply",
"params": {
"a": 2,
"b": 3
},
"id": 1
}
###
POST http://localhost:8000/
Content-Type: application/json
{
"jsonrpc": "2.0",
"method": "divide",
"params": {
"a": 10,
"b": 3
},
"id": 2
}
###
POST http://localhost:8000/
Content-Type: application/json
[
{ "jsonrpc": "2.0", "method": "multiply", "params": { "a": 2, "b": 3 }, "id": 10 },
{"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"}
]

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/neonxp/rpc
go 1.18

20
logger.go Normal file
View File

@ -0,0 +1,20 @@
package jsonrpc2
import "log"
type Logger interface {
Logf(format string, args ...interface{})
}
type nopLogger struct{}
func (n nopLogger) Logf(format string, args ...interface{}) {
}
type stdLogger struct{}
func (n stdLogger) Logf(format string, args ...interface{}) {
log.Printf(format, args...)
}
var StdLogger = stdLogger{}

146
server.go Normal file
View File

@ -0,0 +1,146 @@
package jsonrpc2
import (
"bufio"
"context"
"encoding/json"
"io"
"net/http"
"sync"
)
const version = "2.0"
type Server struct {
Logger Logger
IgnoreNotifications bool
handlers map[string]Handler
mu sync.RWMutex
}
func (r *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
buf := bufio.NewReader(request.Body)
defer request.Body.Close()
firstByte, err := buf.Peek(1)
if err != nil {
r.Logger.Logf("Can't read body: %v", err)
writeError(ErrCodeParseError, writer)
return
}
if string(firstByte) == "[" {
r.batchRequest(writer, request, buf)
return
}
r.singleRequest(writer, request, buf)
}
func New() *Server {
return &Server{
Logger: nopLogger{},
IgnoreNotifications: true,
handlers: map[string]Handler{},
mu: sync.RWMutex{},
}
}
func (r *Server) Register(method string, handler Handler) {
r.mu.Lock()
defer r.mu.Unlock()
r.handlers[method] = handler
}
func (r *Server) singleRequest(writer http.ResponseWriter, request *http.Request, buf *bufio.Reader) {
req := new(rpcRequest)
if err := json.NewDecoder(buf).Decode(req); err != nil {
r.Logger.Logf("Can't read body: %v", err)
writeError(ErrCodeParseError, writer)
return
}
resp := r.callMethod(request.Context(), req)
if req.Id == nil && r.IgnoreNotifications {
// notification request
return
}
if err := json.NewEncoder(writer).Encode(resp); err != nil {
r.Logger.Logf("Can't write response: %v", err)
writeError(ErrCodeInternalError, writer)
return
}
}
func (r *Server) batchRequest(writer http.ResponseWriter, request *http.Request, buf *bufio.Reader) {
var req []rpcRequest
if err := json.NewDecoder(buf).Decode(&req); err != nil {
r.Logger.Logf("Can't read body: %v", err)
writeError(ErrCodeParseError, writer)
return
}
var responses []*rpcResponse
wg := sync.WaitGroup{}
wg.Add(len(req))
for _, j := range req {
go func(req rpcRequest) {
defer wg.Done()
resp := r.callMethod(request.Context(), &req)
if req.Id == nil && r.IgnoreNotifications {
// notification request
return
}
responses = append(responses, resp)
}(j)
}
wg.Wait()
if err := json.NewEncoder(writer).Encode(responses); err != nil {
r.Logger.Logf("Can't write response: %v", err)
writeError(ErrCodeInternalError, writer)
}
}
func (r *Server) callMethod(ctx context.Context, req *rpcRequest) *rpcResponse {
r.mu.RLock()
h, ok := r.handlers[req.Method]
r.mu.RUnlock()
if !ok {
return &rpcResponse{
Jsonrpc: version,
Error: NewError(ErrCodeMethodNotFound),
Id: req.Id,
}
}
resp, err := h(ctx, req.Params)
if err != nil {
r.Logger.Logf("User error %v", err)
return &rpcResponse{
Jsonrpc: version,
Error: err,
Id: req.Id,
}
}
return &rpcResponse{
Jsonrpc: version,
Result: resp,
Id: req.Id,
}
}
func writeError(code int, w io.Writer) {
_ = json.NewEncoder(w).Encode(rpcResponse{
Jsonrpc: version,
Error: NewError(code),
})
}
type rpcRequest struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
Id any `json:"id"`
}
type rpcResponse struct {
Jsonrpc string `json:"jsonrpc"`
Result json.RawMessage `json:"result,omitempty"`
Error error `json:"error,omitempty"`
Id any `json:"id,omitempty"`
}

25
wrapper.go Normal file
View File

@ -0,0 +1,25 @@
package jsonrpc2
import (
"context"
"encoding/json"
)
func Wrap[RQ any, RS any](handler func(context.Context, *RQ) (RS, error)) Handler {
return func(ctx context.Context, in json.RawMessage) (json.RawMessage, error) {
req := new(RQ)
if err := json.Unmarshal(in, req); err != nil {
return nil, NewError(ErrCodeParseError)
}
resp, err := handler(ctx, req)
if err != nil {
return nil, Error{
Code: ErrUser,
Message: err.Error(),
}
}
return json.Marshal(resp)
}
}
type Handler func(context.Context, json.RawMessage) (json.RawMessage, error)