From 4cf58de9bb7b109dddbc80adefe52cdf61328d0d Mon Sep 17 00:00:00 2001 From: Alexander Kiryukhin Date: Mon, 31 Jan 2022 20:17:31 +0300 Subject: Small refactoring --- README.md | 33 +++++++----- errors.go | 42 --------------- examples/http/main.go | 10 ++-- http/server.go | 33 ++++++++++++ logger.go | 20 ------- rpc/errors.go | 42 +++++++++++++++ rpc/logger.go | 20 +++++++ rpc/server.go | 127 +++++++++++++++++++++++++++++++++++++++++++ rpc/wrapper.go | 25 +++++++++ server.go | 146 -------------------------------------------------- wrapper.go | 25 --------- 11 files changed, 273 insertions(+), 250 deletions(-) delete mode 100644 errors.go create mode 100644 http/server.go delete mode 100644 logger.go create mode 100644 rpc/errors.go create mode 100644 rpc/logger.go create mode 100644 rpc/server.go create mode 100644 rpc/wrapper.go delete mode 100644 server.go delete mode 100644 wrapper.go diff --git a/README.md b/README.md index 13ff84b..750c251 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,15 @@ Go 1.18+ required ## Features: - [x] Batch request and responses -- [ ] WebSockets +- [ ] WebSocket transport -## Usage +## Usage (http transport) -1. Create JSON-RPC 2.0 server: +1. Create JSON-RPC/HTTP server: ```go - s := jsonrpc2.New() + import "github.com/neonxp/jsonrpc2/http" + ... + s := http.New() ``` 2. Write handler: ```go @@ -22,15 +24,19 @@ Go 1.18+ required } ``` 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: +3. Wrap handler with `rpc.Wrap` method and register it in server: ```go - s.Register("multiply", jsonrpc2.Wrap(Multiply)) + s.Register("multiply", rpc.Wrap(Multiply)) ``` 4. Use server as common http handler: ```go http.ListenAndServe(":8000", s) ``` +## Custom transport + +See [http/server.go](/http/server.go) for example of transport implementation. + ## Complete example [Full code](/examples/http) @@ -39,18 +45,19 @@ Go 1.18+ required package main import ( - "context" - "net/http" + "context" + "net/http" - "github.com/neonxp/jsonrpc2" + httpRPC "github.com/neonxp/jsonrpc2/http" + "github.com/neonxp/jsonrpc2/rpc" ) func main() { - s := jsonrpc2.New() - s.Register("multiply", jsonrpc2.Wrap(Multiply)) // Register handlers - s.Register("divide", jsonrpc2.Wrap(Divide)) + s := httpRPC.New() + s.Register("multiply", rpc.Wrap(Multiply)) + s.Register("divide", rpc.Wrap(Divide)) - http.ListenAndServe(":8000", s) + http.ListenAndServe(":8000", s) } func Multiply(ctx context.Context, args *Args) (int, error) { diff --git a/errors.go b/errors.go deleted file mode 100644 index cd87fbb..0000000 --- a/errors.go +++ /dev/null @@ -1,42 +0,0 @@ -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} -} diff --git a/examples/http/main.go b/examples/http/main.go index 730fc03..5783c05 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -5,13 +5,15 @@ import ( "errors" "net/http" - "github.com/neonxp/jsonrpc2" + httpRPC "github.com/neonxp/jsonrpc2/http" + "github.com/neonxp/jsonrpc2/rpc" ) func main() { - s := jsonrpc2.New() - s.Register("multiply", jsonrpc2.Wrap(Multiply)) - s.Register("divide", jsonrpc2.Wrap(Divide)) + s := httpRPC.New() + + s.Register("multiply", rpc.Wrap(Multiply)) + s.Register("divide", rpc.Wrap(Divide)) http.ListenAndServe(":8000", s) } diff --git a/http/server.go b/http/server.go new file mode 100644 index 0000000..5fca66a --- /dev/null +++ b/http/server.go @@ -0,0 +1,33 @@ +package http + +import ( + "bufio" + "net/http" + + "github.com/neonxp/jsonrpc2/rpc" +) + +type Server struct { + *rpc.RpcServer +} + +func New() *Server { + return &Server{RpcServer: rpc.New()} +} + +func (r *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + writer.Header().Set("Content-Type", "application/json") + reader := bufio.NewReader(request.Body) + defer request.Body.Close() + firstByte, err := reader.Peek(1) + if err != nil { + r.Logger.Logf("Can't read body: %v", err) + rpc.WriteError(rpc.ErrCodeParseError, writer) + return + } + if string(firstByte) == "[" { + r.BatchRequest(request.Context(), reader, writer) + return + } + r.SingleRequest(request.Context(), reader, writer) +} diff --git a/logger.go b/logger.go deleted file mode 100644 index 7907e4f..0000000 --- a/logger.go +++ /dev/null @@ -1,20 +0,0 @@ -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{} diff --git a/rpc/errors.go b/rpc/errors.go new file mode 100644 index 0000000..1af84ee --- /dev/null +++ b/rpc/errors.go @@ -0,0 +1,42 @@ +package rpc + +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 RpcServer 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} +} diff --git a/rpc/logger.go b/rpc/logger.go new file mode 100644 index 0000000..25fe2d6 --- /dev/null +++ b/rpc/logger.go @@ -0,0 +1,20 @@ +package rpc + +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{} diff --git a/rpc/server.go b/rpc/server.go new file mode 100644 index 0000000..12f07e8 --- /dev/null +++ b/rpc/server.go @@ -0,0 +1,127 @@ +package rpc + +import ( + "context" + "encoding/json" + "io" + "sync" +) + +const version = "2.0" + +type RpcServer struct { + Logger Logger + IgnoreNotifications bool + handlers map[string]Handler + mu sync.RWMutex +} + +func New() *RpcServer { + return &RpcServer{ + Logger: nopLogger{}, + IgnoreNotifications: true, + handlers: map[string]Handler{}, + mu: sync.RWMutex{}, + } +} + +func (r *RpcServer) Register(method string, handler Handler) { + r.mu.Lock() + defer r.mu.Unlock() + r.handlers[method] = handler +} + +func (r *RpcServer) SingleRequest(ctx context.Context, reader io.Reader, writer io.Writer) { + req := new(rpcRequest) + if err := json.NewDecoder(reader).Decode(req); err != nil { + r.Logger.Logf("Can't read body: %v", err) + WriteError(ErrCodeParseError, writer) + return + } + resp := r.callMethod(ctx, 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 *RpcServer) BatchRequest(ctx context.Context, reader io.Reader, writer io.Writer) { + var req []rpcRequest + if err := json.NewDecoder(reader).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(ctx, &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 *RpcServer) 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"` +} diff --git a/rpc/wrapper.go b/rpc/wrapper.go new file mode 100644 index 0000000..0bc0a5c --- /dev/null +++ b/rpc/wrapper.go @@ -0,0 +1,25 @@ +package rpc + +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) diff --git a/server.go b/server.go deleted file mode 100644 index 5625f78..0000000 --- a/server.go +++ /dev/null @@ -1,146 +0,0 @@ -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"` -} diff --git a/wrapper.go b/wrapper.go deleted file mode 100644 index 8058a27..0000000 --- a/wrapper.go +++ /dev/null @@ -1,25 +0,0 @@ -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) -- cgit v1.2.3