Skip to main content

infinity_bridge_wire/
msg.rs

1use alloc::string::String;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(tag = "t")]
7pub enum WireMsg {
8    // ── Connection lifecycle ─────────────────────────────────────────
9    /// Sent by the relay gauge immediately after WebSocket connect.
10    #[serde(rename = "hello")]
11    Hello(HelloPayload),
12
13    /// Keepalive probe. Originated by the host, relayed to the gauge.
14    #[serde(rename = "ping")]
15    Ping {
16        /// Millisecond timestamp (originator's clock).
17        #[serde(default)]
18        ts: Option<u64>,
19    },
20
21    /// Keepalive response. Sent by the relay back to the host.
22    #[serde(rename = "pong")]
23    Pong {
24        /// Echoed timestamp from the ping.
25        #[serde(default)]
26        ts: Option<u64>,
27    },
28
29    // ── Request / response ───────────────────────────────────────────
30    /// Command from host → WASM (routed through relay).
31    /// The WASM side must reply with an [`Ack`] carrying the same `id`.
32    #[serde(rename = "cmd")]
33    Cmd(CmdPayload),
34
35    /// Acknowledgement from WASM → host (routed through relay).
36    #[serde(rename = "ack")]
37    Ack(AckPayload),
38
39    // ── Fire-and-forget ──────────────────────────────────────────────
40    /// Unacknowledged event in either direction.
41    #[serde(rename = "event")]
42    Event(EventPayload),
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct HelloPayload {
47    /// Arbitrary client identifier (e.g. `"msfs-gauge"`).
48    #[serde(default)]
49    pub client: Option<String>,
50
51    /// Aircraft type (e.g. `"DC-10"`).
52    #[serde(default)]
53    pub aircraft: Option<String>,
54
55    /// Tail number or livery identifier.
56    #[serde(default)]
57    pub tail: Option<String>,
58
59    /// Session identifier — unique per flight session.
60    #[serde(default)]
61    pub session: Option<String>,
62
63    /// Protocol version spoken by this client.
64    #[serde(default)]
65    pub v: Option<u32>,
66
67    /// Arbitrary extra metadata the consumer wants to pass on connect.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub meta: Option<Value>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct CmdPayload {
74    /// Correlation ID — the ack must carry the same value.
75    pub id: String,
76
77    /// Optional command name for routing on the WASM side.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub name: Option<String>,
80
81    /// Arbitrary JSON payload.
82    pub payload: Value,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AckPayload {
87    /// Correlation ID matching the originating [`CmdPayload::id`].
88    pub id: String,
89
90    /// `true` if the command succeeded.
91    pub ok: bool,
92
93    /// Error description when `ok == false`.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub error: Option<String>,
96
97    /// Response data when `ok == true`.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub response: Option<Value>,
100
101    /// Set to `true` if this is a duplicate ack (idempotency hit).
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub duplicate: Option<bool>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct EventPayload {
108    /// Event name for routing (e.g. `"state_changed"`).
109    pub name: String,
110
111    /// Arbitrary JSON data.
112    pub data: Value,
113}
114
115impl AckPayload {
116    pub fn ok(id: String, response: Value) -> Self {
117        Self {
118            id,
119            ok: true,
120            error: None,
121            response: Some(response),
122            duplicate: None,
123        }
124    }
125
126    pub fn err(id: String, error: String) -> Self {
127        Self {
128            id,
129            ok: false,
130            error: Some(error),
131            response: None,
132            duplicate: None,
133        }
134    }
135
136    pub fn duplicate(id: String) -> Self {
137        Self {
138            id,
139            ok: true,
140            error: None,
141            response: None,
142            duplicate: Some(true),
143        }
144    }
145}
146
147impl CmdPayload {
148    pub fn new(id: String, payload: Value) -> Self {
149        Self {
150            id,
151            name: None,
152            payload,
153        }
154    }
155
156    pub fn named(id: String, name: impl Into<String>, payload: Value) -> Self {
157        Self {
158            id,
159            name: Some(name.into()),
160            payload,
161        }
162    }
163}
164
165impl EventPayload {
166    pub fn new(name: impl Into<String>, data: Value) -> Self {
167        Self {
168            name: name.into(),
169            data,
170        }
171    }
172}
173
174impl WireMsg {
175    pub fn to_json(&self) -> Result<String, serde_json::Error> {
176        serde_json::to_string(self)
177    }
178
179    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
180        serde_json::from_str(json)
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use serde_json::json;
188
189    #[test]
190    fn round_trip_cmd() {
191        let cmd = WireMsg::Cmd(CmdPayload::named(
192            "abc-123".into(),
193            "get_state",
194            json!({"key": "value"}),
195        ));
196        let json = cmd.to_json().unwrap();
197        let parsed = WireMsg::from_json(&json).unwrap();
198
199        match parsed {
200            WireMsg::Cmd(c) => {
201                assert_eq!(c.id, "abc-123");
202                assert_eq!(c.name.as_deref(), Some("get_state"));
203                assert_eq!(c.payload, json!({"key": "value"}));
204            }
205            other => panic!("expected Cmd, got {:?}", other),
206        }
207    }
208
209    #[test]
210    fn round_trip_ack_ok() {
211        let ack = WireMsg::Ack(AckPayload::ok("abc-123".into(), json!(42)));
212        let json = ack.to_json().unwrap();
213        let parsed = WireMsg::from_json(&json).unwrap();
214
215        match parsed {
216            WireMsg::Ack(a) => {
217                assert!(a.ok);
218                assert_eq!(a.response, Some(json!(42)));
219                assert!(a.error.is_none());
220            }
221            other => panic!("expected Ack, got {:?}", other),
222        }
223    }
224
225    #[test]
226    fn round_trip_ack_err() {
227        let ack = WireMsg::Ack(AckPayload::err("abc-123".into(), "boom".into()));
228        let json = ack.to_json().unwrap();
229        let parsed = WireMsg::from_json(&json).unwrap();
230
231        match parsed {
232            WireMsg::Ack(a) => {
233                assert!(!a.ok);
234                assert_eq!(a.error.as_deref(), Some("boom"));
235            }
236            other => panic!("expected Ack, got {:?}", other),
237        }
238    }
239
240    #[test]
241    fn round_trip_event() {
242        let evt = WireMsg::Event(EventPayload::new(
243            "state_changed",
244            json!({"phase": "cruise"}),
245        ));
246        let json = evt.to_json().unwrap();
247        let parsed = WireMsg::from_json(&json).unwrap();
248
249        match parsed {
250            WireMsg::Event(e) => {
251                assert_eq!(e.name, "state_changed");
252                assert_eq!(e.data, json!({"phase": "cruise"}));
253            }
254            other => panic!("expected Event, got {:?}", other),
255        }
256    }
257
258    #[test]
259    fn round_trip_hello() {
260        let hello = WireMsg::Hello(HelloPayload {
261            client: Some("msfs-gauge".into()),
262            aircraft: Some("DC-10".into()),
263            tail: None,
264            session: Some("12345".into()),
265            v: Some(1),
266            meta: None,
267        });
268        let json = hello.to_json().unwrap();
269        let parsed = WireMsg::from_json(&json).unwrap();
270
271        match parsed {
272            WireMsg::Hello(h) => {
273                assert_eq!(h.client.as_deref(), Some("msfs-gauge"));
274                assert_eq!(h.v, Some(1));
275            }
276            other => panic!("expected Hello, got {:?}", other),
277        }
278    }
279
280    #[test]
281    fn round_trip_ping_pong() {
282        let ping = WireMsg::Ping {
283            ts: Some(1234567890),
284        };
285        let json = ping.to_json().unwrap();
286        assert!(json.contains("\"t\":\"ping\""));
287
288        let pong = WireMsg::Pong {
289            ts: Some(1234567890),
290        };
291        let json = pong.to_json().unwrap();
292        let parsed = WireMsg::from_json(&json).unwrap();
293        match parsed {
294            WireMsg::Pong { ts } => assert_eq!(ts, Some(1234567890)),
295            other => panic!("expected Pong, got {:?}", other),
296        }
297    }
298
299    #[test]
300    fn duplicate_ack() {
301        let ack = WireMsg::Ack(AckPayload::duplicate("abc-123".into()));
302        let json = ack.to_json().unwrap();
303        let parsed = WireMsg::from_json(&json).unwrap();
304
305        match parsed {
306            WireMsg::Ack(a) => {
307                assert!(a.ok);
308                assert_eq!(a.duplicate, Some(true));
309            }
310            other => panic!("expected Ack, got {:?}", other),
311        }
312    }
313
314    #[test]
315    fn unknown_type_fails() {
316        let bad = r#"{"t":"banana","stuff":42}"#;
317        assert!(WireMsg::from_json(bad).is_err());
318    }
319
320    #[test]
321    fn skip_serializing_none_fields() {
322        let ack = AckPayload::ok("id".into(), json!(null));
323        let json = serde_json::to_string(&ack).unwrap();
324        // `error` and `duplicate` should be absent, not null
325        assert!(!json.contains("\"error\""));
326        assert!(!json.contains("\"duplicate\""));
327    }
328}