From 9ce4585034c99c340bf0f802638d33ed6fc31dd8 Mon Sep 17 00:00:00 2001 From: Sam Herrmann Date: Sat, 24 Apr 2021 14:49:40 -0400 Subject: [PATCH] Add StringID call option Fixes #43 --- call_opt.go | 9 +++++ call_opt_test.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ jsonrpc2.go | 11 ++++-- jsonrpc2_test.go | 44 ----------------------- 4 files changed, 109 insertions(+), 47 deletions(-) create mode 100644 call_opt_test.go diff --git a/call_opt.go b/call_opt.go index b554bac..73fe9c2 100644 --- a/call_opt.go +++ b/call_opt.go @@ -28,3 +28,12 @@ func PickID(id ID) CallOption { return nil }) } + +// StringID returns a call option that instructs the request ID to be set as a +// string. +func StringID() CallOption { + return callOptionFunc(func(r *Request) error { + r.ID.IsString = true + return nil + }) +} diff --git a/call_opt_test.go b/call_opt_test.go new file mode 100644 index 0000000..82b05ca --- /dev/null +++ b/call_opt_test.go @@ -0,0 +1,92 @@ +package jsonrpc2_test + +import ( + "context" + "fmt" + "testing" + + "github.com/sourcegraph/jsonrpc2" +) + +func TestPickID(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + a, b := inMemoryPeerConns() + defer a.Close() + defer b.Close() + + handler := handlerFunc(func(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { + if err := conn.Reply(ctx, req.ID, fmt.Sprintf("hello, #%s: %s", req.ID, *req.Params)); err != nil { + t.Error(err) + } + }) + connA := jsonrpc2.NewConn(ctx, jsonrpc2.NewBufferedStream(a, jsonrpc2.VSCodeObjectCodec{}), handler) + connB := jsonrpc2.NewConn(ctx, jsonrpc2.NewBufferedStream(b, jsonrpc2.VSCodeObjectCodec{}), noopHandler{}) + defer connA.Close() + defer connB.Close() + + const n = 100 + for i := 0; i < n; i++ { + var opts []jsonrpc2.CallOption + id := jsonrpc2.ID{Num: uint64(i)} + + // This is the actual test, every 3rd request we specify the + // ID and ensure we get a response with the correct ID echoed + // back + if i%3 == 0 { + id = jsonrpc2.ID{ + Str: fmt.Sprintf("helloworld-%d", i/3), + IsString: true, + } + opts = append(opts, jsonrpc2.PickID(id)) + } + + var got string + if err := connB.Call(ctx, "f", []int32{1, 2, 3}, &got, opts...); err != nil { + t.Fatal(err) + } + if want := fmt.Sprintf("hello, #%s: [1,2,3]", id); got != want { + t.Errorf("got result %q, want %q", got, want) + } + } +} + +func TestStringID(t *testing.T) { + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + a, b := inMemoryPeerConns() + defer a.Close() + defer b.Close() + + handler := handlerFunc(func(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { + replyWithError := func(msg string) { + respErr := &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidRequest, Message: msg} + if err := conn.ReplyWithError(ctx, req.ID, respErr); err != nil { + t.Error(err) + } + } + if !req.ID.IsString { + replyWithError("ID.IsString should be true") + return + } + if len(req.ID.Str) == 0 { + replyWithError("ID.Str should be populated but is empty") + return + } + if err := conn.Reply(ctx, req.ID, "ok"); err != nil { + t.Error(err) + } + }) + connA := jsonrpc2.NewConn(ctx, jsonrpc2.NewBufferedStream(a, jsonrpc2.VSCodeObjectCodec{}), handler) + connB := jsonrpc2.NewConn(ctx, jsonrpc2.NewBufferedStream(b, jsonrpc2.VSCodeObjectCodec{}), noopHandler{}) + defer connA.Close() + defer connB.Close() + + var res string + if err := connB.Call(ctx, "f", nil, &res, jsonrpc2.StringID()); err != nil { + t.Fatal(err) + } +} diff --git a/jsonrpc2.go b/jsonrpc2.go index d1288e2..005b65c 100644 --- a/jsonrpc2.go +++ b/jsonrpc2.go @@ -366,9 +366,14 @@ func (c *Conn) send(_ context.Context, m *anyMessage, wait bool) (cc *call, err // responses. if m.request != nil && wait { cc = &call{request: m.request, seq: c.seq, done: make(chan error, 1)} - if !m.request.ID.IsString && m.request.ID.Num == 0 { - // unset, use next seq as call ID - m.request.ID.Num = c.seq + + isIDUnset := len(m.request.ID.Str) == 0 && m.request.ID.Num == 0 + if isIDUnset { + if m.request.ID.IsString { + m.request.ID.Str = strconv.FormatUint(c.seq, 10) + } else { + m.request.ID.Num = c.seq + } } id = m.request.ID c.pending[id] = cc diff --git a/jsonrpc2_test.go b/jsonrpc2_test.go index 9710ab5..c319a1b 100644 --- a/jsonrpc2_test.go +++ b/jsonrpc2_test.go @@ -243,50 +243,6 @@ func (h handlerFunc) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonr h(ctx, conn, req) } -func TestPickID(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - a, b := inMemoryPeerConns() - defer a.Close() - defer b.Close() - - handler := handlerFunc(func(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { - if err := conn.Reply(ctx, req.ID, fmt.Sprintf("hello, #%s: %s", req.ID, *req.Params)); err != nil { - t.Error(err) - } - }) - connA := jsonrpc2.NewConn(ctx, jsonrpc2.NewBufferedStream(a, jsonrpc2.VSCodeObjectCodec{}), handler) - connB := jsonrpc2.NewConn(ctx, jsonrpc2.NewBufferedStream(b, jsonrpc2.VSCodeObjectCodec{}), noopHandler{}) - defer connA.Close() - defer connB.Close() - - const n = 100 - for i := 0; i < n; i++ { - var opts []jsonrpc2.CallOption - id := jsonrpc2.ID{Num: uint64(i)} - - // This is the actual test, every 3rd request we specify the - // ID and ensure we get a response with the correct ID echoed - // back - if i%3 == 0 { - id = jsonrpc2.ID{ - Str: fmt.Sprintf("helloworld-%d", i/3), - IsString: true, - } - opts = append(opts, jsonrpc2.PickID(id)) - } - - var got string - if err := connB.Call(ctx, "f", []int32{1, 2, 3}, &got, opts...); err != nil { - t.Fatal(err) - } - if want := fmt.Sprintf("hello, #%s: [1,2,3]", id); got != want { - t.Errorf("got result %q, want %q", got, want) - } - } -} - func TestHandlerBlocking(t *testing.T) { // We send N notifications with an increasing parameter. Since the // handler is blocking, we expect to process the notifications in the