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 = ¶ms
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