From fefaab160207f0505bdf60a4f5e0f83427b2efc6 Mon Sep 17 00:00:00 2001 From: thiagodeev Date: Tue, 19 Aug 2025 14:06:43 -0300 Subject: [PATCH 1/4] feat: add reverseHandlersFormatter to Config and update websocketClient to use it --- client.go | 6 +++++- options.go | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 69a9cfe..286e229 100644 --- a/client.go +++ b/client.go @@ -305,7 +305,11 @@ func websocketClient(ctx context.Context, addr string, namespace string, outs [] var hnd reqestHandler if len(config.reverseHandlers) > 0 { - h := makeHandler(defaultServerConfig()) + sc := defaultServerConfig() + if config.reverseHandlersFormatter != nil { + sc.methodNameFormatter = config.reverseHandlersFormatter + } + h := makeHandler(sc) h.aliasedMethods = config.aliasedHandlerMethods for _, reverseHandler := range config.reverseHandlers { h.register(reverseHandler.ns, reverseHandler.hnd) diff --git a/options.go b/options.go index 8b4d0e9..e2837ee 100644 --- a/options.go +++ b/options.go @@ -23,8 +23,9 @@ type Config struct { paramEncoders map[reflect.Type]ParamEncoder errors *Errors - reverseHandlers []clientHandler - aliasedHandlerMethods map[string]string + reverseHandlers []clientHandler + reverseHandlersFormatter MethodNameFormatter + aliasedHandlerMethods map[string]string httpClient *http.Client @@ -101,6 +102,13 @@ func WithClientHandler(ns string, hnd interface{}) func(c *Config) { } } +// Just like WithMethodNameFormatter, but for client handlers. +func WithClientHandlerFormatter(namer MethodNameFormatter) func(c *Config) { + return func(c *Config) { + c.reverseHandlersFormatter = namer + } +} + // WithClientHandlerAlias creates an alias for a client HANDLER method - for handlers created // with WithClientHandler func WithClientHandlerAlias(alias, original string) func(c *Config) { From 6cf87e3e8cb3f993d5853cc55d0d07feaa3d6507 Mon Sep 17 00:00:00 2001 From: thiagodeev Date: Wed, 20 Aug 2025 21:18:46 -0300 Subject: [PATCH 2/4] test: add TestDifferentMethodNamersWithClientHandler to validate method name formatting with various configurations --- method_formatter_test.go | 53 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/method_formatter_test.go b/method_formatter_test.go index d003ddd..a5648d2 100644 --- a/method_formatter_test.go +++ b/method_formatter_test.go @@ -3,11 +3,12 @@ package jsonrpc import ( "context" "fmt" - "github.com/stretchr/testify/require" "net/http" "net/http/httptest" "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestDifferentMethodNamers(t *testing.T) { @@ -123,3 +124,53 @@ func TestDifferentMethodNamersWithClient(t *testing.T) { }) } } + +func TestDifferentMethodNamersWithClientHandler(t *testing.T) { + tests := map[string]struct { + namer MethodNameFormatter + }{ + "default namer & ws": { + namer: DefaultMethodNameFormatter, + }, + "lower first char namer & ws": { + namer: NewMethodNameFormatter(true, LowerFirstCharCase), + }, + "no namespace namer & ws": { + namer: NewMethodNameFormatter(false, OriginalCase), + }, + "no namespace & lower first char & ws": { + namer: NewMethodNameFormatter(false, LowerFirstCharCase), + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + rpcServer := NewServer(WithReverseClient[RevCallTestClientProxy]("Client"), WithServerMethodNameFormatter(test.namer)) + rpcServer.Register("Server", &RevCallTestServerHandler{}) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + // setup client + + var client struct { + Call func() error + } + + closer, err := NewMergeClient( + context.Background(), + "ws://"+testServ.Listener.Addr().String(), + "Server", + []any{&client}, + nil, + WithMethodNameFormatter(test.namer), + WithClientHandler("Client", &RevCallTestClientHandler{}), + WithClientHandlerFormatter(test.namer), + ) + require.NoError(t, err) + defer closer() + + e := client.Call() + require.NoError(t, e) + }) + } +} From b710b309f35823d0c61b714aaa3b9839b6013b58 Mon Sep 17 00:00:00 2001 From: thiagodeev Date: Wed, 20 Aug 2025 23:53:08 -0300 Subject: [PATCH 3/4] docs: update README.md to enhance reverse calling feature description and add method name formatter options --- README.md | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 03eff66..338ae1a 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ fmt.Printf("Current value: %d\n", client.AddGet(5)) ``` ### Reverse Calling Feature -The go-jsonrpc library also supports reverse calling, where the server can make calls to the client. This is useful in scenarios where the server needs to notify or request data from the client. +The go-jsonrpc library also supports reverse calling, where the server can make calls to the client. This is useful in scenarios where the server needs to notify, request data from the client, or for subscriptions (e.g. `eth_subscribe`). NOTE: Reverse calling only works in websocket mode @@ -246,11 +246,13 @@ if err := client.Call(); err != nil { ## Options -### Using `WithServerMethodNameFormatter` +### Using method name formatters + +#### Using `WithServerMethodNameFormatter` `WithServerMethodNameFormatter` allows you to customize a function that formats the JSON-RPC method name, given namespace and method name. -There are four possible options: +There are four possible out-of-the-box 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` @@ -261,6 +263,8 @@ There are four possible options: > Go exported methods are capitalized, so, the method name will be capitalized as well. > e.g. `SimpleServerHandler.AddGet` (capital "A" in "AddGet") +You can also create your own method name formatter by creating a function that implements the `jsonrpc.MethodNameFormatter` interface. + ```go func main() { // create a new server instance with a custom separator @@ -286,7 +290,7 @@ func main() { } ``` -### Using `WithMethodNameFormatter` +#### Using `WithMethodNameFormatter` `WithMethodNameFormatter` is the client-side counterpart to `WithServerMethodNameFormatter`. @@ -304,6 +308,27 @@ func main() { } ``` +#### Using `WithClientHandlerFormatter` + +Same as `WithMethodNameFormatter`, but for client handlers. Using it you can fully customize the JSON-RPC method name for client handlers, +given namespace and method name. + +```go +func main() { + closer, err := jsonrpc.NewMergeClient( + context.Background(), + "http://example.com", + "SimpleServerHandler", + []any{&client}, + nil, + jsonrpc.WithMethodNameFormatter(jsonrpc.NewMethodNameFormatter(false, OriginalCase)), + jsonrpc.WithClientHandler("Client", &RevCallTestClientHandler{}), + jsonrpc.WithClientHandlerFormatter(jsonrpc.NewMethodNameFormatter(false, OriginalCase)), + ) + defer closer() +} +``` + ## Contribute PRs are welcome! From b997fd5cbc076300b7c42b67bcfd45d3e5341483 Mon Sep 17 00:00:00 2001 From: thiagodeev Date: Wed, 20 Aug 2025 23:58:07 -0300 Subject: [PATCH 4/4] docs: expand README.md with detailed examples of method name alias usage for JSON-RPC --- README.md | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 338ae1a..26ed871 100644 --- a/README.md +++ b/README.md @@ -323,11 +323,116 @@ func main() { nil, jsonrpc.WithMethodNameFormatter(jsonrpc.NewMethodNameFormatter(false, OriginalCase)), jsonrpc.WithClientHandler("Client", &RevCallTestClientHandler{}), - jsonrpc.WithClientHandlerFormatter(jsonrpc.NewMethodNameFormatter(false, OriginalCase)), + jsonrpc.WithClientHandlerFormatter(jsonrpc.NewMethodNameFormatter(false, OriginalCase)), ) defer closer() } ``` +### Using method name alias + +You can also create an alias for a method name. This is useful if you want to use a different method name in the JSON-RPC +request than the actual method name for a specific method. + +#### Usage of method name alias in the server + +```go +type SimpleServerHandler struct {} + +func (h *SimpleServerHandler) Double(in int) int { + return in * 2 +} + +// create a new server instance +rpcServer := jsonrpc.NewServer() + +// create a handler instance and register it +serverHandler := &SimpleServerHandler{} +rpcServer.Register("SimpleServerHandler", serverHandler) + +// create an alias for the Double method. This will allow you to call the server's Double method +// with the name "rand_myRandomAlias" in the JSON-RPC request. +rpcServer.AliasMethod("rand_myRandomAlias", "SimpleServerHandler.Double") + +``` + +#### Usage of method name alias with client handlers + +```go +// setup the client handler +type ReverseHandler struct {} + +func (h *ReverseHandler) DoubleOnClient(in int) int { + return in * 2 +} + +// create a new client instance with the client handler + method name alias +closer, err := jsonrpc.NewMergeClient( + context.Background(), + "http://example.com", + "SimpleServerHandler", + []any{&client}, + nil, + jsonrpc.WithClientHandler("Client", &ReverseHandler{}), + // this allows the server to call the client's DoubleOnClient method using the name "rand_theClientRandomAlias" in the JSON-RPC request. + jsonrpc.WithClientHandlerAlias("rand_theClientRandomAlias", "Client.DoubleOnClient"), +) +``` + +#### Usage of a struct tag to define method name alias + +There are two cases where you can also use the `rpc_method` struct tag to define method name alias: +in the client struct and in the reverse handler struct in the server. + +In the client struct: +```go +// setup the client struct +var client struct { + AddInt func(int) int `rpc_method:"rand_aRandomAlias"` +} + +// create a new client instance with the client struct that has the `rpc_method` struct tag +closer, err := jsonrpc.NewMergeClient( + context.Background(), + "http://example.com", + "SimpleServerHandler", + []any{&client}, + nil, +) + +// since we defined the method name alias in the client struct, this will send a JSON-RPC request with "rand_aRandomAlias" as the method name to the +// server instead of "SimpleServerHandler.AddInt". +result, err := client.AddInt(10) + +``` + +In the server's reverse handler struct: + +```go +// Define the client handler interface +type ClientHandler struct { + CallOnClient func(int) (int, error) `rpc_method:"rand_theClientRandomAlias"` +} + +// Define the server handler +type ServerHandler struct {} + +func (h *ServerHandler) Call(ctx context.Context) (int, error) { + revClient, _ := jsonrpc.ExtractReverseClient[ClientHandler](ctx) + + // Reverse call to the client. + // Since we defined the method name alias in the client handler struct tag, this + // will send a JSON-RPC request with "rand_theClientRandomAlias" as the method name to the + // client instead of "Client.CallOnClient". + result, err := revClient.CallOnClient(7) + + // ... +} + +// Setup server with reverse client capability +rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[ClientHandler]("Client")) +rpcServer.Register("ServerHandler", &ServerHandler{}) +``` + ## Contribute