mirror of
https://github.com/sourcegraph/jsonrpc2.git
synced 2026-06-16 04:04:56 +02:00
Merge pull request #6 from sourcegraph/explicit-null
support responses with null result ({"result":null})
This commit is contained in:
commit
6e88a787fb
4 changed files with 62 additions and 35 deletions
|
|
@ -3,7 +3,6 @@ package jsonrpc2
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"reflect"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandlerWithError implements Handler by calling the func for each
|
// HandlerWithError implements Handler by calling the func for each
|
||||||
|
|
@ -22,9 +21,6 @@ func (h HandlerWithError) Handle(ctx context.Context, conn *Conn, req *Request)
|
||||||
|
|
||||||
resp := &Response{ID: req.ID}
|
resp := &Response{ID: req.ID}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if isNilValue(result) {
|
|
||||||
result = struct{}{}
|
|
||||||
}
|
|
||||||
err = resp.SetResult(result)
|
err = resp.SetResult(result)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -41,17 +37,3 @@ func (h HandlerWithError) Handle(ctx context.Context, conn *Conn, req *Request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isNilValue tests if an interface is empty, because an empty interface does
|
|
||||||
// not encode any information, we can't encode it in JSON so that the proxy
|
|
||||||
// knows it's a response, not a request.
|
|
||||||
func isNilValue(resp interface{}) bool {
|
|
||||||
if resp == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
kind := reflect.TypeOf(resp).Kind()
|
|
||||||
value := reflect.ValueOf(resp)
|
|
||||||
nilPtr := kind == reflect.Ptr && value.IsNil()
|
|
||||||
nilSlice := kind == reflect.Slice && value.IsNil()
|
|
||||||
return nilPtr || nilSlice
|
|
||||||
}
|
|
||||||
|
|
|
||||||
49
jsonrpc2.go
49
jsonrpc2.go
|
|
@ -3,6 +3,7 @@
|
||||||
package jsonrpc2
|
package jsonrpc2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -131,6 +132,9 @@ func (r *Response) MarshalJSON() ([]byte, error) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return nil, errors.New("can't marshal nil *jsonrpc2.Response")
|
return nil, errors.New("can't marshal nil *jsonrpc2.Response")
|
||||||
}
|
}
|
||||||
|
if (r.Result == nil || len(*r.Result) == 0) && r.Error == nil {
|
||||||
|
return nil, errors.New("can't marshal *jsonrpc2.Response (must have result or error)")
|
||||||
|
}
|
||||||
b, err := json.Marshal(*r)
|
b, err := json.Marshal(*r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -373,7 +377,10 @@ func (c *Conn) Call(ctx context.Context, method string, params, result interface
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if result != nil && call.response.Result != nil {
|
if result != nil {
|
||||||
|
if call.response.Result == nil {
|
||||||
|
call.response.Result = &jsonNull
|
||||||
|
}
|
||||||
// TODO(sqs): error handling
|
// TODO(sqs): error handling
|
||||||
if err := json.Unmarshal(*call.response.Result, result); err != nil {
|
if err := json.Unmarshal(*call.response.Result, result); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -386,6 +393,8 @@ func (c *Conn) Call(ctx context.Context, method string, params, result interface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var jsonNull = json.RawMessage("null")
|
||||||
|
|
||||||
// Notify is like Call, but it returns when the notification request
|
// Notify is like Call, but it returns when the notification request
|
||||||
// is sent (without waiting for a response, because JSON-RPC
|
// is sent (without waiting for a response, because JSON-RPC
|
||||||
// notifications do not have responses).
|
// notifications do not have responses).
|
||||||
|
|
@ -540,15 +549,16 @@ func (m *anyMessage) UnmarshalJSON(data []byte) error {
|
||||||
// The presence of these fields distinguishes between the 2
|
// The presence of these fields distinguishes between the 2
|
||||||
// message types.
|
// message types.
|
||||||
type msg struct {
|
type msg struct {
|
||||||
Method *string `json:"method"`
|
ID interface{} `json:"id"`
|
||||||
Result interface{} `json:"result"`
|
Method *string `json:"method"`
|
||||||
Error interface{} `json:"error"`
|
Result anyValueWithExplicitNull `json:"result"`
|
||||||
|
Error interface{} `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var isRequest, isResponse bool
|
var isRequest, isResponse bool
|
||||||
checkType := func(m *msg) error {
|
checkType := func(m *msg) error {
|
||||||
mIsRequest := m.Method != nil
|
mIsRequest := m.Method != nil
|
||||||
mIsResponse := m.Result != nil || m.Error != nil
|
mIsResponse := m.Result.null || m.Result.value != nil || m.Error != nil
|
||||||
if (!mIsRequest && !mIsResponse) || (mIsRequest && mIsResponse) {
|
if (!mIsRequest && !mIsResponse) || (mIsRequest && mIsResponse) {
|
||||||
return errors.New("jsonrpc2: unable to determine message type (request or response)")
|
return errors.New("jsonrpc2: unable to determine message type (request or response)")
|
||||||
}
|
}
|
||||||
|
|
@ -590,7 +600,34 @@ func (m *anyMessage) UnmarshalJSON(data []byte) error {
|
||||||
case !isRequest && isResponse:
|
case !isRequest && isResponse:
|
||||||
v = &m.response
|
v = &m.response
|
||||||
}
|
}
|
||||||
return json.Unmarshal(data, v)
|
if err := json.Unmarshal(data, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isRequest && isResponse && m.response.Error == nil && m.response.Result == nil {
|
||||||
|
m.response.Result = &jsonNull
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// anyValueWithExplicitNull is used to distinguish {} from
|
||||||
|
// {"result":null} by anyMessage's JSON unmarshaler.
|
||||||
|
type anyValueWithExplicitNull struct {
|
||||||
|
null bool // JSON "null"
|
||||||
|
value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v anyValueWithExplicitNull) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(v.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *anyValueWithExplicitNull) UnmarshalJSON(data []byte) error {
|
||||||
|
data = bytes.TrimSpace(data)
|
||||||
|
if string(data) == "null" {
|
||||||
|
*v = anyValueWithExplicitNull{null: true}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*v = anyValueWithExplicitNull{}
|
||||||
|
return json.Unmarshal(data, &v.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,19 @@ func TestRequest_MarshalJSON_jsonrpc(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if want := `"jsonrpc":"2.0"`; !strings.Contains(string(b), want) {
|
if want := `{"method":"","id":0,"jsonrpc":"2.0"}`; string(b) != want {
|
||||||
t.Errorf("got %s, want it to include the string %s", b, want)
|
t.Errorf("got %q, want %q", b, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResponse_MarshalJSON_jsonrpc(t *testing.T) {
|
func TestResponse_MarshalJSON_jsonrpc(t *testing.T) {
|
||||||
b, err := json.Marshal(&jsonrpc2.Response{})
|
null := json.RawMessage("null")
|
||||||
|
b, err := json.Marshal(&jsonrpc2.Response{Result: &null})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if want := `"jsonrpc":"2.0"`; !strings.Contains(string(b), want) {
|
if want := `{"id":0,"result":null,"jsonrpc":"2.0"}`; string(b) != want {
|
||||||
t.Errorf("got %s, want it to include the string %s", b, want)
|
t.Errorf("got %q, want %q", b, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,24 @@ import (
|
||||||
|
|
||||||
func TestAnyMessage(t *testing.T) {
|
func TestAnyMessage(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
request, response bool
|
request, response, invalid bool
|
||||||
}{
|
}{
|
||||||
// Single messages
|
// Single messages
|
||||||
`{}`: {},
|
`{}`: {invalid: true},
|
||||||
`{"foo":"bar"}`: {},
|
`{"foo":"bar"}`: {invalid: true},
|
||||||
`{"method":"m"}`: {request: true},
|
`{"method":"m"}`: {request: true},
|
||||||
`{"result":123}`: {response: true},
|
`{"result":123}`: {response: true},
|
||||||
|
`{"result":null}`: {response: true},
|
||||||
`{"error":{"code":456,"message":"m"}}`: {response: true},
|
`{"error":{"code":456,"message":"m"}}`: {response: true},
|
||||||
}
|
}
|
||||||
for s, want := range tests {
|
for s, want := range tests {
|
||||||
var m anyMessage
|
var m anyMessage
|
||||||
json.Unmarshal([]byte(s), &m)
|
if err := json.Unmarshal([]byte(s), &m); err != nil {
|
||||||
|
if !want.invalid {
|
||||||
|
t.Errorf("%s: error: %s", s, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (m.request != nil) != want.request {
|
if (m.request != nil) != want.request {
|
||||||
t.Errorf("%s: got request %v, want %v", s, m.request != nil, want.request)
|
t.Errorf("%s: got request %v, want %v", s, m.request != nil, want.request)
|
||||||
}
|
}
|
||||||
|
|
@ -30,6 +36,7 @@ func TestAnyMessage(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMessageCodec(t *testing.T) {
|
func TestMessageCodec(t *testing.T) {
|
||||||
|
obj := json.RawMessage(`{"foo":"bar"}`)
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
v, vempty interface{}
|
v, vempty interface{}
|
||||||
}{
|
}{
|
||||||
|
|
@ -38,8 +45,8 @@ func TestMessageCodec(t *testing.T) {
|
||||||
vempty: &Request{ID: ID{Num: 123}},
|
vempty: &Request{ID: ID{Num: 123}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
v: &Response{ID: ID{Num: 123}},
|
v: &Response{ID: ID{Num: 123}, Result: &obj},
|
||||||
vempty: &Response{ID: ID{Num: 123}},
|
vempty: &Response{ID: ID{Num: 123}, Result: &obj},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue