1use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct WsRequest {
16 pub id: Uuid,
17 pub method: String,
18 pub params: Value,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct WsResponse {
38 pub id: Uuid,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub result: Option<Value>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub error: Option<String>,
43}
44
45impl WsResponse {
46 pub fn ok(id: Uuid, result: Value) -> Self {
47 Self {
48 id,
49 result: Some(result),
50 error: None,
51 }
52 }
53
54 pub fn err(id: Uuid, error: String) -> Self {
55 Self {
56 id,
57 result: None,
58 error: Some(error),
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct WsEvent {
66 pub event: String,
67 pub payload: Value,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(untagged)]
82pub enum WsMessage {
83 Request(WsRequest),
84 Response(WsResponse),
85 Event(WsEvent),
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use serde_json::json;
92
93 #[test]
94 fn ws_request_roundtrip() {
95 let id = Uuid::new_v4();
96 let req = WsRequest {
97 id,
98 method: "control".into(),
99 params: json!({"foo": 42}),
100 };
101 let json = serde_json::to_string(&req).unwrap();
102 let decoded: WsRequest = serde_json::from_str(&json).unwrap();
103 assert_eq!(decoded.id, id);
104 assert_eq!(decoded.method, "control");
105 assert_eq!(decoded.params, json!({"foo": 42}));
106 }
107
108 #[test]
109 fn ws_response_ok_roundtrip() {
110 let id = Uuid::new_v4();
111 let resp = WsResponse::ok(id, json!({"status": "running"}));
112 let json = serde_json::to_string(&resp).unwrap();
113 let decoded: WsResponse = serde_json::from_str(&json).unwrap();
114 assert_eq!(decoded.id, id);
115 assert_eq!(decoded.result, Some(json!({"status": "running"})));
116 assert!(decoded.error.is_none());
117 }
118
119 #[test]
120 fn ws_response_err_roundtrip() {
121 let id = Uuid::new_v4();
122 let resp = WsResponse::err(id, "something failed".into());
123 let json = serde_json::to_string(&resp).unwrap();
124 let decoded: WsResponse = serde_json::from_str(&json).unwrap();
125 assert_eq!(decoded.id, id);
126 assert!(decoded.result.is_none());
127 assert_eq!(decoded.error.as_deref(), Some("something failed"));
128 }
129
130 #[test]
131 fn ws_event_roundtrip() {
132 let evt = WsEvent {
133 event: "log".into(),
134 payload: json!({"line": "hello"}),
135 };
136 let json = serde_json::to_string(&evt).unwrap();
137 let decoded: WsEvent = serde_json::from_str(&json).unwrap();
138 assert_eq!(decoded.event, "log");
139 assert_eq!(decoded.payload, json!({"line": "hello"}));
140 }
141
142 #[test]
143 fn ws_message_dispatches_to_response() {
144 let id = Uuid::new_v4();
145 let json = json!({"id": id, "result": {"ok": true}}).to_string();
146 let msg: WsMessage = serde_json::from_str(&json).unwrap();
147 assert!(matches!(msg, WsMessage::Response(_)));
148 }
149
150 #[test]
151 fn ws_message_dispatches_to_event() {
152 let json = json!({"event": "log", "payload": {}}).to_string();
153 let msg: WsMessage = serde_json::from_str(&json).unwrap();
154 assert!(matches!(msg, WsMessage::Event(_)));
155 }
156
157 #[test]
158 fn ws_response_ok_helper() {
159 let id = Uuid::new_v4();
160 let resp = WsResponse::ok(id, json!("done"));
161 assert_eq!(resp.result, Some(json!("done")));
162 assert!(resp.error.is_none());
163 }
164
165 #[test]
166 fn ws_response_err_helper() {
167 let id = Uuid::new_v4();
168 let resp = WsResponse::err(id, "bad".into());
169 assert!(resp.result.is_none());
170 assert_eq!(resp.error.as_deref(), Some("bad"));
171 }
172
173 #[test]
174 fn ws_request_nil_id_rejected() {
175 let bad = r#"{"id": null}"#;
177 assert!(serde_json::from_str::<WsRequest>(bad).is_err());
178 }
179
180 #[test]
181 fn ws_message_unknown_shape() {
182 let bad = json!({"random": "stuff"}).to_string();
184 assert!(serde_json::from_str::<WsMessage>(&bad).is_err());
185 }
186
187 #[test]
200 fn ws_response_some_null_equals_none_on_wire() {
201 let id = Uuid::new_v4();
202 let with_some_null = WsResponse {
203 id,
204 result: Some(serde_json::Value::Null),
205 error: None,
206 };
207 let with_none = WsResponse {
208 id,
209 result: None,
210 error: None,
211 };
212
213 let json_some = serde_json::to_string(&with_some_null).unwrap();
217 let json_none = serde_json::to_string(&with_none).unwrap();
218 assert!(
219 json_some.contains("\"result\":null") || json_some == json_none,
220 "expected Some(Null) to serialize as result:null or be omitted; got {json_some}"
221 );
222
223 let from_some: WsResponse = serde_json::from_str(&json_some).unwrap();
225 let from_none: WsResponse = serde_json::from_str(&json_none).unwrap();
226 assert_eq!(from_some.result, None, "Some(Null) must round-trip to None");
227 assert_eq!(from_none.result, None);
228 }
229
230 use proptest::prelude::*;
248
249 fn arb_json_value() -> impl Strategy<Value = serde_json::Value> {
251 use serde_json::Value;
252 let leaf = prop_oneof![
253 Just(Value::Null),
254 any::<bool>().prop_map(Value::Bool),
255 any::<i64>().prop_map(|n| Value::Number(n.into())),
256 (any::<i32>()).prop_map(|n| serde_json::Number::from_f64(f64::from(n))
263 .map(Value::Number)
264 .unwrap_or(Value::Null)),
265 "[a-zA-Z0-9 _-]{0,32}".prop_map(Value::String),
266 ];
267 leaf.prop_recursive(
268 3, 16, 4, |inner| {
272 prop_oneof![
273 prop::collection::vec(inner.clone(), 0..4).prop_map(Value::Array),
274 prop::collection::hash_map("[a-zA-Z][a-zA-Z0-9_]{0,8}", inner, 0..4)
275 .prop_map(|m| Value::Object(m.into_iter().collect())),
276 ]
277 },
278 )
279 }
280
281 fn arb_method() -> impl Strategy<Value = String> {
282 "[a-z][a-z_]{0,16}".prop_map(|s| s.to_string())
283 }
284
285 fn arb_event() -> impl Strategy<Value = String> {
286 "[a-z][a-z_]{0,16}".prop_map(|s| s.to_string())
287 }
288
289 fn arb_ws_request() -> impl Strategy<Value = WsRequest> {
290 (any::<u128>(), arb_method(), arb_json_value()).prop_map(|(id, method, params)| WsRequest {
291 id: Uuid::from_u128(id),
292 method,
293 params,
294 })
295 }
296
297 fn arb_ws_response() -> impl Strategy<Value = WsResponse> {
298 (
306 any::<u128>(),
307 prop::option::of(arb_json_value().prop_filter("not Value::Null", |v| !v.is_null())),
308 prop::option::of("[a-z0-9 ]{0,32}".prop_map(|s| s.to_string())),
309 )
310 .prop_map(|(id, result, error)| WsResponse {
311 id: Uuid::from_u128(id),
312 result,
313 error,
314 })
315 }
316
317 fn arb_ws_event() -> impl Strategy<Value = WsEvent> {
318 (arb_event(), arb_json_value()).prop_map(|(event, payload)| WsEvent { event, payload })
319 }
320
321 proptest! {
322 #![proptest_config(ProptestConfig::with_cases(2000))]
323
324 #[test]
326 fn prop_request_roundtrip(req in arb_ws_request()) {
327 let json = serde_json::to_string(&req).unwrap();
328 let decoded: WsRequest = serde_json::from_str(&json)
329 .expect("request must deserialize");
330 prop_assert_eq!(decoded.id, req.id);
331 prop_assert_eq!(decoded.method, req.method);
332 prop_assert_eq!(decoded.params, req.params);
333 }
334
335 #[test]
337 fn prop_response_roundtrip(resp in arb_ws_response()) {
338 let json = serde_json::to_string(&resp).unwrap();
339 let decoded: WsResponse = serde_json::from_str(&json)
340 .expect("response must deserialize");
341 prop_assert_eq!(decoded.id, resp.id);
342 prop_assert_eq!(decoded.result, resp.result);
343 prop_assert_eq!(decoded.error, resp.error);
344 }
345
346 #[test]
348 fn prop_event_roundtrip(evt in arb_ws_event()) {
349 let json = serde_json::to_string(&evt).unwrap();
350 let decoded: WsEvent = serde_json::from_str(&json)
351 .expect("event must deserialize");
352 prop_assert_eq!(decoded.event, evt.event);
353 prop_assert_eq!(decoded.payload, evt.payload);
354 }
355
356 #[test]
360 fn prop_message_request_does_not_escape(req in arb_ws_request()) {
361 let msg = WsMessage::Request(req.clone());
362 let json = serde_json::to_string(&msg).unwrap();
363 let decoded: WsMessage = serde_json::from_str(&json)
364 .expect("wsmessage must deserialize");
365 match decoded {
366 WsMessage::Request(r) => {
367 prop_assert_eq!(r.id, req.id);
368 prop_assert_eq!(r.method, req.method);
369 prop_assert_eq!(r.params, req.params);
370 }
371 other => prop_assert!(
372 false,
373 "Request escaped to other variant: {:?}",
374 other
375 ),
376 }
377 }
378
379 #[test]
381 fn prop_message_response_does_not_escape(resp in arb_ws_response()) {
382 let msg = WsMessage::Response(resp.clone());
383 let json = serde_json::to_string(&msg).unwrap();
384 let decoded: WsMessage = serde_json::from_str(&json)
385 .expect("wsmessage must deserialize");
386 match decoded {
387 WsMessage::Response(r) => {
388 prop_assert_eq!(r.id, resp.id);
389 prop_assert_eq!(r.result, resp.result);
390 prop_assert_eq!(r.error, resp.error);
391 }
392 other => prop_assert!(
393 false,
394 "Response escaped to other variant: {:?}",
395 other
396 ),
397 }
398 }
399
400 #[test]
402 fn prop_message_event_does_not_escape(evt in arb_ws_event()) {
403 let msg = WsMessage::Event(evt.clone());
404 let json = serde_json::to_string(&msg).unwrap();
405 let decoded: WsMessage = serde_json::from_str(&json)
406 .expect("wsmessage must deserialize");
407 match decoded {
408 WsMessage::Event(e) => {
409 prop_assert_eq!(e.event, evt.event);
410 prop_assert_eq!(e.payload, evt.payload);
411 }
412 other => prop_assert!(
413 false,
414 "Event escaped to other variant: {:?}",
415 other
416 ),
417 }
418 }
419 }
420}