diff options
Diffstat (limited to 'rpc')
-rw-r--r-- | rpc/contract.go | 46 | ||||
-rw-r--r-- | rpc/errors.go | 12 | ||||
-rw-r--r-- | rpc/middleware.go | 21 | ||||
-rw-r--r-- | rpc/middleware/logger.go | 41 | ||||
-rw-r--r-- | rpc/middleware/validation.go | 72 | ||||
-rw-r--r-- | rpc/options.go | 4 | ||||
-rw-r--r-- | rpc/server.go | 61 | ||||
-rw-r--r-- | rpc/wrapper.go | 4 |
8 files changed, 191 insertions, 70 deletions
diff --git a/rpc/contract.go b/rpc/contract.go new file mode 100644 index 0000000..aa1f194 --- /dev/null +++ b/rpc/contract.go @@ -0,0 +1,46 @@ +//Package rpc provides abstract rpc server +// +//Copyright (C) 2022 Alexander Kiryukhin <i@neonxp.dev> +// +//This file is part of go.neonxp.dev/jsonrpc2 project. +// +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//This program is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this program. If not, see <https://www.gnu.org/licenses/>. + +package rpc + +import ( + "context" + "encoding/json" +) + +type RpcHandler func(ctx context.Context, req *RpcRequest) *RpcResponse + +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"` +} + +type Flusher interface { + // Flush sends any buffered data to the client. + Flush() +} diff --git a/rpc/errors.go b/rpc/errors.go index 71a7168..f6d2f49 100644 --- a/rpc/errors.go +++ b/rpc/errors.go @@ -31,12 +31,12 @@ const ( ) 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", + ErrCodeParseError: "Parse error", // Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. + ErrCodeInvalidRequest: "Invalid Request", // The JSON sent is not a valid Request object. + ErrCodeMethodNotFound: "Method not found", // The method does not exist / is not available. + ErrCodeInvalidParams: "Invalid params", // Invalid method parameter(s). + ErrCodeInternalError: "Internal error", // Internal JSON-RPC error. + ErrUser: "Other error", } //-32000 to -32099 RpcServer error Reserved for implementation-defined server-errors. diff --git a/rpc/middleware.go b/rpc/middleware.go index cd99823..3887109 100644 --- a/rpc/middleware.go +++ b/rpc/middleware.go @@ -19,25 +19,4 @@ package rpc -import ( - "context" - "strings" - "time" -) - type Middleware func(handler RpcHandler) RpcHandler - -type RpcHandler func(ctx context.Context, req *RpcRequest) *RpcResponse - -func LoggerMiddleware(logger Logger) Middleware { - return func(handler RpcHandler) RpcHandler { - return func(ctx context.Context, req *RpcRequest) *RpcResponse { - t1 := time.Now().UnixMicro() - resp := handler(ctx, req) - t2 := time.Now().UnixMicro() - args := strings.ReplaceAll(string(req.Params), "\n", "") - logger.Logf("rpc call=%s, args=%s, take=%dμs", req.Method, args, (t2 - t1)) - return resp - } - } -} diff --git a/rpc/middleware/logger.go b/rpc/middleware/logger.go new file mode 100644 index 0000000..dbf5a4d --- /dev/null +++ b/rpc/middleware/logger.go @@ -0,0 +1,41 @@ +//Package middleware provides middlewares for rpc server +// +//Copyright (C) 2022 Alexander Kiryukhin <i@neonxp.dev> +// +//This file is part of go.neonxp.dev/jsonrpc2 project. +// +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//This program is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this program. If not, see <https://www.gnu.org/licenses/>. + +package middleware + +import ( + "context" + "strings" + "time" + + "go.neonxp.dev/jsonrpc2/rpc" +) + +func Logger(logger rpc.Logger) rpc.Middleware { + return func(handler rpc.RpcHandler) rpc.RpcHandler { + return func(ctx context.Context, req *rpc.RpcRequest) *rpc.RpcResponse { + t1 := time.Now().UnixMicro() + resp := handler(ctx, req) + t2 := time.Now().UnixMicro() + args := strings.ReplaceAll(string(req.Params), "\n", "") + logger.Logf("rpc call=%s, args=%s, take=%dμs", req.Method, args, (t2 - t1)) + return resp + } + } +} diff --git a/rpc/middleware/validation.go b/rpc/middleware/validation.go new file mode 100644 index 0000000..e994383 --- /dev/null +++ b/rpc/middleware/validation.go @@ -0,0 +1,72 @@ +//Package middleware provides middlewares for rpc server +// +//Copyright (C) 2022 Alexander Kiryukhin <i@neonxp.dev> +// +//This file is part of go.neonxp.dev/jsonrpc2 project. +// +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//This program is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this program. If not, see <https://www.gnu.org/licenses/>. + +package middleware + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/qri-io/jsonschema" + + "go.neonxp.dev/jsonrpc2/rpc" +) + +type MethodSchema struct { + Request jsonschema.Schema + Response jsonschema.Schema +} + +func Validation(serviceSchema map[string]MethodSchema) (rpc.Middleware, error) { + return func(handler rpc.RpcHandler) rpc.RpcHandler { + return func(ctx context.Context, req *rpc.RpcRequest) *rpc.RpcResponse { + if rs, ok := serviceSchema[strings.ToLower(req.Method)]; ok { + if errResp := formatError(ctx, req.Id, rs.Request, req.Params); errResp != nil { + return errResp + } + resp := handler(ctx, req) + if errResp := formatError(ctx, req.Id, rs.Response, resp.Result); errResp != nil { + return errResp + } + return resp + } + return handler(ctx, req) + } + }, nil +} + +func formatError(ctx context.Context, requestId any, schema jsonschema.Schema, data json.RawMessage) *rpc.RpcResponse { + errs, err := schema.ValidateBytes(ctx, data) + if err != nil { + return rpc.ErrorResponse(requestId, err) + } + if errs != nil && len(errs) > 0 { + messages := []string{} + for _, msg := range errs { + messages = append(messages, fmt.Sprintf("%s: %s", msg.PropertyPath, msg.Message)) + } + return rpc.ErrorResponse(requestId, rpc.Error{ + Code: rpc.ErrCodeInvalidParams, + Message: strings.Join(messages, "\n"), + }) + } + return nil +} diff --git a/rpc/options.go b/rpc/options.go index 825dbca..683df66 100644 --- a/rpc/options.go +++ b/rpc/options.go @@ -19,7 +19,9 @@ package rpc -import "go.neonxp.dev/jsonrpc2/transport" +import ( + "go.neonxp.dev/jsonrpc2/transport" +) type Option func(s *RpcServer) diff --git a/rpc/server.go b/rpc/server.go index f39bdaa..3c9410a 100644 --- a/rpc/server.go +++ b/rpc/server.go @@ -23,6 +23,7 @@ import ( "context" "encoding/json" "io" + "strings" "sync" "golang.org/x/sync/errgroup" @@ -34,7 +35,7 @@ const version = "2.0" type RpcServer struct { logger Logger - handlers map[string]Handler + handlers map[string]HandlerFunc middlewares []Middleware transports []transport.Transport mu sync.RWMutex @@ -43,7 +44,7 @@ type RpcServer struct { func New(opts ...Option) *RpcServer { s := &RpcServer{ logger: nopLogger{}, - handlers: map[string]Handler{}, + handlers: map[string]HandlerFunc{}, transports: []transport.Transport{}, mu: sync.RWMutex{}, } @@ -59,10 +60,11 @@ func (r *RpcServer) Use(opts ...Option) { } } -func (r *RpcServer) Register(method string, handler Handler) { +func (r *RpcServer) Register(method string, handler HandlerFunc) { r.mu.Lock() defer r.mu.Unlock() - r.handlers[method] = handler + r.logger.Logf("Register method %s", method) + r.handlers[strings.ToLower(method)] = handler } func (r *RpcServer) Run(ctx context.Context) error { @@ -99,7 +101,7 @@ func (r *RpcServer) Resolve(ctx context.Context, rd io.Reader, w io.Writer, para defer mu.Unlock() if err := enc.Encode(resp); err != nil { r.logger.Logf("Can't write response: %v", err) - WriteError(ErrCodeInternalError, enc) + enc.Encode(ErrorResponse(req.Id, ErrorFromCode(ErrCodeInternalError))) } if w, canFlush := w.(Flusher); canFlush { w.Flush() @@ -122,53 +124,32 @@ func (r *RpcServer) Resolve(ctx context.Context, rd io.Reader, w io.Writer, para func (r *RpcServer) callMethod(ctx context.Context, req *RpcRequest) *RpcResponse { r.mu.RLock() - h, ok := r.handlers[req.Method] + h, ok := r.handlers[strings.ToLower(req.Method)] r.mu.RUnlock() if !ok { - return &RpcResponse{ - Jsonrpc: version, - Error: ErrorFromCode(ErrCodeMethodNotFound), - Id: req.Id, - } + return ErrorResponse(req.Id, ErrorFromCode(ErrCodeMethodNotFound)) } 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 ErrorResponse(req.Id, err) } + + return ResultResponse(req.Id, resp) +} + +func ResultResponse(id any, resp json.RawMessage) *RpcResponse { return &RpcResponse{ Jsonrpc: version, Result: resp, - Id: req.Id, + Id: id, } } -func WriteError(code int, enc *json.Encoder) { - enc.Encode(RpcResponse{ +func ErrorResponse(id any, err error) *RpcResponse { + return &RpcResponse{ Jsonrpc: version, - Error: ErrorFromCode(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"` -} - -type Flusher interface { - // Flush sends any buffered data to the client. - Flush() + Error: err, + Id: id, + } } diff --git a/rpc/wrapper.go b/rpc/wrapper.go index 1d6361c..8aa9556 100644 --- a/rpc/wrapper.go +++ b/rpc/wrapper.go @@ -24,7 +24,7 @@ import ( "encoding/json" ) -func H[RQ any, RS any](handler func(context.Context, *RQ) (RS, error)) Handler { +func H[RQ any, RS any](handler func(context.Context, *RQ) (RS, error)) HandlerFunc { return func(ctx context.Context, in json.RawMessage) (json.RawMessage, error) { req := new(RQ) if err := json.Unmarshal(in, req); err != nil { @@ -41,4 +41,4 @@ func H[RQ any, RS any](handler func(context.Context, *RQ) (RS, error)) Handler { } } -type Handler func(context.Context, json.RawMessage) (json.RawMessage, error) +type HandlerFunc func(context.Context, json.RawMessage) (json.RawMessage, error) |