mirror of
https://github.com/go-micro/go-micro.git
synced 2025-01-17 17:44:30 +02:00
217 lines
5.4 KiB
Go
217 lines
5.4 KiB
Go
package jsonrpc2
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"math"
|
|
"reflect"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"go-micro.dev/v4/codec"
|
|
)
|
|
|
|
const seqNotify = math.MaxUint64
|
|
|
|
type clientCodec struct {
|
|
dec *json.Decoder // for reading JSON values
|
|
enc *json.Encoder // for writing JSON values
|
|
c io.Closer
|
|
|
|
// temporary work space
|
|
resp clientResponse
|
|
|
|
// JSON-RPC responses include the request id but not the request method.
|
|
// Package rpc expects both.
|
|
// We save the request method in pending when sending a request
|
|
// and then look it up by request ID when filling out the rpc Response.
|
|
mutex sync.Mutex // protects pending
|
|
pending map[interface{}]string // map request id to method name
|
|
}
|
|
|
|
func newClientCodec(conn io.ReadWriteCloser) *clientCodec {
|
|
return &clientCodec{
|
|
dec: json.NewDecoder(conn),
|
|
enc: json.NewEncoder(conn),
|
|
c: conn,
|
|
pending: make(map[interface{}]string),
|
|
}
|
|
}
|
|
|
|
type clientRequest struct {
|
|
Version string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params interface{} `json:"params,omitempty"`
|
|
ID interface{} `json:"id,omitempty"`
|
|
}
|
|
|
|
func (c *clientCodec) Write(m *codec.Message, b interface{}) error {
|
|
// If return error: it will be returned as is for this call.
|
|
// Allow param to be only Array, Slice, Map or Struct.
|
|
// When param is nil or uninitialized Map or Slice - omit "params".
|
|
if b != nil {
|
|
switch k := reflect.TypeOf(b).Kind(); k {
|
|
case reflect.Map:
|
|
if reflect.TypeOf(b).Key().Kind() == reflect.String {
|
|
if reflect.ValueOf(b).IsNil() {
|
|
b = nil
|
|
}
|
|
}
|
|
case reflect.Slice:
|
|
if reflect.ValueOf(b).IsNil() {
|
|
b = nil
|
|
}
|
|
case reflect.Array, reflect.Struct:
|
|
case reflect.Ptr:
|
|
switch k := reflect.TypeOf(b).Elem().Kind(); k {
|
|
case reflect.Map:
|
|
if reflect.TypeOf(b).Elem().Key().Kind() == reflect.String {
|
|
if reflect.ValueOf(b).Elem().IsNil() {
|
|
b = nil
|
|
}
|
|
}
|
|
case reflect.Slice:
|
|
if reflect.ValueOf(b).Elem().IsNil() {
|
|
b = nil
|
|
}
|
|
case reflect.Array, reflect.Struct:
|
|
default:
|
|
return NewError(errInternal.Code, "unsupported param type: Ptr to "+k.String())
|
|
}
|
|
default:
|
|
return NewError(errInternal.Code, "unsupported param type: "+k.String())
|
|
}
|
|
}
|
|
|
|
var req clientRequest
|
|
|
|
i, _ := strconv.ParseInt(m.Id, 10, 64)
|
|
|
|
if uint64(i) != seqNotify {
|
|
c.mutex.Lock()
|
|
c.pending[m.Id] = m.Endpoint
|
|
c.mutex.Unlock()
|
|
req.ID = m.Id
|
|
}
|
|
|
|
req.Version = "2.0"
|
|
req.Method = m.Endpoint
|
|
req.Params = b
|
|
if err := c.enc.Encode(&req); err != nil {
|
|
return NewError(errInternal.Code, err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type clientResponse struct {
|
|
Version string `json:"jsonrpc"`
|
|
ID interface{} `json:"id"`
|
|
Result *json.RawMessage `json:"result,omitempty"`
|
|
Error *Error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r *clientResponse) reset() {
|
|
r.Version = ""
|
|
r.ID = nil
|
|
r.Result = nil
|
|
r.Error = nil
|
|
}
|
|
|
|
func (r *clientResponse) UnmarshalJSON(raw []byte) error {
|
|
r.reset()
|
|
type resp *clientResponse
|
|
if err := json.Unmarshal(raw, resp(r)); err != nil {
|
|
return errors.New("bad response: " + string(raw))
|
|
}
|
|
|
|
var o = make(map[string]*json.RawMessage)
|
|
if err := json.Unmarshal(raw, &o); err != nil {
|
|
return errors.New("bad response: " + string(raw))
|
|
}
|
|
_, okVer := o["jsonrpc"]
|
|
_, okID := o["id"]
|
|
_, okRes := o["result"]
|
|
_, okErr := o["error"]
|
|
if !okVer || !okID || !(okRes || okErr) || (okRes && okErr) || len(o) > 3 {
|
|
return errors.New("bad response: " + string(raw))
|
|
}
|
|
if r.Version != "2.0" {
|
|
return errors.New("bad response: " + string(raw))
|
|
}
|
|
if okRes && r.Result == nil {
|
|
r.Result = &null
|
|
}
|
|
if okErr {
|
|
if o["error"] == nil {
|
|
return errors.New("bad response: " + string(raw))
|
|
}
|
|
oe := make(map[string]*json.RawMessage)
|
|
if err := json.Unmarshal(*o["error"], &oe); err != nil {
|
|
return errors.New("bad response: " + string(raw))
|
|
}
|
|
if oe["code"] == nil || oe["message"] == nil {
|
|
return errors.New("bad response: " + string(raw))
|
|
}
|
|
if _, ok := oe["data"]; (!ok && len(oe) > 2) || len(oe) > 3 {
|
|
return errors.New("bad response: " + string(raw))
|
|
}
|
|
}
|
|
if o["id"] == nil && !okErr {
|
|
return errors.New("bad response: " + string(raw))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *clientCodec) ReadHeader(m *codec.Message) error {
|
|
// If return err:
|
|
// - io.EOF will became ErrShutdown or io.ErrUnexpectedEOF
|
|
// - it will be returned as is for all pending calls
|
|
// - client will be shutdown
|
|
// So, return io.EOF as is, return *Error for all other errors.
|
|
c.resp.reset()
|
|
if err := c.dec.Decode(&c.resp); err != nil {
|
|
if err == io.EOF {
|
|
return err
|
|
}
|
|
return NewError(errInternal.Code, err.Error())
|
|
}
|
|
if c.resp.ID == nil {
|
|
return c.resp.Error
|
|
}
|
|
|
|
c.mutex.Lock()
|
|
m.Endpoint = c.pending[c.resp.ID]
|
|
delete(c.pending, c.resp.ID)
|
|
c.mutex.Unlock()
|
|
|
|
m.Error = ""
|
|
m.Id = c.resp.ID.(string)
|
|
if c.resp.Error != nil {
|
|
m.Error = c.resp.Error.Error()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *clientCodec) ReadBody(x interface{}) error {
|
|
// If x!=nil and return error e:
|
|
// - this call get e.Error() appended to "reading body "
|
|
// - other pending calls get error as is XXX actually other calls
|
|
// shouldn't be affected by this error at all, so let's at least
|
|
// provide different error message for other calls
|
|
if x == nil {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(*c.resp.Result, x); err != nil {
|
|
e := NewError(errInternal.Code, err.Error())
|
|
e.Data = NewError(errInternal.Code, "some other Call failed to unmarshal Reply")
|
|
return e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *clientCodec) Close() error {
|
|
return c.c.Close()
|
|
}
|