1use alloc::sync::Arc;
26
27use serde::{Deserialize, Serialize};
28use serde_json::value::RawValue;
29
30use crate::session::{CodecError, EnvelopeCodec, Inbound, Outbound, Payload, RpcError};
31
32#[derive(Clone, Copy, Default)]
34pub struct AimxCodec;
35
36#[derive(Serialize, Deserialize)]
41struct Frame<'a> {
42 t: &'a str,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 id: Option<u64>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 seq: Option<u64>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 last: Option<bool>,
50 #[serde(default, borrow, skip_serializing_if = "Option::is_none")]
51 method: Option<&'a str>,
52 #[serde(default, borrow, skip_serializing_if = "Option::is_none")]
53 topic: Option<&'a str>,
54 #[serde(default, borrow, skip_serializing_if = "Option::is_none")]
55 sub: Option<&'a str>,
56 #[serde(default, borrow, skip_serializing_if = "Option::is_none")]
57 params: Option<&'a RawValue>,
58 #[serde(default, borrow, skip_serializing_if = "Option::is_none")]
59 payload: Option<&'a RawValue>,
60 #[serde(default, borrow, skip_serializing_if = "Option::is_none")]
61 data: Option<&'a RawValue>,
62 #[serde(default, borrow, skip_serializing_if = "Option::is_none")]
63 ok: Option<&'a RawValue>,
64 #[serde(default, borrow, skip_serializing_if = "Option::is_none")]
65 err: Option<&'a str>,
66}
67
68impl<'a> Frame<'a> {
69 fn tagged(t: &'a str) -> Self {
71 Self {
72 t,
73 id: None,
74 seq: None,
75 last: None,
76 method: None,
77 topic: None,
78 sub: None,
79 params: None,
80 payload: None,
81 data: None,
82 ok: None,
83 err: None,
84 }
85 }
86}
87
88fn as_raw(bytes: &[u8]) -> Result<&RawValue, CodecError> {
91 serde_json::from_slice(bytes).map_err(|_| CodecError::Malformed)
92}
93
94fn payload_of(raw: Option<&RawValue>) -> Payload {
97 match raw {
98 Some(r) => Arc::from(r.get().as_bytes()),
99 None => Arc::from(&b"null"[..]),
100 }
101}
102
103fn err_code(e: &RpcError) -> &'static str {
104 match e {
105 RpcError::NotFound => "not_found",
106 RpcError::Denied => "denied",
107 RpcError::Internal => "internal",
108 RpcError::VersionMismatch => "version_mismatch",
109 }
110}
111
112fn code_err(s: &str) -> RpcError {
113 match s {
114 "not_found" => RpcError::NotFound,
115 "denied" => RpcError::Denied,
116 "version_mismatch" => RpcError::VersionMismatch,
117 _ => RpcError::Internal,
118 }
119}
120
121fn write_frame(out: &mut alloc::vec::Vec<u8>, frame: &Frame<'_>) -> Result<(), CodecError> {
122 let bytes = serde_json::to_vec(frame).map_err(|_| CodecError::Malformed)?;
125 out.extend_from_slice(&bytes);
126 Ok(())
127}
128
129fn write_frame_splicing_data(
161 out: &mut alloc::vec::Vec<u8>,
162 frame: &Frame<'_>,
163 data: &[u8],
164) -> Result<(), CodecError> {
165 debug_assert!(frame.data.is_none(), "data must be spliced, not serialized");
166 debug_assert!(
167 frame.ok.is_none() && frame.err.is_none(),
168 "fields declared after `data` must be None so the spliced `data` stays last"
169 );
170 let scaffold = serde_json::to_vec(frame).map_err(|_| CodecError::Malformed)?;
171 let Some((&b'}', head)) = scaffold.split_last() else {
173 return Err(CodecError::Malformed);
174 };
175 out.extend_from_slice(head);
176 out.extend_from_slice(br#","data":"#);
177 out.extend_from_slice(data);
178 out.push(b'}');
179 Ok(())
180}
181
182impl EnvelopeCodec for AimxCodec {
183 fn decode(&self, frame: &[u8]) -> Result<Inbound, CodecError> {
185 let f: Frame = serde_json::from_slice(frame).map_err(|_| CodecError::Malformed)?;
186 match f.t {
187 "req" => Ok(Inbound::Request {
188 id: f.id.ok_or(CodecError::Malformed)?,
189 method: f.method.ok_or(CodecError::Malformed)?.into(),
190 params: payload_of(f.params),
191 }),
192 "sub" => Ok(Inbound::Subscribe {
193 id: f.id.ok_or(CodecError::Malformed)?,
194 topic: f.topic.ok_or(CodecError::Malformed)?.into(),
195 }),
196 "unsub" => Ok(Inbound::Unsubscribe {
197 sub: f.sub.ok_or(CodecError::Malformed)?.into(),
198 }),
199 "write" => Ok(Inbound::Write {
200 topic: f.topic.ok_or(CodecError::Malformed)?.into(),
201 payload: payload_of(f.payload),
202 }),
203 "ping" => Ok(Inbound::Ping),
204 _ => Err(CodecError::Malformed),
205 }
206 }
207
208 fn encode(&self, msg: Outbound<'_>, out: &mut alloc::vec::Vec<u8>) -> Result<(), CodecError> {
209 match msg {
210 Outbound::Reply { id, result } => {
211 let mut frame = Frame::tagged("reply");
212 frame.id = Some(id);
213 match &result {
214 Ok(data) => {
215 let raw = as_raw(data)?;
216 frame.ok = Some(raw);
217 write_frame(out, &frame)
218 }
219 Err(e) => {
220 frame.err = Some(err_code(e));
221 write_frame(out, &frame)
222 }
223 }
224 }
225 Outbound::Event {
226 sub,
227 seq,
228 topic,
229 data,
230 } => {
231 let mut frame = Frame::tagged("event");
232 frame.sub = Some(sub);
233 frame.seq = Some(seq);
234 frame.topic = topic;
235 write_frame_splicing_data(out, &frame, &data)
238 }
239 Outbound::Snapshot {
240 sub,
241 seq,
242 last,
243 topic,
244 data,
245 } => {
246 let raw = as_raw(&data)?;
247 let mut frame = Frame::tagged("snap");
248 frame.sub = Some(sub);
249 frame.seq = Some(seq);
250 frame.last = last.then_some(true);
253 frame.topic = Some(topic);
254 frame.data = Some(raw);
255 write_frame(out, &frame)
256 }
257 Outbound::Pong => write_frame(out, &Frame::tagged("pong")),
258 Outbound::Subscribed { sub } => {
261 let mut frame = Frame::tagged("subscribed");
262 frame.sub = Some(sub);
263 write_frame(out, &frame)
264 }
265 }
266 }
267
268 fn encode_inbound(
270 &self,
271 msg: Inbound,
272 out: &mut alloc::vec::Vec<u8>,
273 ) -> Result<(), CodecError> {
274 match msg {
275 Inbound::Request { id, method, params } => {
276 let raw = as_raw(¶ms)?;
277 let mut frame = Frame::tagged("req");
278 frame.id = Some(id);
279 frame.method = Some(&method);
280 frame.params = Some(raw);
281 write_frame(out, &frame)
282 }
283 Inbound::Subscribe { id, topic } => {
284 let mut frame = Frame::tagged("sub");
285 frame.id = Some(id);
286 frame.topic = Some(&topic);
287 write_frame(out, &frame)
288 }
289 Inbound::Unsubscribe { sub } => {
290 let mut frame = Frame::tagged("unsub");
291 frame.sub = Some(&sub);
292 write_frame(out, &frame)
293 }
294 Inbound::Write { topic, payload } => {
295 let raw = as_raw(&payload)?;
296 let mut frame = Frame::tagged("write");
297 frame.topic = Some(&topic);
298 frame.payload = Some(raw);
299 write_frame(out, &frame)
300 }
301 Inbound::Ping => write_frame(out, &Frame::tagged("ping")),
302 }
303 }
304
305 fn decode_outbound<'a>(&self, frame: &'a [u8]) -> Result<Outbound<'a>, CodecError> {
306 let f: Frame<'a> = serde_json::from_slice(frame).map_err(|_| CodecError::Malformed)?;
307 match f.t {
308 "reply" => {
309 let id = f.id.ok_or(CodecError::Malformed)?;
310 let result = match (f.ok, f.err) {
311 (Some(_), _) => Ok(payload_of(f.ok)),
312 (None, Some(code)) => Err(code_err(code)),
313 (None, None) => return Err(CodecError::Malformed),
314 };
315 Ok(Outbound::Reply { id, result })
316 }
317 "event" => Ok(Outbound::Event {
318 sub: f.sub.ok_or(CodecError::Malformed)?,
319 seq: f.seq.ok_or(CodecError::Malformed)?,
320 topic: f.topic,
321 data: payload_of(f.data),
322 }),
323 "snap" => Ok(Outbound::Snapshot {
324 sub: f.sub.ok_or(CodecError::Malformed)?,
325 seq: f.seq.ok_or(CodecError::Malformed)?,
326 last: f.last.unwrap_or(false),
327 topic: f.topic.ok_or(CodecError::Malformed)?,
328 data: payload_of(f.data),
329 }),
330 "subscribed" => Ok(Outbound::Subscribed {
331 sub: f.sub.ok_or(CodecError::Malformed)?,
332 }),
333 "pong" => Ok(Outbound::Pong),
334 _ => Err(CodecError::Malformed),
335 }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use alloc::string::ToString;
343 use alloc::vec::Vec;
344
345 fn payload(s: &str) -> Payload {
346 Arc::from(s.as_bytes())
347 }
348
349 fn roundtrip_inbound(msg: Inbound) -> Inbound {
351 let codec = AimxCodec;
352 let mut out = Vec::new();
353 codec.encode_inbound(msg, &mut out).expect("encode_inbound");
354 codec.decode(&out).expect("decode")
355 }
356
357 fn encode_outbound(msg: Outbound<'_>) -> Vec<u8> {
359 let codec = AimxCodec;
360 let mut out = Vec::new();
361 codec.encode(msg, &mut out).expect("encode");
362 out
363 }
364
365 #[test]
366 fn request_roundtrip() {
367 match roundtrip_inbound(Inbound::Request {
368 id: 7,
369 method: "record.get".to_string(),
370 params: payload(r#"{"name":"temp"}"#),
371 }) {
372 Inbound::Request { id, method, params } => {
373 assert_eq!(id, 7);
374 assert_eq!(method, "record.get");
375 assert_eq!(¶ms[..], br#"{"name":"temp"}"#);
376 }
377 _ => panic!("expected Request"),
378 }
379 }
380
381 #[test]
382 fn subscribe_and_unsubscribe_roundtrip() {
383 match roundtrip_inbound(Inbound::Subscribe {
384 id: 3,
385 topic: "sensors.#".to_string(),
386 }) {
387 Inbound::Subscribe { id, topic } => {
388 assert_eq!(id, 3);
389 assert_eq!(topic, "sensors.#");
390 }
391 _ => panic!("expected Subscribe"),
392 }
393 match roundtrip_inbound(Inbound::Unsubscribe {
394 sub: "3".to_string(),
395 }) {
396 Inbound::Unsubscribe { sub } => assert_eq!(sub, "3"),
397 _ => panic!("expected Unsubscribe"),
398 }
399 }
400
401 #[test]
402 fn write_roundtrip_splices_payload_verbatim() {
403 match roundtrip_inbound(Inbound::Write {
404 topic: "cfg".to_string(),
405 payload: payload(r#"{"value":{"level":9}}"#),
406 }) {
407 Inbound::Write { topic, payload } => {
408 assert_eq!(topic, "cfg");
409 assert_eq!(&payload[..], br#"{"value":{"level":9}}"#);
410 }
411 _ => panic!("expected Write"),
412 }
413 }
414
415 #[test]
416 fn ping_pong_roundtrip() {
417 assert!(matches!(roundtrip_inbound(Inbound::Ping), Inbound::Ping));
418 let frame = encode_outbound(Outbound::Pong);
419 assert!(matches!(
420 AimxCodec.decode_outbound(&frame).unwrap(),
421 Outbound::Pong
422 ));
423 }
424
425 #[test]
426 fn reply_ok_roundtrip() {
427 let frame = encode_outbound(Outbound::Reply {
428 id: 11,
429 result: Ok(payload(r#"{"status":"success"}"#)),
430 });
431 match AimxCodec.decode_outbound(&frame).unwrap() {
432 Outbound::Reply { id, result } => {
433 assert_eq!(id, 11);
434 assert_eq!(&result.unwrap()[..], br#"{"status":"success"}"#);
435 }
436 _ => panic!("expected Reply"),
437 }
438 }
439
440 #[test]
441 fn reply_err_codes_roundtrip() {
442 for err in [
443 RpcError::NotFound,
444 RpcError::Denied,
445 RpcError::Internal,
446 RpcError::VersionMismatch,
447 ] {
448 let frame = encode_outbound(Outbound::Reply {
449 id: 1,
450 result: Err(err.clone()),
451 });
452 match AimxCodec.decode_outbound(&frame).unwrap() {
453 Outbound::Reply { result, .. } => assert_eq!(result.unwrap_err(), err),
454 _ => panic!("expected Reply"),
455 }
456 }
457 }
458
459 #[test]
460 fn event_roundtrip_without_topic() {
461 let frame = encode_outbound(Outbound::Event {
462 sub: "5",
463 seq: 2,
464 topic: None,
465 data: payload("42"),
466 });
467 assert!(!frame.windows(7).any(|w| w == b"\"topic\""));
469 match AimxCodec.decode_outbound(&frame).unwrap() {
470 Outbound::Event {
471 sub,
472 seq,
473 topic,
474 data,
475 } => {
476 assert_eq!((sub, seq, topic), ("5", 2, None));
477 assert_eq!(&data[..], b"42");
478 }
479 _ => panic!("expected Event"),
480 }
481 }
482
483 #[test]
484 fn event_roundtrip_with_topic() {
485 let frame = encode_outbound(Outbound::Event {
486 sub: "5",
487 seq: 9,
488 topic: Some("temp.vienna"),
489 data: payload(r#"{"c":21.5}"#),
490 });
491 match AimxCodec.decode_outbound(&frame).unwrap() {
492 Outbound::Event { sub, topic, .. } => {
493 assert_eq!(sub, "5");
494 assert_eq!(topic, Some("temp.vienna"));
495 }
496 _ => panic!("expected Event"),
497 }
498 }
499
500 #[test]
505 fn event_encode_splices_data_byte_for_byte() {
506 let with_topic = encode_outbound(Outbound::Event {
507 sub: "5",
508 seq: 9,
509 topic: Some("temp.vienna"),
510 data: payload(r#"{"c":21.5,"a":[1,2,3]}"#),
511 });
512 assert_eq!(
513 with_topic,
514 br#"{"t":"event","seq":9,"topic":"temp.vienna","sub":"5","data":{"c":21.5,"a":[1,2,3]}}"#
515 );
516
517 let without_topic = encode_outbound(Outbound::Event {
518 sub: "5",
519 seq: 2,
520 topic: None,
521 data: payload("42"),
522 });
523 assert_eq!(
524 without_topic,
525 br#"{"t":"event","seq":2,"sub":"5","data":42}"#
526 );
527 }
528
529 #[test]
530 fn snapshot_roundtrip_carries_sub_seq_and_topic() {
531 let frame = encode_outbound(Outbound::Snapshot {
532 sub: "8",
533 seq: 3,
534 last: false,
535 topic: "temp.berlin",
536 data: payload(r#"{"c":18.0}"#),
537 });
538 assert_eq!(
540 frame,
541 br#"{"t":"snap","seq":3,"topic":"temp.berlin","sub":"8","data":{"c":18.0}}"#
542 );
543 match AimxCodec.decode_outbound(&frame).unwrap() {
544 Outbound::Snapshot {
545 sub,
546 seq,
547 last,
548 topic,
549 data,
550 } => {
551 assert_eq!((sub, seq, last, topic), ("8", 3, false, "temp.berlin"));
552 assert_eq!(&data[..], br#"{"c":18.0}"#);
553 }
554 _ => panic!("expected Snapshot"),
555 }
556 }
557
558 #[test]
561 fn last_snapshot_of_a_burst_is_flagged() {
562 let frame = encode_outbound(Outbound::Snapshot {
563 sub: "8",
564 seq: 4,
565 last: true,
566 topic: "temp.berlin",
567 data: payload("1"),
568 });
569 match AimxCodec.decode_outbound(&frame).unwrap() {
570 Outbound::Snapshot { seq, last, .. } => assert_eq!((seq, last), (4, true)),
571 _ => panic!("expected Snapshot"),
572 }
573 }
574
575 #[test]
579 fn snapshot_without_seq_is_malformed() {
580 let frame = br#"{"t":"snap","sub":"8","topic":"temp.berlin","data":1}"#;
581 assert!(matches!(
582 AimxCodec.decode_outbound(frame),
583 Err(CodecError::Malformed)
584 ));
585 }
586
587 #[test]
588 fn subscribed_ack_roundtrip() {
589 let frame = encode_outbound(Outbound::Subscribed { sub: "13" });
590 match AimxCodec.decode_outbound(&frame).unwrap() {
591 Outbound::Subscribed { sub } => assert_eq!(sub, "13"),
592 _ => panic!("expected Subscribed"),
593 }
594 }
595
596 #[test]
597 fn malformed_frames_are_rejected() {
598 let codec = AimxCodec;
599 assert!(codec.decode(b"{not json").is_err());
600 assert!(codec.decode(br#"{"t":"nope"}"#).is_err());
601 assert!(codec.decode(br#"{"t":"sub","id":1}"#).is_err());
603 assert!(codec.decode_outbound(br#"{"t":"reply","id":1}"#).is_err());
604 assert!(codec
606 .decode_outbound(br#"{"t":"snap","topic":"x","data":1}"#)
607 .is_err());
608 }
609}