initial
This commit is contained in:
commit
440f3f4604
8 changed files with 393 additions and 0 deletions
74
README.md
Normal file
74
README.md
Normal 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
42
errors.go
Normal 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
41
examples/http/main.go
Normal 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
42
examples/http/test.http
Normal 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
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/neonxp/rpc
|
||||||
|
|
||||||
|
go 1.18
|
20
logger.go
Normal file
20
logger.go
Normal 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
146
server.go
Normal 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
25
wrapper.go
Normal 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)
|
Loading…
Reference in a new issue