From e93be7f42f9fcac2c537bcd78bab08e6e63ed5bf Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Fri, 21 Sep 2018 11:17:43 -0400 Subject: [PATCH] internal/jsonrpc2: a basic json rpc library to build an lsp on top of Change-Id: I6aa47fffcb29842e3194231e4ad4b6be4386d329 Reviewed-on: https://go-review.googlesource.com/136675 Reviewed-by: Rebecca Stambler --- internal/jsonrpc2/jsonrpc2.go | 341 +++++++++++++++++++++++++++++ internal/jsonrpc2/jsonrpc2_test.go | 159 ++++++++++++++ internal/jsonrpc2/log.go | 34 +++ internal/jsonrpc2/stream.go | 146 ++++++++++++ internal/jsonrpc2/wire.go | 136 ++++++++++++ 5 files changed, 816 insertions(+) create mode 100644 internal/jsonrpc2/jsonrpc2.go create mode 100644 internal/jsonrpc2/jsonrpc2_test.go create mode 100644 internal/jsonrpc2/log.go create mode 100644 internal/jsonrpc2/stream.go create mode 100644 internal/jsonrpc2/wire.go diff --git a/internal/jsonrpc2/jsonrpc2.go b/internal/jsonrpc2/jsonrpc2.go new file mode 100644 index 0000000000..f981e86860 --- /dev/null +++ b/internal/jsonrpc2/jsonrpc2.go @@ -0,0 +1,341 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package jsonrpc2 is a minimal implementation of the JSON RPC 2 spec. +// https://www.jsonrpc.org/specification +// It is intended to be compatible with other implementations at the wire level. +package jsonrpc2 + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "sync/atomic" +) + +// Conn is a JSON RPC 2 client server connection. +// Conn is bidirectional; it does not have a designated server or client end. +type Conn struct { + handle Handler + cancel Canceler + log Logger + stream Stream + done chan struct{} + err error + seq int64 // must only be accessed using atomic operations + pendingMu sync.Mutex // protects the pending map + pending map[ID]chan *Response + handlingMu sync.Mutex // protects the handling map + handling map[ID]context.CancelFunc +} + +// Handler is an option you can pass to NewConn to handle incoming requests. +// If the request returns true from IsNotify then the Handler should not return a +// result or error, otherwise it should handle the Request and return either +// an encoded result, or an error. +// Handlers must be concurrency-safe. +type Handler = func(context.Context, *Conn, *Request) (interface{}, *Error) + +// Canceler is an option you can pass to NewConn which is invoked for +// cancelled outgoing requests. +// The request will have the ID filled in, which can be used to propagate the +// cancel to the other process if needed. +// It is okay to use the connection to send notifications, but the context will +// be in the cancelled state, so you must do it with the background context +// instead. +type Canceler = func(context.Context, *Conn, *Request) + +// Logger is an option you can pass to NewConn which is invoked for +// all messages flowing through a Conn. +type Logger = func(mode string, id *ID, method string, payload *json.RawMessage, err *Error) + +// NewErrorf builds a Error struct for the suppied message and code. +// If args is not empty, message and args will be passed to Sprintf. +func NewErrorf(code int64, format string, args ...interface{}) *Error { + return &Error{ + Code: code, + Message: fmt.Sprintf(format, args...), + } +} + +// NewConn creates a new connection object that reads and writes messages from +// the supplied stream and dispatches incoming messages to the supplied handler. +func NewConn(ctx context.Context, s Stream, options ...interface{}) *Conn { + conn := &Conn{ + stream: s, + done: make(chan struct{}), + pending: make(map[ID]chan *Response), + handling: make(map[ID]context.CancelFunc), + } + for _, opt := range options { + switch opt := opt.(type) { + case Handler: + if conn.handle != nil { + panic("Duplicate Handler function in options list") + } + conn.handle = opt + case Canceler: + if conn.cancel != nil { + panic("Duplicate Canceler function in options list") + } + conn.cancel = opt + case Logger: + if conn.log != nil { + panic("Duplicate Logger function in options list") + } + conn.log = opt + default: + panic(fmt.Errorf("Unknown option type %T in options list", opt)) + } + } + if conn.handle == nil { + // the default handler reports a method error + conn.handle = func(ctx context.Context, c *Conn, r *Request) (interface{}, *Error) { + return nil, NewErrorf(CodeMethodNotFound, "method %q not found", r.Method) + } + } + if conn.cancel == nil { + // the default canceller does nothing + conn.cancel = func(context.Context, *Conn, *Request) {} + } + if conn.log == nil { + // the default logger does nothing + conn.log = func(string, *ID, string, *json.RawMessage, *Error) {} + } + go func() { + conn.err = conn.run(ctx) + close(conn.done) + }() + return conn +} + +// Wait blocks until the connection is terminated, and returns any error that +// cause the termination. +func (c *Conn) Wait(ctx context.Context) error { + select { + case <-c.done: + return c.err + case <-ctx.Done(): + return ctx.Err() + } +} + +// Cancel cancels a pending Call on the server side. +// The call is identified by its id. +// JSON RPC 2 does not specify a cancel message, so cancellation support is not +// directly wired in. This method allows a higher level protocol to choose how +// to propagate the cancel. +func (c *Conn) Cancel(id ID) { + c.handlingMu.Lock() + cancel := c.handling[id] + c.handlingMu.Unlock() + if cancel != nil { + cancel() + } +} + +// Notify is called to send a notification request over the connection. +// It will return as soon as the notification has been sent, as no response is +// possible. +func (c *Conn) Notify(ctx context.Context, method string, params interface{}) error { + jsonParams, err := marshalToRaw(params) + if err != nil { + return fmt.Errorf("marshalling notify parameters: %v", err) + } + request := &Request{ + Method: method, + Params: jsonParams, + } + data, err := json.Marshal(request) + if err != nil { + return fmt.Errorf("marshalling notify request: %v", err) + } + c.log("notify <=", nil, request.Method, request.Params, nil) + return c.stream.Write(ctx, data) +} + +// Call sends a request over the connection and then waits for a response. +// If the response is not an error, it will be decoded into result. +// result must be of a type you an pass to json.Unmarshal. +func (c *Conn) Call(ctx context.Context, method string, params, result interface{}) error { + jsonParams, err := marshalToRaw(params) + if err != nil { + return fmt.Errorf("marshalling call parameters: %v", err) + } + // generate a new request identifier + id := ID{Number: atomic.AddInt64(&c.seq, 1)} + request := &Request{ + ID: &id, + Method: method, + Params: jsonParams, + } + // marshal the request now it is complete + data, err := json.Marshal(request) + if err != nil { + return fmt.Errorf("marshalling call request: %v", err) + } + // we have to add ourselves to the pending map before we send, otherwise we + // are racing the response + rchan := make(chan *Response) + c.pendingMu.Lock() + c.pending[id] = rchan + c.pendingMu.Unlock() + defer func() { + // clean up the pending response handler on the way out + c.pendingMu.Lock() + delete(c.pending, id) + c.pendingMu.Unlock() + }() + // now we are ready to send + c.log("call <=", request.ID, request.Method, request.Params, nil) + if err := c.stream.Write(ctx, data); err != nil { + // sending failed, we will never get a response, so don't leave it pending + return err + } + // now wait for the response + select { + case response := <-rchan: + // is it an error response? + if response.Error != nil { + return response.Error + } + if result == nil || response.Result == nil { + return nil + } + if err := json.Unmarshal(*response.Result, result); err != nil { + return fmt.Errorf("unmarshalling result: %v", err) + } + return nil + case <-ctx.Done(): + // allow the handler to propagate the cancel + c.cancel(ctx, c, request) + return ctx.Err() + } +} + +// combined has all the fields of both Request and Response. +// We can decode this and then work out which it is. +type combined struct { + VersionTag VersionTag `json:"jsonrpc"` + ID *ID `json:"id,omitempty"` + Method string `json:"method"` + Params *json.RawMessage `json:"params,omitempty"` + Result *json.RawMessage `json:"result,omitempty"` + Error *Error `json:"error,omitempty"` +} + +// Run starts a read loop on the supplied reader. +// It must be called exactly once for each Conn. +// It returns only when the reader is closed or there is an error in the stream. +func (c *Conn) run(ctx context.Context) error { + ctx, cancelRun := context.WithCancel(ctx) + for { + // get the data for a message + data, err := c.stream.Read(ctx) + if err != nil { + // the stream failed, we cannot continue + return err + } + // read a combined message + msg := &combined{} + if err := json.Unmarshal(data, msg); err != nil { + // a badly formed message arrived, log it and continue + // we trust the stream to have isolated the error to just this message + c.log("read", nil, "", nil, NewErrorf(0, "unmarshal failed: %v", err)) + continue + } + // work out which kind of message we have + switch { + case msg.Method != "": + // if method is set it must be a request + request := &Request{ + Method: msg.Method, + Params: msg.Params, + ID: msg.ID, + } + if request.IsNotify() { + c.log("notify =>", request.ID, request.Method, request.Params, nil) + // we have a Notify, forward to the handler in a go routine + go func() { + if _, err := c.handle(ctx, c, request); err != nil { + // notify produced an error, we can't forward it to the other side + // because there is no id, so we just log it + c.log("notify failed", nil, request.Method, nil, err) + } + }() + } else { + // we have a Call, forward to the handler in another go routine + reqCtx, cancelReq := context.WithCancel(ctx) + c.handlingMu.Lock() + c.handling[*request.ID] = cancelReq + c.handlingMu.Unlock() + go func() { + defer func() { + // clean up the cancel handler on the way out + c.handlingMu.Lock() + delete(c.handling, *request.ID) + c.handlingMu.Unlock() + cancelReq() + }() + c.log("call =>", request.ID, request.Method, request.Params, nil) + resp, callErr := c.handle(reqCtx, c, request) + var result *json.RawMessage + if result, err = marshalToRaw(resp); err != nil { + callErr = &Error{Message: err.Error()} + } + response := &Response{ + Result: result, + Error: callErr, + ID: request.ID, + } + data, err := json.Marshal(response) + if err != nil { + // failure to marshal leaves the call without a response + // possibly we could attempt to respond with a different message + // but we can probably rely on timeouts instead + c.log("respond =!>", request.ID, request.Method, nil, NewErrorf(0, "%s", err)) + return + } + c.log("respond =>", response.ID, "", response.Result, response.Error) + if err = c.stream.Write(ctx, data); err != nil { + // if a stream write fails, we really need to shut down the whole + // stream and return from the run + c.log("respond =!>", nil, request.Method, nil, NewErrorf(0, "%s", err)) + cancelRun() + return + } + }() + } + case msg.ID != nil: + // we have a response, get the pending entry from the map + c.pendingMu.Lock() + rchan := c.pending[*msg.ID] + if rchan != nil { + delete(c.pending, *msg.ID) + } + c.pendingMu.Unlock() + // and send the reply to the channel + response := &Response{ + Result: msg.Result, + Error: msg.Error, + ID: msg.ID, + } + c.log("response =>", response.ID, "", response.Result, response.Error) + rchan <- response + close(rchan) + default: + c.log("invalid =>", nil, "", nil, NewErrorf(0, "message not a call, notify or response, ignoring")) + } + } +} + +func marshalToRaw(obj interface{}) (*json.RawMessage, error) { + data, err := json.Marshal(obj) + if err != nil { + return nil, err + } + raw := json.RawMessage(data) + return &raw, nil +} diff --git a/internal/jsonrpc2/jsonrpc2_test.go b/internal/jsonrpc2/jsonrpc2_test.go new file mode 100644 index 0000000000..2556083e66 --- /dev/null +++ b/internal/jsonrpc2/jsonrpc2_test.go @@ -0,0 +1,159 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package jsonrpc2_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "path" + "reflect" + "testing" + + "golang.org/x/tools/internal/jsonrpc2" +) + +var logRPC = flag.Bool("logrpc", false, "Enable jsonrpc2 communication logging") + +type callTest struct { + method string + params interface{} + expect interface{} +} + +var callTests = []callTest{ + {"no_args", nil, true}, + {"one_string", "fish", "got:fish"}, + {"one_number", 10, "got:10"}, + {"join", []string{"a", "b", "c"}, "a/b/c"}, + //TODO: expand the test cases +} + +func (test *callTest) newResults() interface{} { + switch e := test.expect.(type) { + case []interface{}: + var r []interface{} + for _, v := range e { + r = append(r, reflect.New(reflect.TypeOf(v)).Interface()) + } + return r + case nil: + return nil + default: + return reflect.New(reflect.TypeOf(test.expect)).Interface() + } +} + +func (test *callTest) verifyResults(t *testing.T, results interface{}) { + if results == nil { + return + } + val := reflect.Indirect(reflect.ValueOf(results)).Interface() + if !reflect.DeepEqual(val, test.expect) { + t.Errorf("%v:Results are incorrect, got %+v expect %+v", test.method, val, test.expect) + } +} + +func TestPlainCall(t *testing.T) { + ctx := context.Background() + a, b := prepare(ctx, t, false) + for _, test := range callTests { + results := test.newResults() + if err := a.Call(ctx, test.method, test.params, results); err != nil { + t.Fatalf("%v:Call failed: %v", test.method, err) + } + test.verifyResults(t, results) + if err := b.Call(ctx, test.method, test.params, results); err != nil { + t.Fatalf("%v:Call failed: %v", test.method, err) + } + test.verifyResults(t, results) + } +} + +func TestHeaderCall(t *testing.T) { + ctx := context.Background() + a, b := prepare(ctx, t, true) + for _, test := range callTests { + results := test.newResults() + if err := a.Call(ctx, test.method, test.params, results); err != nil { + t.Fatalf("%v:Call failed: %v", test.method, err) + } + test.verifyResults(t, results) + if err := b.Call(ctx, test.method, test.params, results); err != nil { + t.Fatalf("%v:Call failed: %v", test.method, err) + } + test.verifyResults(t, results) + } +} + +func prepare(ctx context.Context, t *testing.T, withHeaders bool) (*testHandler, *testHandler) { + a := &testHandler{t: t} + b := &testHandler{t: t} + a.reader, b.writer = io.Pipe() + b.reader, a.writer = io.Pipe() + for _, h := range []*testHandler{a, b} { + h := h + if withHeaders { + h.stream = jsonrpc2.NewHeaderStream(h.reader, h.writer) + } else { + h.stream = jsonrpc2.NewStream(h.reader, h.writer) + } + args := []interface{}{handle} + if *logRPC { + args = append(args, jsonrpc2.Log) + } + h.Conn = jsonrpc2.NewConn(ctx, h.stream, args...) + go func() { + defer func() { + h.reader.Close() + h.writer.Close() + }() + if err := h.Conn.Wait(ctx); err != nil { + t.Fatalf("Stream failed: %v", err) + } + }() + } + return a, b +} + +type testHandler struct { + t *testing.T + reader *io.PipeReader + writer *io.PipeWriter + stream jsonrpc2.Stream + *jsonrpc2.Conn +} + +func handle(ctx context.Context, c *jsonrpc2.Conn, r *jsonrpc2.Request) (interface{}, *jsonrpc2.Error) { + switch r.Method { + case "no_args": + if r.Params != nil { + return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidParams, "Expected no params") + } + return true, nil + case "one_string": + var v string + if err := json.Unmarshal(*r.Params, &v); err != nil { + return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeParseError, "%v", err.Error()) + } + return "got:" + v, nil + case "one_number": + var v int + if err := json.Unmarshal(*r.Params, &v); err != nil { + return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeParseError, "%v", err.Error()) + } + return fmt.Sprintf("got:%d", v), nil + case "join": + var v []string + if err := json.Unmarshal(*r.Params, &v); err != nil { + return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeParseError, "%v", err.Error()) + } + return path.Join(v...), nil + default: + return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not found", r.Method) + } +} diff --git a/internal/jsonrpc2/log.go b/internal/jsonrpc2/log.go new file mode 100644 index 0000000000..3dbde8f76f --- /dev/null +++ b/internal/jsonrpc2/log.go @@ -0,0 +1,34 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package jsonrpc2 + +import ( + "bytes" + "encoding/json" + "fmt" + "log" +) + +// Log is an implementation of Logger that outputs using log.Print +// It is not used by default, but is provided for easy logging in users code. +func Log(mode string, id *ID, method string, payload *json.RawMessage, err *Error) { + buf := &bytes.Buffer{} + fmt.Fprint(buf, mode) + if id == nil { + fmt.Fprintf(buf, " []") + } else { + fmt.Fprintf(buf, " [%v]", id) + } + if method != "" { + fmt.Fprintf(buf, " %s", method) + } + if payload != nil { + fmt.Fprintf(buf, " %s", *payload) + } + if err != nil { + fmt.Fprintf(buf, " failed: %s", err) + } + log.Print(buf) +} diff --git a/internal/jsonrpc2/stream.go b/internal/jsonrpc2/stream.go new file mode 100644 index 0000000000..fe28c55a9a --- /dev/null +++ b/internal/jsonrpc2/stream.go @@ -0,0 +1,146 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package jsonrpc2 + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + "sync" +) + +// Stream abstracts the transport mechanics from the JSON RPC protocol. +// A Conn reads and writes messages using the stream it was provided on +// construction, and assumes that each call to Read or Write fully transfers +// a single message, or returns an error. +type Stream interface { + // Read gets the next message from the stream. + // It is never called concurrently. + Read(context.Context) ([]byte, error) + // Write sends a message to the stream. + // It must be safe for concurrent use. + Write(context.Context, []byte) error +} + +// NewStream returns a Stream built on top of an io.Reader and io.Writer +// The messages are sent with no wrapping, and rely on json decode consistency +// to determine message boundaries. +func NewStream(in io.Reader, out io.Writer) Stream { + return &plainStream{ + in: json.NewDecoder(in), + out: out, + } +} + +type plainStream struct { + in *json.Decoder + outMu sync.Mutex + out io.Writer +} + +func (s *plainStream) Read(ctx context.Context) ([]byte, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + var raw json.RawMessage + if err := s.in.Decode(&raw); err != nil { + return nil, err + } + return raw, nil +} + +func (s *plainStream) Write(ctx context.Context, data []byte) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + s.outMu.Lock() + _, err := s.out.Write(data) + s.outMu.Unlock() + return err +} + +// NewHeaderStream returns a Stream built on top of an io.Reader and io.Writer +// The messages are sent with HTTP content length and MIME type headers. +// This is the format used by LSP and others. +func NewHeaderStream(in io.Reader, out io.Writer) Stream { + return &headerStream{ + in: bufio.NewReader(in), + out: out, + } +} + +type headerStream struct { + in *bufio.Reader + outMu sync.Mutex + out io.Writer +} + +func (s *headerStream) Read(ctx context.Context) ([]byte, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + var length int64 + // read the header, stop on the first empty line + for { + line, err := s.in.ReadString('\n') + if err != nil { + return nil, fmt.Errorf("failed reading header line %q", err) + } + line = strings.TrimSpace(line) + // check we have a header line + if line == "" { + break + } + colon := strings.IndexRune(line, ':') + if colon < 0 { + return nil, fmt.Errorf("invalid header line %q", line) + } + name, value := line[:colon], strings.TrimSpace(line[colon+1:]) + switch name { + case "Content-Length": + if length, err = strconv.ParseInt(value, 10, 32); err != nil { + return nil, fmt.Errorf("failed parsing Content-Length: %v", value) + } + if length <= 0 { + return nil, fmt.Errorf("invalid Content-Length: %v", length) + } + default: + // ignoring unknown headers + } + } + if length == 0 { + return nil, fmt.Errorf("missing Content-Length header") + } + data := make([]byte, length) + if _, err := io.ReadFull(s.in, data); err != nil { + return nil, err + } + return data, nil +} + +func (s *headerStream) Write(ctx context.Context, data []byte) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + s.outMu.Lock() + _, err := fmt.Fprintf(s.out, "Content-Length: %v\r\n\r\n", len(data)) + if err == nil { + _, err = s.out.Write(data) + } + s.outMu.Unlock() + return err +} diff --git a/internal/jsonrpc2/wire.go b/internal/jsonrpc2/wire.go new file mode 100644 index 0000000000..3ff0c3845d --- /dev/null +++ b/internal/jsonrpc2/wire.go @@ -0,0 +1,136 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package jsonrpc2 + +import ( + "encoding/json" + "fmt" + "strconv" +) + +// this file contains the go forms of the wire specification +// see http://www.jsonrpc.org/specification for details + +const ( + // CodeUnknownError should be used for all non coded errors. + CodeUnknownError = -32001 + // CodeParseError is used when invalid JSON was received by the server. + CodeParseError = -32700 + //CodeInvalidRequest is used when the JSON sent is not a valid Request object. + CodeInvalidRequest = -32600 + // CodeMethodNotFound should be returned by the handler when the method does + // not exist / is not available. + CodeMethodNotFound = -32601 + // CodeInvalidParams should be returned by the handler when method + // parameter(s) were invalid. + CodeInvalidParams = -32602 + // CodeInternalError is not currently returned but defined for completeness. + CodeInternalError = -32603 +) + +// Request is sent to a server to represent a Call or Notify operaton. +type Request struct { + // VersionTag is always encoded as the string "2.0" + VersionTag VersionTag `json:"jsonrpc"` + // Method is a string containing the method name to invoke. + Method string `json:"method"` + // Params is either a struct or an array with the parameters of the method. + Params *json.RawMessage `json:"params,omitempty"` + // The id of this request, used to tie the Response back to the request. + // Will be either a string or a number. If not set, the Request is a notify, + // and no response is possible. + ID *ID `json:"id,omitempty"` +} + +// Response is a reply to a Request. +// It will always have the ID field set to tie it back to a request, and will +// have either the Result or Error fields set depending on whether it is a +// success or failure response. +type Response struct { + // VersionTag is always encoded as the string "2.0" + VersionTag VersionTag `json:"jsonrpc"` + // Result is the response value, and is required on success. + Result *json.RawMessage `json:"result,omitempty"` + // Error is a structured error response if the call fails. + Error *Error `json:"error,omitempty"` + // ID must be set and is the identifier of the Request this is a response to. + ID *ID `json:"id,omitempty"` +} + +// Error represents a structured error in a Response. +type Error struct { + // Code is an error code indicating the type of failure. + Code int64 `json:"code"` + // Message is a short description of the error. + Message string `json:"message"` + // Data is optional structured data containing additional information about the error. + Data *json.RawMessage `json:"data"` +} + +// VersionTag is a special 0 sized struct that encodes as the jsonrpc version +// tag. +// It will fail during decode if it is not the correct version tag in the +// stream. +type VersionTag struct{} + +// ID is a Request identifier. +// Only one of either the Name or Number members will be set, using the +// number form if the Name is the empty string. +type ID struct { + Name string + Number int64 +} + +// IsNotify returns true if this request is a notification. +func (r *Request) IsNotify() bool { + return r.ID == nil +} + +func (err *Error) Error() string { + if err == nil { + return "" + } + return err.Message +} + +func (VersionTag) MarshalJSON() ([]byte, error) { + return json.Marshal("2.0") +} + +func (VersionTag) UnmarshalJSON(data []byte) error { + version := "" + if err := json.Unmarshal(data, &version); err != nil { + return err + } + if version != "2.0" { + return fmt.Errorf("Invalid RPC version %v", version) + } + return nil +} + +// String returns a string representation of the ID. +// The representation is non ambiguous, string forms are quoted, number forms +// are preceded by a # +func (id *ID) String() string { + if id.Name != "" { + return strconv.Quote(id.Name) + } + return "#" + strconv.FormatInt(id.Number, 10) +} + +func (id *ID) MarshalJSON() ([]byte, error) { + if id.Name != "" { + return json.Marshal(id.Name) + } + return json.Marshal(id.Number) +} + +func (id *ID) UnmarshalJSON(data []byte) error { + *id = ID{} + if err := json.Unmarshal(data, &id.Number); err == nil { + return nil + } + return json.Unmarshal(data, &id.Name) +}