cuprate_json_rpc/
request.rs

1//! JSON-RPC 2.0 request object.
2
3//---------------------------------------------------------------------------------------------------- Use
4use serde::{Deserialize, Serialize};
5
6use crate::{id::Id, version::Version};
7
8//---------------------------------------------------------------------------------------------------- Request
9/// [The request object](https://www.jsonrpc.org/specification#request_object).
10///
11/// The generic `T` is the body type of the request, i.e. it is the
12/// type that holds both the `method` and `params`.
13#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct Request<T> {
15    /// JSON-RPC protocol version; always `2.0`.
16    pub jsonrpc: Version,
17
18    /// An identifier established by the Client.
19    ///
20    /// If it is not included it is assumed to be a
21    /// [notification](https://www.jsonrpc.org/specification#notification).
22    ///
23    /// ### `None` vs `Some(Id::Null)`
24    /// This field will be completely omitted during serialization if [`None`],
25    /// however if it is `Some(Id::Null)`, it will be serialized as `"id": null`.
26    ///
27    /// Note that the JSON-RPC 2.0 specification discourages the use of `Id::NUll`,
28    /// so if there is no ID needed, consider using `None`.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub id: Option<Id>,
31
32    #[serde(flatten)]
33    /// The `method` and `params` fields.
34    ///
35    /// - `method`: A type that serializes as the name of the method to be invoked.
36    /// - `params`: A structured value that holds the parameter values to be used during the invocation of the method.
37    ///
38    /// As mentioned in the library documentation, there are no `method/params` fields in [`Request`],
39    /// they are both merged in this `body` field which is `#[serde(flatten)]`ed.
40    ///
41    /// ### Invariant
42    /// Your `T` must serialize as `method` and `params` to comply with the specification.
43    pub body: T,
44}
45
46impl<T> Request<T> {
47    /// Create a new [`Self`] with no [`Id`].
48    ///
49    /// ```rust
50    /// use cuprate_json_rpc::Request;
51    ///
52    /// assert_eq!(Request::new("").id, None);
53    /// ```
54    pub const fn new(body: T) -> Self {
55        Self {
56            jsonrpc: Version,
57            id: None,
58            body,
59        }
60    }
61
62    /// Create a new [`Self`] with an [`Id`].
63    ///
64    /// ```rust
65    /// use cuprate_json_rpc::{Id, Request};
66    ///
67    /// assert_eq!(Request::new_with_id(Id::Num(0), "").id, Some(Id::Num(0)));
68    /// ```
69    pub const fn new_with_id(id: Id, body: T) -> Self {
70        Self {
71            jsonrpc: Version,
72            id: Some(id),
73            body,
74        }
75    }
76
77    /// Returns `true` if the request is [notification](https://www.jsonrpc.org/specification#notification).
78    ///
79    /// In other words, if `id` is [`None`], this returns `true`.
80    ///
81    /// ```rust
82    /// use cuprate_json_rpc::{Id, Request};
83    ///
84    /// assert!(Request::new("").is_notification());
85    /// assert!(!Request::new_with_id(Id::Null, "").is_notification());
86    /// ```
87    pub const fn is_notification(&self) -> bool {
88        self.id.is_none()
89    }
90}
91
92//---------------------------------------------------------------------------------------------------- Trait impl
93impl<T> std::fmt::Display for Request<T>
94where
95    T: std::fmt::Display + Serialize,
96{
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        match serde_json::to_string_pretty(self) {
99            Ok(json) => write!(f, "{json}"),
100            Err(_) => Err(std::fmt::Error),
101        }
102    }
103}
104
105//---------------------------------------------------------------------------------------------------- TESTS
106#[cfg(test)]
107mod test {
108    use super::*;
109    use crate::{
110        id::Id,
111        tests::{assert_ser, Body},
112    };
113
114    use pretty_assertions::assert_eq;
115    use serde_json::{json, Value};
116
117    /// Basic serde tests.
118    #[test]
119    fn serde() {
120        let id = Id::Num(123);
121        let body = Body {
122            method: "a_method".into(),
123            params: [0, 1, 2],
124        };
125
126        let req = Request::new_with_id(id, body);
127
128        assert!(!req.is_notification());
129
130        let ser: String = serde_json::to_string(&req).unwrap();
131        let de: Request<Body<[u8; 3]>> = serde_json::from_str(&ser).unwrap();
132
133        assert_eq!(req, de);
134    }
135
136    /// Asserts that fields must be `lowercase`.
137    #[test]
138    #[should_panic(
139        expected = "called `Result::unwrap()` on an `Err` value: Error(\"missing field `jsonrpc`\", line: 1, column: 63)"
140    )]
141    fn lowercase() {
142        let id = Id::Num(123);
143        let body = Body {
144            method: "a_method".into(),
145            params: [0, 1, 2],
146        };
147
148        let req = Request::new_with_id(id, body);
149
150        let ser: String = serde_json::to_string(&req).unwrap();
151        assert_eq!(
152            ser,
153            r#"{"jsonrpc":"2.0","id":123,"method":"a_method","params":[0,1,2]}"#,
154        );
155
156        let mixed_case = r#"{"jSoNRPC":"2.0","ID":123,"method":"a_method","params":[0,1,2]}"#;
157        let de: Request<Body<[u8; 3]>> = serde_json::from_str(mixed_case).unwrap();
158        assert_eq!(de, req);
159    }
160
161    /// Tests that null `id` shows when serializing.
162    #[test]
163    fn request_null_id() {
164        let req = Request::new_with_id(
165            Id::Null,
166            Body {
167                method: "m".into(),
168                params: "p".to_string(),
169            },
170        );
171        let json = json!({
172            "jsonrpc": "2.0",
173            "id": null,
174            "method": "m",
175            "params": "p",
176        });
177
178        assert_ser(&req, &json);
179    }
180
181    /// Tests that a `None` `id` omits the field when serializing.
182    #[test]
183    fn request_none_id() {
184        let req = Request::new(Body {
185            method: "a".into(),
186            params: "b".to_string(),
187        });
188        let json = json!({
189            "jsonrpc": "2.0",
190            "method": "a",
191            "params": "b",
192        });
193
194        assert_ser(&req, &json);
195    }
196
197    /// Tests that omitting `params` omits the field when serializing.
198    #[test]
199    fn request_no_params() {
200        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201        struct NoParamMethod {
202            method: String,
203        }
204
205        let req = Request::new_with_id(
206            Id::Num(123),
207            NoParamMethod {
208                method: "asdf".to_string(),
209            },
210        );
211        let json = json!({
212            "jsonrpc": "2.0",
213            "id": 123,
214            "method": "asdf",
215        });
216
217        assert_ser(&req, &json);
218    }
219
220    /// Tests that tagged enums serialize correctly.
221    #[test]
222    fn request_tagged_enums() {
223        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
224        struct GetHeight {
225            height: u64,
226        }
227
228        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
229        #[serde(tag = "method", content = "params")]
230        #[serde(rename_all = "snake_case")]
231        enum Methods {
232            GetHeight(/* param: */ GetHeight),
233        }
234
235        let req = Request::new_with_id(Id::Num(123), Methods::GetHeight(GetHeight { height: 0 }));
236        let json = json!({
237            "jsonrpc": "2.0",
238            "id": 123,
239            "method": "get_height",
240            "params": {
241                "height": 0,
242            },
243        });
244
245        assert_ser(&req, &json);
246    }
247
248    /// Tests that requests serialize into the expected JSON value.
249    #[test]
250    fn request_is_expected_value() {
251        // Test values: (request, expected_value)
252        let array: [(Request<Body<[u8; 3]>>, Value); 3] = [
253            (
254                Request::new_with_id(
255                    Id::Num(123),
256                    Body {
257                        method: "method_1".into(),
258                        params: [0, 1, 2],
259                    },
260                ),
261                json!({
262                    "jsonrpc": "2.0",
263                    "id": 123,
264                    "method": "method_1",
265                    "params": [0, 1, 2],
266                }),
267            ),
268            (
269                Request::new_with_id(
270                    Id::Null,
271                    Body {
272                        method: "method_2".into(),
273                        params: [3, 4, 5],
274                    },
275                ),
276                json!({
277                    "jsonrpc": "2.0",
278                    "id": null,
279                    "method": "method_2",
280                    "params": [3, 4, 5],
281                }),
282            ),
283            (
284                Request::new_with_id(
285                    Id::Str("string_id".into()),
286                    Body {
287                        method: "method_3".into(),
288                        params: [6, 7, 8],
289                    },
290                ),
291                json!({
292                    "jsonrpc": "2.0",
293                    "method": "method_3",
294                    "id": "string_id",
295                    "params": [6, 7, 8],
296                }),
297            ),
298        ];
299
300        for (request, expected_value) in array {
301            assert_ser(&request, &expected_value);
302        }
303    }
304
305    /// Tests that non-ordered fields still deserialize okay.
306    #[test]
307    fn deserialize_out_of_order_keys() {
308        let expected = Request::new_with_id(
309            Id::Str("id".into()),
310            Body {
311                method: "method".into(),
312                params: [0, 1, 2],
313            },
314        );
315
316        let json = json!({
317            "method": "method",
318            "id": "id",
319            "params": [0, 1, 2],
320            "jsonrpc": "2.0",
321        });
322
323        let resp = serde_json::from_value::<Request<Body<[u8; 3]>>>(json).unwrap();
324        assert_eq!(resp, expected);
325    }
326
327    /// Tests that unknown fields are ignored, and deserialize continues.
328    /// Also that unicode and backslashes work.
329    #[test]
330    fn unknown_fields_and_unicode() {
331        let expected = Request::new_with_id(
332            Id::Str("id".into()),
333            Body {
334                method: "method".into(),
335                params: [0, 1, 2],
336            },
337        );
338
339        let json = json!({
340            "unknown_field": 123,
341            "method": "method",
342            "unknown_field": 123,
343            "id": "id",
344            "\nhello": 123,
345            "params": [0, 1, 2],
346            "\u{00f8}": 123,
347            "jsonrpc": "2.0",
348            "unknown_field": 123,
349        });
350
351        let resp = serde_json::from_value::<Request<Body<[u8; 3]>>>(json).unwrap();
352        assert_eq!(resp, expected);
353    }
354}