diff options
Diffstat (limited to 'rpc')
-rw-r--r-- | rpc/errors.go | 42 | ||||
-rw-r--r-- | rpc/logger.go | 20 | ||||
-rw-r--r-- | rpc/server.go | 127 | ||||
-rw-r--r-- | rpc/wrapper.go | 25 |
4 files changed, 214 insertions, 0 deletions
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) |