shep_core/protocol/
wire.rs1use core::fmt;
7
8use bytes::Bytes;
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11use tokio_util::codec::LengthDelimitedCodec;
12
13pub const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
15
16#[must_use]
18pub fn codec() -> LengthDelimitedCodec {
19 LengthDelimitedCodec::builder()
20 .length_field_type::<u32>()
21 .max_frame_length(MAX_FRAME_BYTES)
22 .new_codec()
23}
24
25pub fn encode_frame<T: Serialize>(value: &T) -> Result<Bytes, WireError> {
32 let vec = serde_json::to_vec(value).map_err(|e| WireError::Json(e.to_string()))?;
33 if vec.len() > MAX_FRAME_BYTES {
34 return Err(WireError::FrameTooLarge(vec.len()));
35 }
36 Ok(Bytes::from(vec)) }
38
39pub fn decode_frame<T: DeserializeOwned>(frame: &[u8]) -> Result<T, WireError> {
45 serde_json::from_slice(frame).map_err(|e| WireError::Json(e.to_string()))
46}
47
48#[derive(serde::Deserialize)]
57struct ReplyIdOnly {
58 id: u64,
59 #[allow(dead_code, reason = "present only to narrow the match; never read")]
60 result: serde::de::IgnoredAny,
61}
62
63#[must_use]
71pub fn reply_id(frame: &[u8]) -> Option<u64> {
72 decode_frame::<ReplyIdOnly>(frame).ok().map(|r| r.id)
73}
74
75#[non_exhaustive]
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum WireError {
82 Json(String),
84 FrameTooLarge(usize),
86}
87
88impl fmt::Display for WireError {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 Self::Json(m) => write!(f, "wire frame JSON error: {m}"),
92 Self::FrameTooLarge(n) => {
93 write!(
94 f,
95 "frame of {n} bytes exceeds the {MAX_FRAME_BYTES}-byte limit"
96 )
97 }
98 }
99 }
100}
101
102impl core::error::Error for WireError {}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::protocol::{Envelope, Request};
108
109 #[test]
110 fn encode_decode_round_trip() {
111 let env = Envelope {
112 id: 9,
113 deadline_ms: Some(5000),
114 body: Request::Ping,
115 };
116 let bytes = encode_frame(&env).unwrap();
117 let back: Envelope = decode_frame(&bytes).unwrap();
118 assert_eq!(back, env);
119 }
120
121 #[test]
122 fn decode_rejects_garbage_with_json_error() {
123 assert!(matches!(
124 decode_frame::<Envelope>(b"not json"),
125 Err(WireError::Json(_))
126 ));
127 }
128
129 #[test]
130 fn reply_id_reads_the_id_off_a_real_reply() {
131 assert_eq!(
132 reply_id(br#"{"id":7,"result":{"Ok":{"kind":"Pong"}}}"#),
133 Some(7)
134 );
135 }
136
137 #[test]
138 fn reply_id_is_none_for_an_event() {
139 assert_eq!(reply_id(br#"{"event":"from_the_future","data":{}}"#), None);
140 }
141
142 #[test]
143 fn reply_id_is_none_for_non_json() {
144 assert_eq!(reply_id(b"not json"), None);
145 }
146
147 #[test]
148 fn reply_id_is_none_for_a_future_progress_frame() {
149 assert_eq!(
153 reply_id(br#"{"kind":"progress","id":7,"percent":40}"#),
154 None
155 );
156 }
157
158 #[test]
159 fn codec_uses_u32_prefix_and_max_frame() {
160 let c = codec();
161 assert_eq!(c.max_frame_length(), MAX_FRAME_BYTES);
162 }
163
164 #[tokio::test]
165 async fn framed_stream_round_trip() {
166 use futures_util::{SinkExt, StreamExt};
167 use tokio_util::codec::{FramedRead, FramedWrite};
168
169 let (client, server) = tokio::io::duplex(64 * 1024);
170 let mut writer = FramedWrite::new(client, codec());
171 let mut reader = FramedRead::new(server, codec());
172
173 let env = Envelope {
174 id: 1,
175 deadline_ms: None,
176 body: Request::ListFlock,
177 };
178 writer.send(encode_frame(&env).unwrap()).await.unwrap();
179
180 let frame = reader.next().await.unwrap().unwrap();
181 let back: Envelope = decode_frame(&frame).unwrap();
182 assert_eq!(back, env);
183 }
184}