Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,24 @@ 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 three predefined formatters:
- `jsonrpc.DefaultMethodNameFormatter` - default method name formatter, e.g. `SimpleServerHandler.AddGet`
- `jsonrpc.NoNamespaceMethodNameFormatter` - method name formatter without namespace, e.g. `AddGet`
- `jsonrpc.NoNamespaceDecapitalizedMethodNameFormatter` - method name formatter without namespace and decapitalized, e.g. `addGet`
Copy link
Member

Choose a reason for hiding this comment

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

I don't love this name, but NoNamespaceLowerCamelCaseMethodNameFormatter would be getting a bit out of hand I think

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✅ I've adjusted it according to your idea with NewMethodNameFormatter


> [!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 +285,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(test.namer),
)
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,
methodNameFormatter: config.methodNamer,
paramEncoders: config.paramEncoders,
errors: config.errors,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
methodNameFormatter: config.methodNamer,
paramEncoders: config.paramEncoders,
errors: config.errors,
paramEncoders: config.paramEncoders,
errors: config.errors,
methodNameFormatter: config.methodNamer,

retain ordering of the fields as defined

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✅ Done

}

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,
methodNameFormatter: config.methodNamer,
Copy link
Member

Choose a reason for hiding this comment

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

ditto here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✅ Done

paramEncoders: config.paramEncoders,
errors: config.errors,
}

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,
methodNameFormatter: config.methodNamer,
Copy link
Member

Choose a reason for hiding this comment

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

ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✅ Done

paramEncoders: config.paramEncoders,
errors: config.errors,
}

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
26 changes: 26 additions & 0 deletions method_formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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

// DefaultMethodNameFormatter joins the namespace and method name with a dot.
func DefaultMethodNameFormatter(namespace, method string) string {
return namespace + "." + method
}

// NoNamespaceMethodNameFormatter returns the method name as is, without the namespace.
func NoNamespaceMethodNameFormatter(_, method string) string {
return method
}

// NoNamespaceDecapitalizedMethodNameFormatter returns the method name as is, without the namespace, and decapitalizes the first letter.
// e.g. "Inc" -> "inc"
func NoNamespaceDecapitalizedMethodNameFormatter(_, method string) string {
if len(method) == 0 {
return ""
}
return strings.ToLower(method[:1]) + method[1:]
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// DefaultMethodNameFormatter joins the namespace and method name with a dot.
func DefaultMethodNameFormatter(namespace, method string) string {
return namespace + "." + method
}
// NoNamespaceMethodNameFormatter returns the method name as is, without the namespace.
func NoNamespaceMethodNameFormatter(_, method string) string {
return method
}
// NoNamespaceDecapitalizedMethodNameFormatter returns the method name as is, without the namespace, and decapitalizes the first letter.
// e.g. "Inc" -> "inc"
func NoNamespaceDecapitalizedMethodNameFormatter(_, method string) string {
if len(method) == 0 {
return ""
}
return strings.ToLower(method[:1]) + method[1:]
}
// 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)

How about something like this to solve the weird naming problem?

Copy link
Contributor Author

@chris-4chain chris-4chain Apr 1, 2025

Choose a reason for hiding this comment

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

Good idea 💯

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✅ Done

113 changes: 113 additions & 0 deletions method_formatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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",
},
"no namespace namer": {
namer: NoNamespaceMethodNameFormatter,
requestedMethod: "Inc",
},
"no namespace & decapitalized namer": {
namer: NoNamespaceDecapitalizedMethodNameFormatter,
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://",
},
"no namespace namer & http": {
namer: NoNamespaceMethodNameFormatter,
urlPrefix: "http://",
},
"no namespace namer & ws": {
namer: NoNamespaceMethodNameFormatter,
urlPrefix: "ws://",
},
"no namespace & decapitalized namer & http": {
namer: NoNamespaceDecapitalizedMethodNameFormatter,
urlPrefix: "http://",
},
"no namespace & decapitalized namer & ws": {
namer: NoNamespaceDecapitalizedMethodNameFormatter,
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,
methodNameFormatter: c.methodNameFormatter,
paramEncoders: map[reflect.Type]ParamEncoder{},
}

// 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