Skip to content

Commit e691565

Browse files
chris-4chainrvagg
andauthored
feat: introduce MethodNameFormatter (#130)
* feat: method namer * feat: adjust to MethodNameFormatter concept * feat: NewMethodNameFormatter concept * feat: client instantiation ordering * Update README.md --------- Co-authored-by: Rod Vagg <[email protected]>
1 parent 8e8f524 commit e691565

File tree

7 files changed

+222
-23
lines changed

7 files changed

+222
-23
lines changed

README.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,25 @@ if err := client.Call(); err != nil {
246246

247247
## Options
248248

249-
### Using `WithMethodNameFormatter`
249+
### Using `WithServerMethodNameFormatter`
250+
251+
`WithServerMethodNameFormatter` allows you to customize a function that formats the JSON-RPC method name, given namespace and method name.
252+
253+
There are four possible options:
254+
- `jsonrpc.DefaultMethodNameFormatter` - default method name formatter, e.g. `SimpleServerHandler.AddGet`
255+
- `jsonrpc.NewMethodNameFormatter(true, jsonrpc.LowerFirstCharCase)` - method name formatter with namespace, e.g. `SimpleServerHandler.addGet`
256+
- `jsonrpc.NewMethodNameFormatter(false, jsonrpc.OriginalCase)` - method name formatter without namespace, e.g. `AddGet`
257+
- `jsonrpc.NewMethodNameFormatter(false, jsonrpc.LowerFirstCharCase)` - method name formatter without namespace and with the first char lowercased, e.g. `addGet`
258+
259+
> [!NOTE]
260+
> The default method name formatter concatenates the namespace and method name with a dot.
261+
> Go exported methods are capitalized, so, the method name will be capitalized as well.
262+
> e.g. `SimpleServerHandler.AddGet` (capital "A" in "AddGet")
250263
251264
```go
252265
func main() {
253266
// create a new server instance with a custom separator
254-
rpcServer := jsonrpc.NewServer(jsonrpc.WithMethodNameFormatter(
267+
rpcServer := jsonrpc.NewServer(jsonrpc.WithServerMethodNameFormatter(
255268
func(namespace, method string) string {
256269
return namespace + "_" + method
257270
}),
@@ -273,11 +286,28 @@ func main() {
273286
}
274287
```
275288

289+
### Using `WithMethodNameFormatter`
290+
291+
`WithMethodNameFormatter` is the client-side counterpart to `WithServerMethodNameFormatter`.
292+
293+
```go
294+
func main() {
295+
closer, err := NewMergeClient(
296+
context.Background(),
297+
"http://example.com",
298+
"SimpleServerHandler",
299+
[]any{&client},
300+
nil,
301+
WithMethodNameFormatter(jsonrpc.NewMethodNameFormatter(false, OriginalCase)),
302+
)
303+
defer closer()
304+
}
305+
```
276306

277307
## Contribute
278308

279309
PRs are welcome!
280310

281311
## License
282312

283-
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)
313+
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)

client.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ type client struct {
104104
doRequest func(context.Context, clientRequest) (clientResponse, error)
105105
exiting <-chan struct{}
106106
idCtr int64
107+
108+
methodNameFormatter MethodNameFormatter
107109
}
108110

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

140142
c := client{
141-
namespace: namespace,
142-
paramEncoders: config.paramEncoders,
143-
errors: config.errors,
143+
namespace: namespace,
144+
paramEncoders: config.paramEncoders,
145+
errors: config.errors,
146+
methodNameFormatter: config.methodNamer,
144147
}
145148

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

