Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea/
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,53 @@ if err := client.Call(); err != nil {
}
```

## Options


### Using `WithNamespaceSeparator`
```go
func main() {
// create a new server instance with a custom namespace separator
rpcServer := jsonrpc.NewServer(jsonrpc.WithNamespaceSeparator("_"))

// create a handler instance and register it
serverHandler := &SimpleServerHandler{}
rpcServer.Register("SimpleServerHandler", serverHandler)

// serve the api
testServ := httptest.NewServer(rpcServer)
defer testServ.Close()

fmt.Println("URL: ", "ws://"+testServ.Listener.Addr().String())

// rpc method becomes SimpleServerHandler_AddGet

[..do other app stuff / wait..]
}
```

### Using `WithMethodNameTransformer`
```go
func main() {
// create a new server instance with a custom method transformer
rpcServer := jsonrpc.NewServer(jsonrpc.WithMethodNameTransformer(toSnakeCase))

// create a handler instance and register it
serverHandler := &SimpleServerHandler{}
rpcServer.Register("SimpleServerHandler", serverHandler)

// serve the api
testServ := httptest.NewServer(rpcServer)
defer testServ.Close()

fmt.Println("URL: ", "ws://"+testServ.Listener.Addr().String())

// rpc method becomes SimpleServerHandler.add_get

[..do other app stuff / wait..]
}
```

## Contribute

PRs are welcome!
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/filecoin-project/go-jsonrpc

go 1.23
go 1.23.0

require (
github.com/google/uuid v1.1.1
Expand Down
24 changes: 20 additions & 4 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ type handler struct {

paramDecoders map[reflect.Type]ParamDecoder

tracer Tracer
methodNameTransformer MethodNameTransformer

tracer Tracer
separator string
}

type Tracer func(method string, params []reflect.Value, results []reflect.Value, err error)
Expand All @@ -90,9 +93,12 @@ func makeHandler(sc ServerConfig) *handler {
aliasedMethods: map[string]string{},
paramDecoders: sc.paramDecoders,

methodNameTransformer: sc.methodNameTransformer,

maxRequestSize: sc.maxRequestSize,

tracer: sc.tracer,
tracer: sc.tracer,
separator: sc.separator,
}
}

Expand Down Expand Up @@ -125,8 +131,11 @@ func (s *handler) register(namespace string, r interface{}) {
}

valOut, errOut, _ := processFuncOut(funcType)
if s.methodNameTransformer != nil {
method.Name = s.methodNameTransformer(method.Name)
}

s.methods[namespace+"."+method.Name] = methodHandler{
s.methods[namespace+s.separator+method.Name] = methodHandler{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about letting the user control everything with a "method name formatter". I.e.:

  1. Define a WithMethodNameFormatter option that takes a func(namespace, method string) string.
  2. Default to func(namespace, method) string { return namespace + "." + name }.

That way the user can combine the method name and namespace in any way they want.

Also, please don't modify method.Name (it's probably safe, but I would avoid it).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g., in case the user wants something like method.namepace or /namespace/method etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s.methods[s.methodNameFormatter(namespace, method.Name)] = methodHandler{
			paramReceivers: recvs,
			nParams:        ins,

			handlerFunc: method.Func,
			receiver:    val,

			hasCtx:       hasCtx,
			hasRawParams: hasRawParams,

			errOut: errOut,
			valOut: valOut,
		}

add a formatter handler now and defaults to namespace.method

paramReceivers: recvs,
nParams: ins,

Expand Down Expand Up @@ -303,7 +312,14 @@ func (s *handler) createError(err error) *JSONRPCError {
return out
}

func (s *handler) handle(ctx context.Context, req request, w func(func(io.Writer)), rpcError rpcErrFunc, done func(keepCtx bool), chOut chanOut) {
func (s *handler) handle(
ctx context.Context,
req request,
w func(func(io.Writer)),
rpcError rpcErrFunc,
done func(keepCtx bool),
chOut chanOut,
) {
// Not sure if we need to sanitize the incoming req.Method or not.
ctx, span := s.getSpan(ctx, req)
ctx, _ = tag.New(ctx, tag.Insert(metrics.RPCMethod, req.Method))
Expand Down
23 changes: 21 additions & 2 deletions options_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@ type jsonrpcReverseClient struct{ reflect.Type }

type ParamDecoder func(ctx context.Context, json []byte) (reflect.Value, error)

type MethodNameTransformer func(string) string

const defaultSeparator = "."

type ServerConfig struct {
maxRequestSize int64
pingInterval time.Duration

paramDecoders map[reflect.Type]ParamDecoder
errors *Errors

reverseClientBuilder func(context.Context, *wsConn) (context.Context, error)
tracer Tracer
reverseClientBuilder func(context.Context, *wsConn) (context.Context, error)
tracer Tracer
methodNameTransformer MethodNameTransformer
separator string
}

type ServerOption func(c *ServerConfig)
Expand All @@ -32,6 +38,7 @@ func defaultServerConfig() ServerConfig {
maxRequestSize: DEFAULT_MAX_REQUEST_SIZE,

pingInterval: 5 * time.Second,
separator: defaultSeparator,
}
}

Expand Down Expand Up @@ -59,6 +66,18 @@ func WithServerPingInterval(d time.Duration) ServerOption {
}
}

func WithNamespaceSeparator(separator string) ServerOption {
return func(c *ServerConfig) {
c.separator = separator
}
}

func WithMethodNameTransformer(methodNameTransformer MethodNameTransformer) ServerOption {
return func(c *ServerConfig) {
c.methodNameTransformer = methodNameTransformer
}
}

// WithTracer allows the instantiator to trace the method calls and results.
// This is useful for debugging a client-server interaction.
func WithTracer(l Tracer) ServerOption {
Expand Down
75 changes: 74 additions & 1 deletion rpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/http/httptest"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -1248,7 +1249,11 @@ func TestIDHandling(t *testing.T) {
expect interface{}
expectErr bool
}{
{`{"id":"8116d306-56cc-4637-9dd7-39ce1548a5a0","jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, "8116d306-56cc-4637-9dd7-39ce1548a5a0", false},
{
`{"id":"8116d306-56cc-4637-9dd7-39ce1548a5a0","jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`,
"8116d306-56cc-4637-9dd7-39ce1548a5a0",
false,
},
{`{"id":1234,"jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, float64(1234), false},
{`{"id":null,"jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, nil, false},
{`{"id":1234.0,"jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, 1234.0, false},
Expand Down Expand Up @@ -1711,3 +1716,71 @@ func TestNewCustomClient(t *testing.T) {
require.Equal(t, 13, n)
require.Equal(t, int32(13), serverHandler.n)
}

func TestReverseCallWithCustomSeparator(t *testing.T) {
// setup server

rpcServer := NewServer(WithNamespaceSeparator("_"))
rpcServer.Register("Server", &RawParamHandler{})

// httptest stuff
testServ := httptest.NewServer(rpcServer)
defer testServ.Close()

// setup client

var client struct {
Call func(ctx context.Context, ps RawParams) error `rpc_method:"Server_Call"`
}
closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "Server", []interface{}{
&client,
}, nil)
require.NoError(t, err)

// do the call!

e := client.Call(context.Background(), []byte(`{"I": 1}`))
require.NoError(t, e)

closer()
}

type MethodTransformedHandler struct{}

func (h *RawParamHandler) CallSomethingInSnakeCase(ctx context.Context, v int) (int, error) {
return v + 1, nil
}

func TestCallWithMethodTransformer(t *testing.T) {
// setup server

var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")

rpcServer := NewServer(WithMethodNameTransformer(func(method string) string {
snake := matchFirstCap.ReplaceAllString(method, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
return strings.ToLower(snake)
}))
rpcServer.Register("Raw", &RawParamHandler{})

// httptest stuff
testServ := httptest.NewServer(rpcServer)
defer testServ.Close()

// setup client
var client struct {
Call func(ctx context.Context, v int) (int, error) `rpc_method:"Raw.call_something_in_snake_case"`
}
closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "Raw", []interface{}{
&client,
}, nil)
require.NoError(t, err)

// this will block if it's not sent as a notification
n, err := client.Call(context.Background(), 6)
require.NoError(t, err)
require.Equal(t, 7, n)

closer()
}