1
0
Fork 0
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:
Quinn Slack 2017-01-23 19:55:20 -08:00 committed by GitHub
commit 6e88a787fb
4 changed files with 62 additions and 35 deletions

View file

@ -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
}

View file

@ -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 (

View file

@ -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)
} }
} }

View file

@ -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 {