193196
func httpClient(ctx context.Context, addr string, namespace string, outs []interface{}, requestHeader http.Header, config Config) (ClientCloser, error) {
194197
c := client{
195-
namespace: namespace,
196-
paramEncoders: config.paramEncoders,
197-
errors: config.errors,
198+
namespace: namespace,
199+
paramEncoders: config.paramEncoders,
200+
errors: config.errors,
201+
methodNameFormatter: config.methodNamer,
198202
}
199203

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

289293
c := client{
290-
namespace: namespace,
291-
paramEncoders: config.paramEncoders,
292-
errors: config.errors,
294+
namespace: namespace,
295+
paramEncoders: config.paramEncoders,
296+
errors: config.errors,
297+
methodNameFormatter: config.methodNamer,
293298
}
294299

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

713-
name := c.namespace + "." + f.Name
718+
name := c.methodNameFormatter(c.namespace, f.Name)
714719
if tag, ok := f.Tag.Lookup(ProxyTagRPCMethod); ok {
715720
name = tag
716721
}

method_formatter.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package jsonrpc
2+
3+
import "strings"
4+
5+
// MethodNameFormatter is a function that takes a namespace and a method name and returns the full method name, sent via JSON-RPC.
6+
// This is useful if you want to customize the default behaviour, e.g. send without the namespace or make it lowercase.
7+
type MethodNameFormatter func(namespace, method string) string
8+
9+
// CaseStyle represents the case style for method names.
10+
type CaseStyle int
11+
12+
const (
13+
OriginalCase CaseStyle = iota
14+
LowerFirstCharCase
15+
)
16+
17+
// NewMethodNameFormatter creates a new method name formatter based on the provided options.
18+
func NewMethodNameFormatter(includeNamespace bool, nameCase CaseStyle) MethodNameFormatter {
19+
return func(namespace, method string) string {
20+
formattedMethod := method
21+
if nameCase == LowerFirstCharCase && len(method) > 0 {
22+
formattedMethod = strings.ToLower(method[:1]) + method[1:]
23+
}
24+
if includeNamespace {
25+
return namespace + "." + formattedMethod
26+
}
27+
return formattedMethod
28+
}
29+
}
30+
31+
// DefaultMethodNameFormatter is a pass-through formatter with default options.
32+
var DefaultMethodNameFormatter = NewMethodNameFormatter(true, OriginalCase)

method_formatter_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package jsonrpc
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/stretchr/testify/require"
7+
"net/http"
8+
"net/http/httptest"
9+
"strings"
10+
"testing"
11+
)
12+
13+
func TestDifferentMethodNamers(t *testing.T) {
14+
tests := map[string]struct {
15+
namer MethodNameFormatter
16+
17+
requestedMethod string
18+
}{
19+
"default namer": {
20+
namer: DefaultMethodNameFormatter,
21+
requestedMethod: "SimpleServerHandler.Inc",
22+
},
23+
"lower fist char": {
24+
namer: NewMethodNameFormatter(true, LowerFirstCharCase),
25+
requestedMethod: "SimpleServerHandler.inc",
26+
},
27+
"no namespace namer": {
28+
namer: NewMethodNameFormatter(false, OriginalCase),
29+
requestedMethod: "Inc",
30+
},
31+
"no namespace & lower fist char": {
32+
namer: NewMethodNameFormatter(false, LowerFirstCharCase),
33+
requestedMethod: "inc",
34+
},
35+
}
36+
for name, test := range tests {
37+
t.Run(name, func(t *testing.T) {
38+
rpcServer := NewServer(WithServerMethodNameFormatter(test.namer))
39+
40+
serverHandler := &SimpleServerHandler{}
41+
rpcServer.Register("SimpleServerHandler", serverHandler)
42+
43+
testServ := httptest.NewServer(rpcServer)
44+
defer testServ.Close()
45+
46+
req := fmt.Sprintf(`{"jsonrpc": "2.0", "method": "%s", "params": [], "id": 1}`, test.requestedMethod)
47+
48+
res, err := http.Post(testServ.URL, "application/json", strings.NewReader(req))
49+
require.NoError(t, err)
50+
51+
require.Equal(t, http.StatusOK, res.StatusCode)
52+
require.Equal(t, int32(1), serverHandler.n)
53+
})
54+
}
55+
}
56+
57+
func TestDifferentMethodNamersWithClient(t *testing.T) {
58+
tests := map[string]struct {
59+
namer MethodNameFormatter
60+
urlPrefix string
61+
}{
62+
"default namer & http": {
63+
namer: DefaultMethodNameFormatter,
64+
urlPrefix: "http://",
65+
},
66+
"default namer & ws": {
67+
namer: DefaultMethodNameFormatter,
68+
urlPrefix: "ws://",
69+
},
70+
"lower first char namer & http": {
71+
namer: NewMethodNameFormatter(true, LowerFirstCharCase),
72+
urlPrefix: "http://",
73+
},
74+
"lower first char namer & ws": {
75+
namer: NewMethodNameFormatter(true, LowerFirstCharCase),
76+
urlPrefix: "ws://",
77+
},
78+
"no namespace namer & http": {
79+
namer: NewMethodNameFormatter(false, OriginalCase),
80+
urlPrefix: "http://",
81+
},
82+
"no namespace namer & ws": {
83+
namer: NewMethodNameFormatter(false, OriginalCase),
84+
urlPrefix: "ws://",
85+
},
86+
"no namespace & lower first char & http": {
87+
namer: NewMethodNameFormatter(false, LowerFirstCharCase),
88+
urlPrefix: "http://",
89+
},
90+
"no namespace & lower first char & ws": {
91+
namer: NewMethodNameFormatter(false, LowerFirstCharCase),
92+
urlPrefix: "ws://",
93+
},
94+
}
95+
for name, test := range tests {
96+
t.Run(name, func(t *testing.T) {
97+
rpcServer := NewServer(WithServerMethodNameFormatter(test.namer))
98+
99+
serverHandler := &SimpleServerHandler{}
100+
rpcServer.Register("SimpleServerHandler", serverHandler)
101+
102+
testServ := httptest.NewServer(rpcServer)
103+
defer testServ.Close()
104+
105+
var client struct {
106+
AddGet func(int) int
107+
}
108+
109+
closer, err := NewMergeClient(
110+
context.Background(),
111+
test.urlPrefix+testServ.Listener.Addr().String(),
112+
"SimpleServerHandler",
113+
[]any{&client},
114+
nil,
115+
WithHTTPClient(testServ.Client()),
116+
WithMethodNameFormatter(test.namer),
117+
)
118+
require.NoError(t, err)
119+
defer closer()
120+
121+
n := client.AddGet(123)
122+
require.Equal(t, 123, n)
123+
})
124+
}
125+
}

options.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type Config struct {
3030

3131
noReconnect bool
3232
proxyConnFactory func(func() (*websocket.Conn, error)) func() (*websocket.Conn, error) // for testing
33+
34+
methodNamer MethodNameFormatter
3335
}
3436

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

4850
httpClient: _defaultHTTPClient,
51+
52+
methodNamer: DefaultMethodNameFormatter,
4953
}
5054
}
5155

@@ -110,3 +114,9 @@ func WithHTTPClient(h *http.Client) func(c *Config) {
110114
c.httpClient = h
111115
}
112116
}
117+
118+
func WithMethodNameFormatter(namer MethodNameFormatter) func(c *Config) {
119+
return func(c *Config) {
120+
c.methodNamer = namer
121+
}
122+
}

options_server.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ type jsonrpcReverseClient struct{ reflect.Type }
1313

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

16-
type MethodNameFormatter func(namespace, method string) string
17-
1816
type ServerConfig struct {
1917
maxRequestSize int64
2018
pingInterval time.Duration
@@ -34,10 +32,8 @@ func defaultServerConfig() ServerConfig {
3432
paramDecoders: map[reflect.Type]ParamDecoder{},
3533
maxRequestSize: DEFAULT_MAX_REQUEST_SIZE,
3634

37-
pingInterval: 5 * time.Second,
38-
methodNameFormatter: func(namespace, method string) string {
39-
return namespace + "." + method
40-
},
35+
pingInterval: 5 * time.Second,
36+
methodNameFormatter: DefaultMethodNameFormatter,
4137
}
4238
}
4339

@@ -65,7 +61,7 @@ func WithServerPingInterval(d time.Duration) ServerOption {
6561
}
6662
}
6763

68-
func WithMethodNameFormatter(formatter MethodNameFormatter) ServerOption {
64+
func WithServerMethodNameFormatter(formatter MethodNameFormatter) ServerOption {
6965
return func(c *ServerConfig) {
7066
c.methodNameFormatter = formatter
7167
}
@@ -85,8 +81,9 @@ func WithReverseClient[RP any](namespace string) ServerOption {
8581
return func(c *ServerConfig) {
8682
c.reverseClientBuilder = func(ctx context.Context, conn *wsConn) (context.Context, error) {
8783
cl := client{
88-
namespace: namespace,
89-
paramEncoders: map[reflect.Type]ParamEncoder{},
84+
namespace: namespace,
85+
paramEncoders: map[reflect.Type]ParamEncoder{},
86+
methodNameFormatter: c.methodNameFormatter,
9087
}
9188

9289
// todo test that everything is closing correctly

rpc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1719,7 +1719,7 @@ func TestNewCustomClient(t *testing.T) {
17191719
func TestReverseCallWithCustomMethodName(t *testing.T) {
17201720
// setup server
17211721

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

17251725
// httptest stuff

0 commit comments

Comments
 (0)