rpc_router/rpc_message/
notification.rs

1//! Represents a JSON-RPC Notification object.
2
3use crate::RpcRequest;
4use crate::rpc_message::rpc_request_parsing_error::RpcRequestParsingError;
5use crate::rpc_message::support::{extract_value, parse_method, parse_params, validate_version};
6use crate::support::get_json_type;
7use serde::ser::SerializeStruct;
8use serde::{Deserialize, Serialize, Serializer};
9use serde_json::Value;
10
11/// Represents a JSON-RPC Notification object, which is a request without an `id`.
12/// Notifications are inherently unreliable as no response is expected.
13///
14/// Note: Derives `Deserialize` for convenience but custom parsing logic is in `from_value`.
15/// Does not derive `Serialize` as a custom implementation is needed to add `jsonrpc: "2.0"`.
16#[derive(Deserialize, Debug, Clone, PartialEq)]
17pub struct RpcNotification {
18	pub method: String,
19	pub params: Option<Value>,
20}
21
22impl RpcNotification {
23	/// Parses a `serde_json::Value` into an `RpcNotification`.
24	///
25	/// This method performs checks according to the JSON-RPC 2.0 specification:
26	/// 1. Checks if the input is a JSON object.
27	/// 2. Validates the presence and value of the `"jsonrpc": "2.0"` field.
28	/// 3. Validates the presence and type of the `"method"` field (must be a string).
29	/// 4. Validates the type of the `"params"` field (must be array or object if present).
30	/// 5. Ensures no `"id"` field is present (as it's a notification).
31	///
32	/// # Errors
33	/// Returns `RpcRequestParsingError` if any validation fails.
34	pub fn from_value(value: Value) -> Result<RpcNotification, RpcRequestParsingError> {
35		let value_type = get_json_type(&value);
36
37		let Value::Object(mut obj) = value else {
38			return Err(RpcRequestParsingError::RequestInvalidType {
39				actual_type: value_type.to_string(),
40			});
41		};
42
43		// -- Check Version
44		let version_val = extract_value(&mut obj, "jsonrpc");
45		if let Err(version_result) = validate_version(version_val) {
46			// Attempt to get method for better error context (id is None for notifications)
47			let method_val = extract_value(&mut obj, "method");
48			let method = method_val.and_then(|v| v.as_str().map(|s| s.to_string()));
49			return match version_result {
50				Some(v) => Err(RpcRequestParsingError::VersionInvalid {
51					id: None, // Notifications have no ID
52					method,
53					version: v,
54				}),
55				None => Err(RpcRequestParsingError::VersionMissing {
56					id: None, // Notifications have no ID
57					method,
58				}),
59			};
60		}
61
62		// -- Check Method
63		let method_val = extract_value(&mut obj, "method");
64		let method = match parse_method(method_val) {
65			Ok(m) => m,
66			Err(method_result) => {
67				return match method_result {
68					Some(m) => Err(RpcRequestParsingError::MethodInvalidType {
69						id: None, // Notifications have no ID
70						method: m,
71					}),
72					None => Err(RpcRequestParsingError::MethodMissing { id: None }), // Notifications have no ID
73				};
74			}
75		};
76
77		// -- Check Params
78		let params_val = extract_value(&mut obj, "params");
79		let params = parse_params(params_val)?; // Propagates ParamsInvalidType error
80
81		// -- Check for disallowed ID field
82		if let Some(id_val) = extract_value(&mut obj, "id") {
83			return Err(RpcRequestParsingError::NotificationHasId {
84				method: Some(method),
85				id: id_val,
86			});
87		}
88
89		Ok(RpcNotification { method, params })
90	}
91}
92
93// region:    --- Serialize Custom
94
95impl Serialize for RpcNotification {
96	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97	where
98		S: Serializer,
99	{
100		// Determine the number of fields: jsonrpc, method are always present. params is optional.
101		let mut field_count = 2;
102		if self.params.is_some() {
103			field_count += 1;
104		}
105
106		let mut state = serializer.serialize_struct("RpcNotification", field_count)?;
107
108		// Always add "jsonrpc": "2.0"
109		state.serialize_field("jsonrpc", "2.0")?;
110
111		state.serialize_field("method", &self.method)?;
112
113		// Serialize params only if it's Some
114		if let Some(params) = &self.params {
115			state.serialize_field("params", params)?;
116		}
117
118		state.end()
119	}
120}
121
122// endregion: --- Serialize Custom
123
124// region:    --- Froms
125
126impl From<RpcRequest> for RpcNotification {
127	fn from(request: RpcRequest) -> Self {
128		RpcNotification {
129			method: request.method,
130			params: request.params,
131		}
132	}
133}
134
135// endregion: --- Froms
136
137// region:    --- TryFrom
138
139/// Convenient TryFrom, performs strict JSON-RPC 2.0 validation via `RpcNotification::from_value`.
140impl TryFrom<Value> for RpcNotification {
141	type Error = RpcRequestParsingError;
142	fn try_from(value: Value) -> Result<RpcNotification, RpcRequestParsingError> {
143		RpcNotification::from_value(value)
144	}
145}
146
147// endregion: --- TryFrom
148
149// region:    --- Tests
150
151#[cfg(test)]
152mod tests {
153	use super::*;
154	use crate::rpc_message::RpcRequestParsingError;
155	use serde_json::{json, to_value};
156
157	type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>; // For tests.
158
159	// -- Test Data
160	fn notif_value_ok_params_some() -> Value {
161		json!({
162			"jsonrpc": "2.0",
163			"method": "updateState",
164			"params": {"value": 123}
165		})
166	}
167
168	fn notif_value_ok_params_none() -> Value {
169		json!({
170			"jsonrpc": "2.0",
171			"method": "ping"
172		})
173	}
174
175	fn notif_value_ok_params_arr() -> Value {
176		json!({
177			"jsonrpc": "2.0",
178			"method": "notifyUsers",
179			"params": ["user1", "user2"]
180		})
181	}
182
183	fn notif_value_fail_id_present() -> Value {
184		json!({
185			"jsonrpc": "2.0",
186			"id": 888, // ID is not allowed in notifications
187			"method": "updateState",
188			"params": {"value": 123}
189		})
190	}
191
192	fn notif_value_fail_version_missing() -> Value {
193		json!({
194			// "jsonrpc": "2.0", // Missing
195			"method": "updateState"
196		})
197	}
198
199	fn notif_value_fail_version_invalid() -> Value {
200		json!({
201			"jsonrpc": "1.0", // Invalid
202			"method": "updateState"
203		})
204	}
205
206	fn notif_value_fail_method_missing() -> Value {
207		json!({
208			"jsonrpc": "2.0"
209			// "method": "updateState" // Missing
210		})
211	}
212
213	fn notif_value_fail_method_invalid() -> Value {
214		json!({
215			"jsonrpc": "2.0",
216			"method": 123 // Invalid type
217		})
218	}
219
220	fn notif_value_fail_params_invalid() -> Value {
221		json!({
222			"jsonrpc": "2.0",
223			"method": "update",
224			"params": "not-array-or-object" // Invalid type
225		})
226	}
227
228	// region:    --- Serialize Tests
229	#[test]
230	fn test_rpc_notification_serialize_ok_params_some() -> Result<()> {
231		// -- Setup & Fixtures
232		let notif = RpcNotification {
233			method: "updateState".to_string(),
234			params: Some(json!({"value": 123})),
235		};
236
237		// -- Exec
238		let value = to_value(notif)?;
239
240		// -- Check
241		assert_eq!(value, notif_value_ok_params_some());
242		Ok(())
243	}
244
245	#[test]
246	fn test_rpc_notification_serialize_ok_params_none() -> Result<()> {
247		// -- Setup & Fixtures
248		let notif = RpcNotification {
249			method: "ping".to_string(),
250			params: None,
251		};
252
253		// -- Exec
254		let value = to_value(notif)?;
255
256		// -- Check
257		assert_eq!(value, notif_value_ok_params_none());
258		Ok(())
259	}
260
261	#[test]
262	fn test_rpc_notification_serialize_ok_params_arr() -> Result<()> {
263		// -- Setup & Fixtures
264		let notif = RpcNotification {
265			method: "notifyUsers".to_string(),
266			params: Some(json!(["user1", "user2"])),
267		};
268
269		// -- Exec
270		let value = to_value(notif)?;
271
272		// -- Check
273		assert_eq!(value, notif_value_ok_params_arr());
274		Ok(())
275	}
276	// endregion: --- Serialize Tests
277
278	// region:    --- Deserialize (from_value) Tests
279	#[test]
280	fn test_rpc_notification_from_value_ok_params_some() -> Result<()> {
281		// -- Setup & Fixtures
282		let value = notif_value_ok_params_some();
283		let expected = RpcNotification {
284			method: "updateState".to_string(),
285			params: Some(json!({"value": 123})),
286		};
287
288		// -- Exec
289		let notification = RpcNotification::from_value(value)?;
290
291		// -- Check
292		assert_eq!(notification, expected);
293		Ok(())
294	}
295
296	#[test]
297	fn test_rpc_notification_from_value_ok_params_none() -> Result<()> {
298		// -- Setup & Fixtures
299		let value = notif_value_ok_params_none();
300		let expected = RpcNotification {
301			method: "ping".to_string(),
302			params: None,
303		};
304
305		// -- Exec
306		let notification = RpcNotification::from_value(value)?;
307
308		// -- Check
309		assert_eq!(notification, expected);
310		Ok(())
311	}
312
313	#[test]
314	fn test_rpc_notification_from_value_ok_params_arr() -> Result<()> {
315		// -- Setup & Fixtures
316		let value = notif_value_ok_params_arr();
317		let expected = RpcNotification {
318			method: "notifyUsers".to_string(),
319			params: Some(json!(["user1", "user2"])),
320		};
321
322		// -- Exec
323		let notification = RpcNotification::from_value(value)?;
324
325		// -- Check
326		assert_eq!(notification, expected);
327		Ok(())
328	}
329
330	#[test]
331	fn test_rpc_notification_from_value_fail_id_present() -> Result<()> {
332		// -- Setup & Fixtures
333		let value = notif_value_fail_id_present();
334
335		// -- Exec
336		let result = RpcNotification::from_value(value);
337
338		// -- Check
339		assert!(matches!(
340			result,
341			Err(RpcRequestParsingError::NotificationHasId { method: Some(_), id: _ })
342		));
343		if let Err(RpcRequestParsingError::NotificationHasId { method, id }) = result {
344			assert_eq!(method.unwrap(), "updateState");
345			assert_eq!(id, json!(888));
346		} else {
347			panic!("Expected NotificationHasId error");
348		}
349		Ok(())
350	}
351
352	#[test]
353	fn test_rpc_notification_from_value_fail_version_missing() -> Result<()> {
354		// -- Setup & Fixtures
355		let value = notif_value_fail_version_missing();
356
357		// -- Exec
358		let result = RpcNotification::from_value(value);
359
360		// -- Check
361		assert!(matches!(
362			result,
363			Err(RpcRequestParsingError::VersionMissing {
364				id: None,
365				method: Some(_)
366			})
367		));
368		if let Err(RpcRequestParsingError::VersionMissing { id, method }) = result {
369			assert!(id.is_none());
370			assert_eq!(method.unwrap(), "updateState");
371		} else {
372			panic!("Expected VersionMissing error");
373		}
374		Ok(())
375	}
376
377	#[test]
378	fn test_rpc_notification_from_value_fail_version_invalid() -> Result<()> {
379		// -- Setup & Fixtures
380		let value = notif_value_fail_version_invalid();
381
382		// -- Exec
383		let result = RpcNotification::from_value(value);
384
385		// -- Check
386		assert!(matches!(
387			result,
388			Err(RpcRequestParsingError::VersionInvalid {
389				id: None,
390				method: Some(_),
391				version: _
392			})
393		));
394		if let Err(RpcRequestParsingError::VersionInvalid { id, method, version }) = result {
395			assert!(id.is_none());
396			assert_eq!(method.unwrap(), "updateState");
397			assert_eq!(version, json!("1.0"));
398		} else {
399			panic!("Expected VersionInvalid error");
400		}
401		Ok(())
402	}
403
404	#[test]
405	fn test_rpc_notification_from_value_fail_method_missing() -> Result<()> {
406		// -- Setup & Fixtures
407		let value = notif_value_fail_method_missing();
408
409		// -- Exec
410		let result = RpcNotification::from_value(value);
411
412		// -- Check
413		assert!(matches!(
414			result,
415			Err(RpcRequestParsingError::MethodMissing { id: None })
416		));
417		Ok(())
418	}
419
420	#[test]
421	fn test_rpc_notification_from_value_fail_method_invalid() -> Result<()> {
422		// -- Setup & Fixtures
423		let value = notif_value_fail_method_invalid();
424
425		// -- Exec
426		let result = RpcNotification::from_value(value);
427
428		// -- Check
429		assert!(matches!(
430			result,
431			Err(RpcRequestParsingError::MethodInvalidType { id: None, method: _ })
432		));
433		if let Err(RpcRequestParsingError::MethodInvalidType { id, method }) = result {
434			assert!(id.is_none());
435			assert_eq!(method, json!(123));
436		} else {
437			panic!("Expected MethodInvalidType error");
438		}
439		Ok(())
440	}
441
442	#[test]
443	fn test_rpc_notification_from_value_fail_params_invalid() -> Result<()> {
444		// -- Setup & Fixtures
445		let value = notif_value_fail_params_invalid();
446
447		// -- Exec
448		let result = RpcNotification::from_value(value);
449
450		// -- Check
451		assert!(matches!(
452			result,
453			Err(RpcRequestParsingError::ParamsInvalidType { actual_type: _ })
454		));
455		if let Err(RpcRequestParsingError::ParamsInvalidType { actual_type }) = result {
456			assert_eq!(actual_type, "String");
457		} else {
458			panic!("Expected ParamsInvalidType error");
459		}
460		Ok(())
461	}
462
463	#[test]
464	fn test_rpc_notification_from_value_fail_not_object() -> Result<()> {
465		// -- Setup & Fixtures
466		let value = json!("not an object");
467
468		// -- Exec
469		let result = RpcNotification::from_value(value);
470
471		// -- Check
472		assert!(matches!(
473			result,
474			Err(RpcRequestParsingError::RequestInvalidType { actual_type: _ })
475		));
476		if let Err(RpcRequestParsingError::RequestInvalidType { actual_type }) = result {
477			assert_eq!(actual_type, "String");
478		} else {
479			panic!("Expected RequestInvalidType error");
480		}
481		Ok(())
482	}
483	// endregion: --- Deserialize (from_value) Tests
484}
485// endregion: --- Tests