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