diff --git a/README.md b/README.md index 03eff66..26ed871 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,132 @@ 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() +} +``` +### 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 PRs are welcome! 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/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) + }) + } +} 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) {