request.go raw

   1  // Copyright 2018 Adam S Levy
   2  //
   3  // Permission is hereby granted, free of charge, to any person obtaining a copy
   4  // of this software and associated documentation files (the "Software"), to
   5  // deal in the Software without restriction, including without limitation the
   6  // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
   7  // sell copies of the Software, and to permit persons to whom the Software is
   8  // furnished to do so, subject to the following conditions:
   9  //
  10  // The above copyright notice and this permission notice shall be included in
  11  // all copies or substantial portions of the Software.
  12  //
  13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  18  // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  19  // IN THE SOFTWARE.
  20  
  21  package jsonrpc2
  22  
  23  import (
  24  	"bytes"
  25  	"encoding/json"
  26  	"fmt"
  27  )
  28  
  29  // Request represents a JSON-RPC 2.0 Request or Notification object.
  30  //
  31  // This type is not needed to use the Client or to write MethodFuncs for the
  32  // HTTPRequestHandler.
  33  //
  34  // Request is intended to be used externally to MarshalJSON for custom clients,
  35  // and internally to UnmarshalJSON for the HTTPRequestHandler.
  36  //
  37  // To make a Request, you must populate ID. If ID is empty, the Request is
  38  // treated as a Notification and does not receive a Response. See MarshalJSON
  39  // for more details.
  40  type Request struct {
  41  	// Method is a string containing the name of the method to be invoked.
  42  	Method string `json:"method"`
  43  
  44  	// Params is a structured value that holds the parameter values to be
  45  	// used during the invocation of the method.
  46  	//
  47  	// This member MAY be omitted.
  48  	Params interface{} `json:"params,omitempty"`
  49  
  50  	// ID is an identifier established by the Client that MUST contain a
  51  	// String, Number, or NULL value if included.
  52  	//
  53  	// If it is not included it is assumed to be a notification. The value
  54  	// SHOULD normally not be Null and Numbers SHOULD NOT contain
  55  	// fractional parts.
  56  	ID interface{} `json:"id,omitempty"`
  57  }
  58  
  59  // jRequest adds the required "jsonrpc" field and allows for detecting if the
  60  // "method" field is omitted or null, since an empty method name is not
  61  // explicitly prohibited in the spec, but a missing or null "method" is.
  62  // Additionally we can distinguish between the "id" field being omitted,
  63  // indicating a Notification, and it being null, which is still technically a
  64  // Request.
  65  type jRequest struct {
  66  	// JSONRPC specifies the version of the JSON-RPC protocol. It MUST be
  67  	// exactly "2.0".
  68  	JSONRPC string `json:"jsonrpc"`
  69  
  70  	// Method allows UnmarshalJSON to detect if "method" was omitted or
  71  	// null without bothering users with a pointer.
  72  	Method *string `json:"method"`
  73  
  74  	// *request allows a Request to be used directly while masking its
  75  	// Un/MarshalJSON methods.
  76  	*request
  77  
  78  	// ID allow UnmarshalJSON to distinguish between a missing ID,
  79  	// indicating a Notification, or a null ID, which is technically
  80  	// allowed for Requests, but not recommended.
  81  	ID json.RawMessage `json:"id,omitempty"`
  82  }
  83  
  84  // request masks the Request Un/MarshalJSON methods to avoid recursion.
  85  type request Request
  86  
  87  // MarshalJSON attempts to marshal r into a valid JSON-RPC 2.0 Request or
  88  // Notification object.
  89  //
  90  // If r.ID is nil, then the returned data represents a Notification.
  91  //
  92  // If r.ID is not nil, then the returned data represents a Request. Also, an
  93  // `invalid "id": ...` error is returned if the r.ID does not marshal into a
  94  // valid JSON number, string, or null. Although technically permitted, it is
  95  // not recommended to use json.RawMessage("null") as an ID, as this is used by
  96  // Responses when there is an error parsing "id".
  97  //
  98  // If r.Params is not nil, then an `invalid "params": ...` error is returned if
  99  // it does not marshal into a valid JSON object, array, or null.
 100  //
 101  // An empty Method, though not recommended, is technically valid and does not
 102  // cause an error.
 103  func (r Request) MarshalJSON() ([]byte, error) {
 104  	jR := jRequest{
 105  		JSONRPC: version,
 106  		Method:  &r.Method,
 107  		request: (*request)(&r),
 108  	}
 109  	if r.ID != nil {
 110  		id, err := json.Marshal(r.ID)
 111  		if err != nil {
 112  			return nil, fmt.Errorf(`invalid "id": %w`, err)
 113  		}
 114  		if err := validateID(id); err != nil {
 115  			return nil, err
 116  		}
 117  		jR.ID = id
 118  	}
 119  	if r.Params != nil {
 120  		params, err := json.Marshal(r.Params)
 121  		if err != nil {
 122  			return nil, fmt.Errorf(`invalid "params": %w`, err)
 123  		}
 124  		if err := validateParams(params); err != nil {
 125  			return nil, err
 126  		}
 127  		r.Params = json.RawMessage(params)
 128  	}
 129  	return json.Marshal(jR)
 130  }
 131  
 132  // UnmarshalJSON attempts to unmarshal a JSON-RPC 2.0 Request or Notification
 133  // into r and then validates it.
 134  //
 135  // To allow for precise JSON type validation and to avoid unneeded unmarshaling
 136  // by the HTTPRequestHandler, "id" and "params" are both unmarshaled into
 137  // json.RawMessage. After a successful call, r.ID and r.Params are set to a
 138  // json.RawMessage that is either nil or contains the raw JSON for the
 139  // respective field. So, if no error is returned, this is guaranteed to not
 140  // panic:
 141  //      id, params := r.ID.(json.RawMessage), r.Params.(json.RawMessage)
 142  //
 143  // If "id" is omitted, then r.ID is set to json.RawMessage(nil). If "id" is
 144  // null, then r.ID is set to json.RawMessage("null").
 145  //
 146  // If "params" is null or omitted, then r.Params is set to
 147  // json.RawMessage(nil).
 148  //
 149  // If any fields are unknown, an error is returned.
 150  //
 151  // If the "jsonrpc" field is not set to the string "2.0", an `invalid "jsonrpc"
 152  // version: ...` error is be returned.
 153  //
 154  // If the "method" field is omitted or null, a `missing "method"` error is
 155  // returned. An explicitly empty "method" string does not cause an error.
 156  //
 157  // If the "id" value is not a JSON number, string, or null, an `invalid "id":
 158  // ...` error is returned.
 159  //
 160  // If the "params" value is not a JSON array, object, or null, an `invalid
 161  // "params": ...` error is returned.
 162  func (r *Request) UnmarshalJSON(data []byte) error {
 163  	// params stores the "params" JSON if it is not omitted or null.
 164  	var params json.RawMessage
 165  	r.Params = &params
 166  	jR := jRequest{request: (*request)(r)}
 167  
 168  	d := json.NewDecoder(bytes.NewBuffer(data))
 169  	d.DisallowUnknownFields()
 170  	if err := d.Decode(&jR); err != nil {
 171  		return err
 172  	}
 173  
 174  	if jR.JSONRPC != version {
 175  		return fmt.Errorf(`invalid "jsonrpc" version: %q`, jR.JSONRPC)
 176  	}
 177  
 178  	if jR.Method == nil {
 179  		return fmt.Errorf(`missing "method"`)
 180  	}
 181  	r.Method = *jR.Method
 182  
 183  	if jR.ID != nil {
 184  		if err := validateID(jR.ID); err != nil {
 185  			return err
 186  		}
 187  	}
 188  	r.ID = jR.ID
 189  
 190  	if params != nil {
 191  		if err := validateParams(params); err != nil {
 192  			return err
 193  		}
 194  	}
 195  	r.Params = params
 196  
 197  	return nil
 198  }
 199  
 200  // String returns r as a JSON object prefixed with "--> " to indicate an
 201  // outgoing Request.
 202  //
 203  // If r.MarshalJSON returns an error then the error string is returned with
 204  // some context.
 205  func (r Request) String() string {
 206  	b, err := json.Marshal(r)
 207  	if err != nil {
 208  		return fmt.Sprintf("%#v.MarshalJSON(): %v", r, err)
 209  	}
 210  	return "--> " + string(b)
 211  }
 212  
 213  // BatchRequest is a type that implements fmt.Stringer for a slice of Requests.
 214  type BatchRequest []Request
 215  
 216  // String returns br as a JSON array prefixed with "--> " to indicate an
 217  // outgoing BatchRequest and with newlines separating the elements of br.
 218  func (br BatchRequest) String() string {
 219  	s := "--> [\n"
 220  	for i, res := range br {
 221  		s += "  " + res.String()[4:]
 222  		if i < len(br)-1 {
 223  			s += ","
 224  		}
 225  		s += "\n"
 226  	}
 227  	return s + "]"
 228  }
 229