Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
138 changes: 134 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`
Expand All @@ -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
Expand All @@ -286,7 +290,7 @@ func main() {
}
```

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

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

Expand All @@ -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!
Expand Down
6 changes: 5 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
53 changes: 52 additions & 1 deletion method_formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
})
}
}
12 changes: 10 additions & 2 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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) {
Expand Down
Loading