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
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,25 @@ if err := client.Call(); err != nil {

## Options

### Using `WithMethodNameFormatter`
### Using `WithServerMethodNameFormatter`

`WithServerMethodNameFormatter` allows you to customize a function that formats the JSON-RPC method name, given namespace and method name.

There are four predefined options:
- `jsonrpc.DefaultMethodNameFormatter` - default method name formatter, e.g. `SimpleServerHandler.AddGet`
- `jsonrpc.NewMethodNameFormatter(true, jsonrpc.LowerFirstCharCase)` - method name formatter with namespace, e.g. `SimpleServerHandler.addGet`
- `jsonrpc.NewMethodNameFormatter(false, jsonrpc.OriginalCase)` - method name formatter without namespace, e.g. `AddGet`
- `jsonrpc.NewMethodNameFormatter(false, jsonrpc.LowerFirstCharCase)` - method name formatter without namespace and with the first char lowercased, e.g. `addGet`

> [!NOTE]
> The default method name formatter concatenates the namespace and method name with a dot.
> Go exported methods are capitalized, so, the method name will be capitalized as well.
> e.g. `SimpleServerHandler.AddGet` (capital "A" in "AddGet")

```go
func main() {
// create a new server instance with a custom separator
rpcServer := jsonrpc.NewServer(jsonrpc.WithMethodNameFormatter(
rpcServer := jsonrpc.NewServer(jsonrpc.WithServerMethodNameFormatter(
func(namespace, method string) string {
return namespace + "_" + method
}),
Expand All @@ -273,11 +286,28 @@ func main() {
}
```

### Using `WithMethodNameFormatter`

`WithMethodNameFormatter` is the client-side counterpart to `WithServerMethodNameFormatter`.

```go
func main() {
closer, err := NewMergeClient(
context.Background(),
"http://example.com",
"SimpleServerHandler",
[]any{&client},
nil,
WithMethodNameFormatter(jsonrpc.NewMethodNameFormatter(false, OriginalCase)),
)
defer closer()
}
```

## Contribute

PRs are welcome!

## License

Dual-licensed under [MIT](https://github.com/filecoin-project/go-jsonrpc/blob/master/LICENSE-MIT) + [Apache 2.0](https://github.com/filecoin-project/go-jsonrpc/blob/master/LICENSE-APACHE)
Dual-licensed under [MIT](https://github.com/filecoin-project/go-jsonrpc/blob/master/LICENSE-MIT) + [Apache 2.0](https://github.com/filecoin-project/go-jsonrpc/blob/master/LICENSE-APACHE)
25 changes: 15 additions & 10 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ type client struct {
doRequest func(context.Context, clientRequest) (clientResponse, error)
exiting <-chan struct{}
idCtr int64

methodNameFormatter MethodNameFormatter
}

// NewMergeClient is like NewClient, but allows to specify multiple structs
Expand Down Expand Up @@ -138,9 +140,10 @@ func NewCustomClient(namespace string, outs []interface{}, doRequest func(ctx co
}

c := client{
namespace: namespace,
paramEncoders: config.paramEncoders,
errors: config.errors,
namespace: namespace,
paramEncoders: config.paramEncoders,
errors: config.errors,
methodNameFormatter: config.methodNamer,
}

stop := make(chan struct{})
Expand Down Expand Up @@ -192,9 +195,10 @@ func NewCustomClient(namespace string, outs []interface{}, doRequest func(ctx co

func httpClient(ctx context.Context, addr string, namespace string, outs []interface{}, requestHeader http.Header, config Config) (ClientCloser, error) {
c := client{
namespace: namespace,
paramEncoders: config.paramEncoders,
errors: config.errors,
namespace: namespace,
paramEncoders: config.paramEncoders,
errors: config.errors,
methodNameFormatter: config.methodNamer,
}

stop := make(chan struct{})
Expand Down Expand Up @@ -287,9 +291,10 @@ func websocketClient(ctx context.Context, addr string, namespace string, outs []
}

c := client{
namespace: namespace,
paramEncoders: config.paramEncoders,
errors: config.errors,
namespace: namespace,
paramEncoders: config.paramEncoders,
errors: config.errors,
methodNameFormatter: config.methodNamer,
}

requests := c.setupRequestChan()
Expand Down Expand Up @@ -710,7 +715,7 @@ func (c *client) makeRpcFunc(f reflect.StructField) (reflect.Value, error) {
return reflect.Value{}, xerrors.New("handler field not a func")
}

name := c.namespace + "." + f.Name
name := c.methodNameFormatter(c.namespace, f.Name)
if tag, ok := f.Tag.Lookup(ProxyTagRPCMethod); ok {
name = tag
}
Expand Down
32 changes: 32 additions & 0 deletions method_formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package jsonrpc

import "strings"

// MethodNameFormatter is a function that takes a namespace and a method name and returns the full method name, sent via JSON-RPC.
// This is useful if you want to customize the default behaviour, e.g. send without the namespace or make it lowercase.
type MethodNameFormatter func(namespace, method string) string

// CaseStyle represents the case style for method names.
type CaseStyle int

const (
OriginalCase CaseStyle = iota
LowerFirstCharCase
)

// NewMethodNameFormatter creates a new method name formatter based on the provided options.
func NewMethodNameFormatter(includeNamespace bool, nameCase CaseStyle) MethodNameFormatter {
return func(namespace, method string) string {
formattedMethod := method
if nameCase == LowerFirstCharCase && len(method) > 0 {
formattedMethod = strings.ToLower(method[:1]) + method[1:]
}
if includeNamespace {
return namespace + "." + formattedMethod
}
return formattedMethod
}
}

// DefaultMethodNameFormatter is a pass-through formatter with default options.
var DefaultMethodNameFormatter = NewMethodNameFormatter(true, OriginalCase)
125 changes: 125 additions & 0 deletions method_formatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package jsonrpc

import (
"context"
"fmt"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestDifferentMethodNamers(t *testing.T) {
tests := map[string]struct {
namer MethodNameFormatter

requestedMethod string
}{
"default namer": {
namer: DefaultMethodNameFormatter,
requestedMethod: "SimpleServerHandler.Inc",
},
"lower fist char": {
namer: NewMethodNameFormatter(true, LowerFirstCharCase),
requestedMethod: "SimpleServerHandler.inc",
},
"no namespace namer": {
namer: NewMethodNameFormatter(false, OriginalCase),
requestedMethod: "Inc",
},
"no namespace & lower fist char": {
namer: NewMethodNameFormatter(false, LowerFirstCharCase),
requestedMethod: "inc",
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
rpcServer := NewServer(WithServerMethodNameFormatter(test.namer))

serverHandler := &SimpleServerHandler{}
rpcServer.Register("SimpleServerHandler", serverHandler)

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

req := fmt.Sprintf(`{"jsonrpc": "2.0", "method": "%s", "params": [], "id": 1}`, test.requestedMethod)

res, err := http.Post(testServ.URL, "application/json", strings.NewReader(req))
require.NoError(t, err)

require.Equal(t, http.StatusOK, res.StatusCode)
require.Equal(t, int32(1), serverHandler.n)
})
}
}

func TestDifferentMethodNamersWithClient(t *testing.T) {
tests := map[string]struct {
namer MethodNameFormatter
urlPrefix string
}{
"default namer & http": {
namer: DefaultMethodNameFormatter,
urlPrefix: "http://",
},
"default namer & ws": {
namer: DefaultMethodNameFormatter,
urlPrefix: "ws://",
},
"lower first char namer & http": {
namer: NewMethodNameFormatter(true, LowerFirstCharCase),
urlPrefix: "http://",
},
"lower first char namer & ws": {
namer: NewMethodNameFormatter(true, LowerFirstCharCase),
urlPrefix: "ws://",
},
"no namespace namer & http": {
namer: NewMethodNameFormatter(false, OriginalCase),
urlPrefix: "http://",
},
"no namespace namer & ws": {
namer: NewMethodNameFormatter(false, OriginalCase),
urlPrefix: "ws://",
},
"no namespace & lower first char & http": {
namer: NewMethodNameFormatter(false, LowerFirstCharCase),
urlPrefix: "http://",
},
"no namespace & lower first char & ws": {
namer: NewMethodNameFormatter(false, LowerFirstCharCase),
urlPrefix: "ws://",
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
rpcServer := NewServer(WithServerMethodNameFormatter(test.namer))

serverHandler := &SimpleServerHandler{}
rpcServer.Register("SimpleServerHandler", serverHandler)

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

var client struct {
AddGet func(int) int
}

closer, err := NewMergeClient(
context.Background(),
test.urlPrefix+testServ.Listener.Addr().String(),
"SimpleServerHandler",
[]any{&client},
nil,
WithHTTPClient(testServ.Client()),
WithMethodNameFormatter(test.namer),
)
require.NoError(t, err)
defer closer()

n := client.AddGet(123)
require.Equal(t, 123, n)
})
}
}
10 changes: 10 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type Config struct {

noReconnect bool
proxyConnFactory func(func() (*websocket.Conn, error)) func() (*websocket.Conn, error) // for testing

methodNamer MethodNameFormatter
}

func defaultConfig() Config {
Expand All @@ -46,6 +48,8 @@ func defaultConfig() Config {
paramEncoders: map[reflect.Type]ParamEncoder{},

httpClient: _defaultHTTPClient,

methodNamer: DefaultMethodNameFormatter,
}
}

Expand Down Expand Up @@ -110,3 +114,9 @@ func WithHTTPClient(h *http.Client) func(c *Config) {
c.httpClient = h
}
}

func WithMethodNameFormatter(namer MethodNameFormatter) func(c *Config) {
return func(c *Config) {
c.methodNamer = namer
}
}
15 changes: 6 additions & 9 deletions options_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ type jsonrpcReverseClient struct{ reflect.Type }

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

type MethodNameFormatter func(namespace, method string) string

type ServerConfig struct {
maxRequestSize int64
pingInterval time.Duration
Expand All @@ -34,10 +32,8 @@ func defaultServerConfig() ServerConfig {
paramDecoders: map[reflect.Type]ParamDecoder{},
maxRequestSize: DEFAULT_MAX_REQUEST_SIZE,

pingInterval: 5 * time.Second,
methodNameFormatter: func(namespace, method string) string {
return namespace + "." + method
},
pingInterval: 5 * time.Second,
methodNameFormatter: DefaultMethodNameFormatter,
}
}

Expand Down Expand Up @@ -65,7 +61,7 @@ func WithServerPingInterval(d time.Duration) ServerOption {
}
}

func WithMethodNameFormatter(formatter MethodNameFormatter) ServerOption {
func WithServerMethodNameFormatter(formatter MethodNameFormatter) ServerOption {
return func(c *ServerConfig) {
c.methodNameFormatter = formatter
}
Expand All @@ -85,8 +81,9 @@ func WithReverseClient[RP any](namespace string) ServerOption {
return func(c *ServerConfig) {
c.reverseClientBuilder = func(ctx context.Context, conn *wsConn) (context.Context, error) {
cl := client{
namespace: namespace,
paramEncoders: map[reflect.Type]ParamEncoder{},
namespace: namespace,
paramEncoders: map[reflect.Type]ParamEncoder{},
methodNameFormatter: c.methodNameFormatter,
}

// todo test that everything is closing correctly
Expand Down
2 changes: 1 addition & 1 deletion rpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1719,7 +1719,7 @@ func TestNewCustomClient(t *testing.T) {
func TestReverseCallWithCustomMethodName(t *testing.T) {
// setup server

rpcServer := NewServer(WithMethodNameFormatter(func(namespace, method string) string { return namespace + "_" + method }))
rpcServer := NewServer(WithServerMethodNameFormatter(func(namespace, method string) string { return namespace + "_" + method }))
rpcServer.Register("Server", &RawParamHandler{})

// httptest stuff
Expand Down