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/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/errors.go b/rpc/errors.go similarity index 91% rename from errors.go rename to rpc/errors.go index cd87fbb..1af84ee 100644 --- a/errors.go +++ b/rpc/errors.go @@ -1,4 +1,4 @@ -package jsonrpc2 +package rpc import "fmt" @@ -20,7 +20,7 @@ var errorMap = map[int]string{ -32000: "Other error", } -//-32000 to -32099 Server error Reserved for implementation-defined server-errors. +//-32000 to -32099 RpcServer error Reserved for implementation-defined server-errors. type Error struct { Code int `json:"code"` diff --git a/logger.go b/rpc/logger.go similarity index 94% rename from logger.go rename to rpc/logger.go index 7907e4f..25fe2d6 100644 --- a/logger.go +++ b/rpc/logger.go @@ -1,4 +1,4 @@ -package jsonrpc2 +package rpc import "log" diff --git a/server.go b/rpc/server.go similarity index 61% rename from server.go rename to rpc/server.go index 5625f78..12f07e8 100644 --- a/server.go +++ b/rpc/server.go @@ -1,42 +1,23 @@ -package jsonrpc2 +package rpc import ( - "bufio" "context" "encoding/json" "io" - "net/http" "sync" ) const version = "2.0" -type Server struct { +type RpcServer 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{ +func New() *RpcServer { + return &RpcServer{ Logger: nopLogger{}, IgnoreNotifications: true, handlers: map[string]Handler{}, @@ -44,36 +25,36 @@ func New() *Server { } } -func (r *Server) Register(method string, handler Handler) { +func (r *RpcServer) 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) { +func (r *RpcServer) SingleRequest(ctx context.Context, reader io.Reader, writer io.Writer) { req := new(rpcRequest) - if err := json.NewDecoder(buf).Decode(req); err != nil { + if err := json.NewDecoder(reader).Decode(req); err != nil { r.Logger.Logf("Can't read body: %v", err) - writeError(ErrCodeParseError, writer) + WriteError(ErrCodeParseError, writer) return } - resp := r.callMethod(request.Context(), req) + 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) + WriteError(ErrCodeInternalError, writer) return } } -func (r *Server) batchRequest(writer http.ResponseWriter, request *http.Request, buf *bufio.Reader) { +func (r *RpcServer) BatchRequest(ctx context.Context, reader io.Reader, writer io.Writer) { var req []rpcRequest - if err := json.NewDecoder(buf).Decode(&req); err != nil { + if err := json.NewDecoder(reader).Decode(&req); err != nil { r.Logger.Logf("Can't read body: %v", err) - writeError(ErrCodeParseError, writer) + WriteError(ErrCodeParseError, writer) return } var responses []*rpcResponse @@ -82,7 +63,7 @@ func (r *Server) batchRequest(writer http.ResponseWriter, request *http.Request, for _, j := range req { go func(req rpcRequest) { defer wg.Done() - resp := r.callMethod(request.Context(), &req) + resp := r.callMethod(ctx, &req) if req.Id == nil && r.IgnoreNotifications { // notification request return @@ -93,11 +74,11 @@ func (r *Server) batchRequest(writer http.ResponseWriter, request *http.Request, wg.Wait() if err := json.NewEncoder(writer).Encode(responses); err != nil { r.Logger.Logf("Can't write response: %v", err) - writeError(ErrCodeInternalError, writer) + WriteError(ErrCodeInternalError, writer) } } -func (r *Server) callMethod(ctx context.Context, req *rpcRequest) *rpcResponse { +func (r *RpcServer) callMethod(ctx context.Context, req *rpcRequest) *rpcResponse { r.mu.RLock() h, ok := r.handlers[req.Method] r.mu.RUnlock() @@ -124,7 +105,7 @@ func (r *Server) callMethod(ctx context.Context, req *rpcRequest) *rpcResponse { } } -func writeError(code int, w io.Writer) { +func WriteError(code int, w io.Writer) { _ = json.NewEncoder(w).Encode(rpcResponse{ Jsonrpc: version, Error: NewError(code), diff --git a/wrapper.go b/rpc/wrapper.go similarity index 97% rename from wrapper.go rename to rpc/wrapper.go index 8058a27..0bc0a5c 100644 --- a/wrapper.go +++ b/rpc/wrapper.go @@ -1,4 +1,4 @@ -package jsonrpc2 +package rpc import ( "context"