aboutsummaryrefslogtreecommitdiff
path: root/rpc
diff options
context:
space:
mode:
authorAlexander Kiryukhin <a.kiryukhin@mail.ru>2022-05-28 16:53:20 +0300
committerAlexander Kiryukhin <a.kiryukhin@mail.ru>2022-05-28 16:53:20 +0300
commitf304a07a8cfe67b2a65f95f27eb10a9b854c4ef8 (patch)
tree9e6a7e9ea6b8d600cccac5a5d50f3232d631d073 /rpc
parent281eda83c9f4bcb06318444df3574df0840205fa (diff)
Improved middlewares
Diffstat (limited to 'rpc')
-rw-r--r--rpc/contract.go46
-rw-r--r--rpc/errors.go12
-rw-r--r--rpc/middleware.go21
-rw-r--r--rpc/middleware/logger.go41
-rw-r--r--rpc/middleware/validation.go72
-rw-r--r--rpc/options.go4
-rw-r--r--rpc/server.go61
-rw-r--r--rpc/wrapper.go4
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)