1use serde::Serialize;
32use serde::de::DeserializeOwned;
33
34use crate::CodecError;
35
36pub trait VersionedCodec: Sized {
46 fn decode_version(version: u8, body: &[u8]) -> Result<Self, CodecError>;
48
49 fn encode_version(&self, version: u8) -> Result<Vec<u8>, CodecError>;
51}
52
53pub fn encode_framed<T: VersionedCodec>(version: u8, value: &T) -> Result<Vec<u8>, CodecError> {
56 let body = value.encode_version(version)?;
57 let mut out = Vec::with_capacity(1 + body.len());
58 out.push(version);
59 out.extend_from_slice(&body);
60 Ok(out)
61}
62
63pub fn decode_framed<T: VersionedCodec>(
69 min_version: u8,
70 max_version: u8,
71 bytes: &[u8],
72) -> Result<T, CodecError> {
73 let (first, body) = bytes.split_first().ok_or(CodecError::Empty)?;
74 let version = *first;
75 if version < min_version || version > max_version {
76 return Err(CodecError::VersionUnsupported {
77 min: min_version,
78 max: max_version,
79 actual: version,
80 });
81 }
82 T::decode_version(version, body)
83}
84
85pub fn encode_postcard<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {
88 postcard::to_stdvec(value).map_err(CodecError::Encode)
89}
90
91pub fn decode_postcard_exact<T: DeserializeOwned>(body: &[u8]) -> Result<T, CodecError> {
96 let (value, remainder) = postcard::take_from_bytes(body).map_err(CodecError::Decode)?;
97 if !remainder.is_empty() {
98 return Err(CodecError::TrailingBytes {
99 extra: remainder.len(),
100 });
101 }
102 Ok(value)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use serde::Deserialize;
109
110 #[derive(Debug, PartialEq, Serialize, Deserialize)]
111 struct Body {
112 idx: u64,
113 name: String,
114 }
115
116 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120 struct Widget {
121 id: u64,
122 label: String,
123 color: Option<String>,
124 }
125
126 #[derive(Serialize, Deserialize)]
128 struct WidgetV1 {
129 id: u64,
130 label: String,
131 }
132
133 #[derive(Serialize, Deserialize)]
135 struct WidgetV2 {
136 id: u64,
137 label: String,
138 color: Option<String>,
139 }
140
141 impl From<WidgetV1> for Widget {
142 fn from(widget: WidgetV1) -> Self {
143 Widget {
144 id: widget.id,
145 label: widget.label,
146 color: None,
147 }
148 }
149 }
150
151 impl From<WidgetV2> for Widget {
152 fn from(widget: WidgetV2) -> Self {
153 Widget {
154 id: widget.id,
155 label: widget.label,
156 color: widget.color,
157 }
158 }
159 }
160
161 const WIDGET_MIN: u8 = 1;
162 const WIDGET_MAX: u8 = 2;
163
164 impl VersionedCodec for Widget {
165 fn decode_version(version: u8, body: &[u8]) -> Result<Self, CodecError> {
166 match version {
167 1 => decode_postcard_exact::<WidgetV1>(body).map(Into::into),
168 2 => decode_postcard_exact::<WidgetV2>(body).map(Into::into),
169 other => Err(CodecError::VersionUnsupported {
170 min: WIDGET_MIN,
171 max: WIDGET_MAX,
172 actual: other,
173 }),
174 }
175 }
176
177 fn encode_version(&self, version: u8) -> Result<Vec<u8>, CodecError> {
178 match version {
179 1 => {
180 if self.color.is_some() {
184 return Err(CodecError::NotRepresentable { version: 1 });
185 }
186 encode_postcard(&WidgetV1 {
187 id: self.id,
188 label: self.label.clone(),
189 })
190 }
191 2 => encode_postcard(&WidgetV2 {
192 id: self.id,
193 label: self.label.clone(),
194 color: self.color.clone(),
195 }),
196 other => Err(CodecError::VersionUnsupported {
197 min: WIDGET_MIN,
198 max: WIDGET_MAX,
199 actual: other,
200 }),
201 }
202 }
203 }
204
205 #[test]
206 fn postcard_body_roundtrips() {
207 let original = Body {
208 idx: 7,
209 name: "ts".into(),
210 };
211 let bytes = encode_postcard(&original).expect("encode");
212 let decoded: Body = decode_postcard_exact(&bytes).expect("decode");
213 assert_eq!(original, decoded);
214 }
215
216 #[test]
217 fn postcard_body_rejects_trailing_bytes() {
218 let mut bytes = encode_postcard(&Body {
219 idx: 1,
220 name: "x".into(),
221 })
222 .expect("encode");
223 bytes.extend_from_slice(&[0xAB, 0xCD]);
224 assert!(matches!(
225 decode_postcard_exact::<Body>(&bytes),
226 Err(CodecError::TrailingBytes { extra: 2 })
227 ));
228 }
229
230 #[test]
231 fn version_unsupported_carries_range_and_actual() {
232 let err = CodecError::VersionUnsupported {
233 min: 1,
234 max: 2,
235 actual: 9,
236 };
237 assert_eq!(
238 err.to_string(),
239 "version unsupported: 9 outside readable range [1, 2]"
240 );
241 }
242
243 #[test]
244 fn not_representable_names_the_version() {
245 let err = CodecError::NotRepresentable { version: 1 };
246 assert_eq!(err.to_string(), "value not representable at version 1");
247 }
248
249 #[test]
250 fn trait_encode_decode_roundtrips_each_version() {
251 let widget = Widget {
252 id: 42,
253 label: "tso".into(),
254 color: Some("amber".into()),
255 };
256 let body_v2 = widget.encode_version(2).expect("encode v2");
257 let decoded_v2 = Widget::decode_version(2, &body_v2).expect("decode v2");
258 assert_eq!(widget, decoded_v2);
259
260 let gated = Widget {
261 id: 7,
262 label: "old".into(),
263 color: None,
264 };
265 let body_v1 = gated.encode_version(1).expect("encode v1");
266 let decoded_v1 = Widget::decode_version(1, &body_v1).expect("decode v1");
267 assert_eq!(gated, decoded_v1);
268 }
269
270 #[test]
271 fn down_convert_rejects_non_representable_state() {
272 let widget = Widget {
275 id: 1,
276 label: "x".into(),
277 color: Some("blue".into()),
278 };
279 assert!(matches!(
280 widget.encode_version(1),
281 Err(CodecError::NotRepresentable { version: 1 })
282 ));
283 }
284
285 #[test]
286 fn encode_framed_prepends_version_and_matches_body() {
287 let widget = Widget {
288 id: 5,
289 label: "z".into(),
290 color: None,
291 };
292 let framed = encode_framed(1, &widget).expect("encode_framed v1");
293 assert_eq!(framed[0], 1, "leading byte is the version");
294 let expected_body = widget.encode_version(1).expect("encode v1 body");
297 assert_eq!(&framed[1..], expected_body.as_slice());
298 }
299
300 #[test]
301 fn decode_framed_dispatches_and_up_converts() {
302 let v1_body = encode_postcard(&WidgetV1 {
305 id: 9,
306 label: "old".into(),
307 })
308 .expect("body");
309 let mut framed = vec![1u8];
310 framed.extend_from_slice(&v1_body);
311 let widget: Widget = decode_framed(WIDGET_MIN, WIDGET_MAX, &framed).expect("decode v1");
312 assert_eq!(
313 widget,
314 Widget {
315 id: 9,
316 label: "old".into(),
317 color: None
318 }
319 );
320 }
321
322 #[test]
323 fn decode_framed_roundtrips_current_version() {
324 let widget = Widget {
325 id: 1,
326 label: "now".into(),
327 color: Some("red".into()),
328 };
329 let framed = encode_framed(2, &widget).expect("encode");
330 let back: Widget = decode_framed(WIDGET_MIN, WIDGET_MAX, &framed).expect("decode");
331 assert_eq!(widget, back);
332 }
333
334 #[test]
335 fn decode_framed_rejects_out_of_range_version() {
336 let framed = [3u8, 0x00];
337 let err = decode_framed::<Widget>(WIDGET_MIN, WIDGET_MAX, &framed).expect_err("reject");
338 assert!(matches!(
339 err,
340 CodecError::VersionUnsupported {
341 min: 1,
342 max: 2,
343 actual: 3
344 }
345 ));
346 }
347
348 #[test]
349 fn decode_framed_rejects_below_range_version() {
350 let framed = [0u8, 0x00];
351 let err = decode_framed::<Widget>(WIDGET_MIN, WIDGET_MAX, &framed).expect_err("reject");
352 assert!(matches!(
353 err,
354 CodecError::VersionUnsupported {
355 min: 1,
356 max: 2,
357 actual: 0
358 }
359 ));
360 }
361
362 #[test]
363 fn decode_framed_rejects_empty() {
364 let err = decode_framed::<Widget>(WIDGET_MIN, WIDGET_MAX, &[]).expect_err("reject");
365 assert!(matches!(err, CodecError::Empty));
366 }
367
368 use proptest::prelude::*;
369
370 proptest! {
371 #[test]
374 fn framed_roundtrips_any(id in any::<u64>(), label in any::<String>(), color in any::<Option<String>>()) {
375 let gated = Widget { id, label: label.clone(), color: None };
376 let framed_v1 = encode_framed(1, &gated).unwrap();
377 let back_v1: Widget = decode_framed(WIDGET_MIN, WIDGET_MAX, &framed_v1).unwrap();
378 prop_assert_eq!(gated, back_v1);
379
380 let full = Widget { id, label, color };
381 let framed_v2 = encode_framed(2, &full).unwrap();
382 let back_v2: Widget = decode_framed(WIDGET_MIN, WIDGET_MAX, &framed_v2).unwrap();
383 prop_assert_eq!(full, back_v2);
384 }
385 }
386}