1use serde::Serialize;
8
9use crate::executor_contract::{MeshControlEnvelope, MeshObjectReadReason};
10
11pub const DATA_CONNECTED: &str = "dataConnected";
14pub const DATA_MESSAGE: &str = "dataMessage";
15pub const DATA_BINARY: &str = "dataBinary";
16pub const DATA_CLOSED: &str = "dataClosed";
17pub const DATA_ERROR: &str = "dataError";
18pub const DATA_INCOMING_CALL: &str = "dataIncomingCall";
19
20#[derive(Debug, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct DataConnectedPayload {
23 pub conn_id: u64,
24 pub peer: String,
25}
26
27#[derive(Debug, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct DataMessagePayload {
30 pub conn_id: u64,
31 pub data: String,
32}
33
34#[derive(Debug, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct DataBinaryPayload {
37 pub conn_id: u64,
38 pub data_base64: String,
39}
40
41#[derive(Debug, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct DataClosedPayload {
44 pub conn_id: u64,
45 pub reason: String,
46}
47
48#[derive(Debug, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct DataErrorPayload {
51 pub conn_id: u64,
52 pub error: String,
53}
54
55#[derive(Debug, Serialize)]
56#[serde(rename_all = "camelCase")]
57pub struct DataIncomingCallPayload {
58 pub conn_id: u64,
59 pub peer: String,
60}
61
62pub const MEDIA_TRACK_READY: &str = "mediaTrackReady";
65pub const MEDIA_CONNECTED: &str = "mediaConnected";
66pub const MEDIA_REMOTE_TRACK: &str = "mediaRemoteTrack";
67pub const MEDIA_CLOSED: &str = "mediaClosed";
68pub const MEDIA_ERROR: &str = "mediaError";
69pub const MEDIA_INCOMING_CALL: &str = "mediaIncomingCall";
70pub const MEDIA_SIGNALING_PROGRESS: &str = "mediaSignalingProgress";
71pub const MEDIA_TRACK_STOPPED: &str = "mediaTrackStopped";
72
73#[derive(Debug, Serialize)]
74#[serde(rename_all = "camelCase")]
75pub struct MediaTrackReadyPayload {
76 pub track_id: u64,
77 pub kind: String,
78}
79
80#[derive(Debug, Serialize)]
81#[serde(rename_all = "camelCase")]
82pub struct MediaConnectedPayload {
83 pub session_id: u64,
84 pub peer: String,
85}
86
87#[derive(Debug, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct MediaRemoteTrackPayload {
90 pub session_id: u64,
91 pub track_id: u64,
92 pub kind: String,
93}
94
95#[derive(Debug, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub struct MediaClosedPayload {
98 pub session_id: u64,
99 pub reason: String,
100}
101
102#[derive(Debug, Serialize)]
103#[serde(rename_all = "camelCase")]
104pub struct MediaErrorPayload {
105 pub session_id: u64,
106 pub error: String,
107}
108
109#[derive(Debug, Serialize)]
110#[serde(rename_all = "camelCase")]
111pub struct MediaIncomingCallPayload {
112 pub peer: String,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub peer_address: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub group_id: Option<String>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub group_size: Option<u32>,
119}
120
121#[derive(Debug, Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct MediaSignalingProgressPayload {
124 pub session_id: u64,
125 pub stage: String,
126}
127
128#[derive(Debug, Serialize)]
133#[serde(rename_all = "camelCase")]
134pub struct MediaTrackStoppedPayload {
135 pub track_id: u64,
136 pub kind: String,
137 pub session_id: u64,
138}
139
140pub const STATEMENT: &str = "statement";
143
144#[derive(Debug, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub struct StatementPayload {
151 pub author: String,
152 pub channel: String,
153 pub data: String,
154 pub timestamp_ms: u64,
155}
156
157pub const MESH_TOPIC: &str = "meshTopic";
160pub const MESH_QUERY: &str = "meshQuery";
161pub const MESH_REPLY: &str = "meshReply";
162pub const MESH_PRESENCE: &str = "meshPresence";
163pub const MESH_PRIVATE_CONTROL: &str = "meshPrivateControl";
164pub const MESH_PRIVATE_RECEIPT: &str = "meshPrivateReceipt";
165
166#[derive(Debug, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub struct MeshTopicPayload {
169 pub topic: String,
170 pub data_base64: String,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub author: Option<String>,
173}
174
175#[derive(Debug, Serialize)]
176#[serde(rename_all = "camelCase")]
177pub struct MeshQueryPayload {
178 pub request_id: String,
179 pub path: String,
180 pub data_base64: String,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub author: Option<String>,
183}
184
185#[derive(Debug, Serialize)]
186#[serde(rename_all = "camelCase")]
187pub struct MeshReplyPayload {
188 pub request_id: String,
189 pub data_base64: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub reason: Option<MeshObjectReadReason>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub expires_at_ms: Option<u64>,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub author: Option<String>,
196}
197
198#[derive(Debug, Serialize)]
199#[serde(rename_all = "camelCase")]
200pub struct MeshPresencePayload {
201 pub peer_id: String,
202 pub state: String,
203}
204
205#[derive(Debug, Serialize)]
206#[serde(rename_all = "camelCase")]
207pub struct MeshPrivateControlPayload {
208 pub capability: String,
209 pub envelope: MeshControlEnvelope,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub author: Option<String>,
212}
213
214#[derive(Debug, Serialize)]
215#[serde(rename_all = "camelCase")]
216pub struct MeshPrivateReceiptPayload {
217 pub capability: String,
218 pub data_base64: String,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub author: Option<String>,
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 fn json(v: &impl serde::Serialize) -> String {
228 serde_json::to_string(v).unwrap()
229 }
230
231 #[test]
232 fn data_connected_json_shape() {
233 let p = DataConnectedPayload {
234 conn_id: 1,
235 peer: "alice".into(),
236 };
237 assert_eq!(json(&p), r#"{"connId":1,"peer":"alice"}"#);
238 }
239
240 #[test]
241 fn data_message_json_shape() {
242 let p = DataMessagePayload {
243 conn_id: 2,
244 data: "hello".into(),
245 };
246 assert_eq!(json(&p), r#"{"connId":2,"data":"hello"}"#);
247 }
248
249 #[test]
250 fn data_binary_json_shape() {
251 let p = DataBinaryPayload {
252 conn_id: 3,
253 data_base64: "AQID".into(),
254 };
255 assert_eq!(json(&p), r#"{"connId":3,"dataBase64":"AQID"}"#);
256 }
257
258 #[test]
259 fn data_closed_json_shape() {
260 let p = DataClosedPayload {
261 conn_id: 4,
262 reason: "done".into(),
263 };
264 assert_eq!(json(&p), r#"{"connId":4,"reason":"done"}"#);
265 }
266
267 #[test]
268 fn data_error_json_shape() {
269 let p = DataErrorPayload {
270 conn_id: 5,
271 error: "fail".into(),
272 };
273 assert_eq!(json(&p), r#"{"connId":5,"error":"fail"}"#);
274 }
275
276 #[test]
277 fn data_incoming_call_json_shape() {
278 let p = DataIncomingCallPayload {
279 conn_id: 6,
280 peer: "bob".into(),
281 };
282 assert_eq!(json(&p), r#"{"connId":6,"peer":"bob"}"#);
283 }
284
285 #[test]
286 fn media_track_ready_json_shape() {
287 let p = MediaTrackReadyPayload {
288 track_id: 1,
289 kind: "video".into(),
290 };
291 assert_eq!(json(&p), r#"{"trackId":1,"kind":"video"}"#);
292 }
293
294 #[test]
295 fn media_connected_json_shape() {
296 let p = MediaConnectedPayload {
297 session_id: 2,
298 peer: "carol".into(),
299 };
300 assert_eq!(json(&p), r#"{"sessionId":2,"peer":"carol"}"#);
301 }
302
303 #[test]
304 fn media_remote_track_json_shape() {
305 let p = MediaRemoteTrackPayload {
306 session_id: 3,
307 track_id: 10,
308 kind: "audio".into(),
309 };
310 assert_eq!(json(&p), r#"{"sessionId":3,"trackId":10,"kind":"audio"}"#);
311 }
312
313 #[test]
314 fn media_closed_json_shape() {
315 let p = MediaClosedPayload {
316 session_id: 4,
317 reason: "hangup".into(),
318 };
319 assert_eq!(json(&p), r#"{"sessionId":4,"reason":"hangup"}"#);
320 }
321
322 #[test]
323 fn media_error_json_shape() {
324 let p = MediaErrorPayload {
325 session_id: 5,
326 error: "timeout".into(),
327 };
328 assert_eq!(json(&p), r#"{"sessionId":5,"error":"timeout"}"#);
329 }
330
331 #[test]
332 fn media_incoming_call_json_shape() {
333 let p = MediaIncomingCallPayload {
334 peer: "dave".into(),
335 peer_address: None,
336 group_id: None,
337 group_size: None,
338 };
339 assert_eq!(json(&p), r#"{"peer":"dave"}"#);
340 }
341
342 #[test]
343 fn media_incoming_call_group_json_shape() {
344 let p = MediaIncomingCallPayload {
345 peer: "alice".into(),
346 peer_address: Some("alice".into()),
347 group_id: Some("grp1".into()),
348 group_size: Some(3),
349 };
350 assert_eq!(
351 json(&p),
352 r#"{"peer":"alice","peerAddress":"alice","groupId":"grp1","groupSize":3}"#
353 );
354 }
355
356 #[test]
357 fn media_incoming_call_1_to_1_omits_group_fields() {
358 let p = MediaIncomingCallPayload {
359 peer: "bob".into(),
360 peer_address: None,
361 group_id: None,
362 group_size: None,
363 };
364 assert_eq!(json(&p), r#"{"peer":"bob"}"#);
365 }
366
367 #[test]
368 fn media_signaling_progress_json_shape() {
369 let p = MediaSignalingProgressPayload {
370 session_id: 7,
371 stage: "ice".into(),
372 };
373 assert_eq!(json(&p), r#"{"sessionId":7,"stage":"ice"}"#);
374 }
375
376 #[test]
377 fn media_track_stopped_json_shape() {
378 let p = MediaTrackStoppedPayload {
379 track_id: 42,
380 kind: "screen".into(),
381 session_id: 0,
382 };
383 assert_eq!(json(&p), r#"{"trackId":42,"kind":"screen","sessionId":0}"#);
384 }
385
386 #[test]
387 fn statement_json_shape() {
388 let p = StatementPayload {
389 author: "alice".into(),
390 channel: "chat".into(),
391 data: "hello".into(),
392 timestamp_ms: 1710000000000,
393 };
394 assert_eq!(
395 json(&p),
396 r#"{"author":"alice","channel":"chat","data":"hello","timestampMs":1710000000000}"#
397 );
398 }
399
400 #[test]
401 fn mesh_topic_json_shape() {
402 let payload = MeshTopicPayload {
403 topic: "room/1".into(),
404 data_base64: "AQID".into(),
405 author: Some("alice".into()),
406 };
407 assert_eq!(
408 json(&payload),
409 r#"{"topic":"room/1","dataBase64":"AQID","author":"alice"}"#
410 );
411 }
412
413 #[test]
414 fn mesh_query_json_shape() {
415 let payload = MeshQueryPayload {
416 request_id: "req-1".into(),
417 path: "mesh/object/1".into(),
418 data_base64: "AQID".into(),
419 author: None,
420 };
421 assert_eq!(
422 json(&payload),
423 r#"{"requestId":"req-1","path":"mesh/object/1","dataBase64":"AQID"}"#
424 );
425 }
426
427 #[test]
428 fn mesh_reply_json_shape() {
429 let payload = MeshReplyPayload {
430 request_id: "req-1".into(),
431 data_base64: Some("AQID".into()),
432 reason: None,
433 expires_at_ms: None,
434 author: Some("bob".into()),
435 };
436 assert_eq!(
437 json(&payload),
438 r#"{"requestId":"req-1","dataBase64":"AQID","author":"bob"}"#
439 );
440 }
441
442 #[test]
443 fn mesh_reply_negative_json_shape() {
444 let payload = MeshReplyPayload {
445 request_id: "req-2".into(),
446 data_base64: None,
447 reason: Some(MeshObjectReadReason::Expired),
448 expires_at_ms: Some(1710000000000),
449 author: None,
450 };
451 assert_eq!(
452 json(&payload),
453 r#"{"requestId":"req-2","dataBase64":null,"reason":"expired","expiresAtMs":1710000000000}"#
454 );
455 }
456
457 #[test]
458 fn mesh_presence_json_shape() {
459 let payload = MeshPresencePayload {
460 peer_id: "peer-1".into(),
461 state: "up".into(),
462 };
463 assert_eq!(json(&payload), r#"{"peerId":"peer-1","state":"up"}"#);
464 }
465
466 #[test]
467 fn mesh_private_control_json_shape() {
468 let payload = MeshPrivateControlPayload {
469 capability: "mesh-private-capability-1".into(),
470 envelope: MeshControlEnvelope {
471 mode: crate::executor_contract::MeshControlMode::Encrypted,
472 data_base64: "AQID".into(),
473 },
474 author: Some("alice".into()),
475 };
476 assert_eq!(
477 json(&payload),
478 r#"{"capability":"mesh-private-capability-1","envelope":{"mode":"encrypted","dataBase64":"AQID"},"author":"alice"}"#
479 );
480 }
481
482 #[test]
483 fn mesh_private_receipt_json_shape() {
484 let payload = MeshPrivateReceiptPayload {
485 capability: "mesh-private-capability-1".into(),
486 data_base64: "AQID".into(),
487 author: None,
488 };
489 assert_eq!(
490 json(&payload),
491 r#"{"capability":"mesh-private-capability-1","dataBase64":"AQID"}"#
492 );
493 }
494}