Skip to main content

inferadb_ledger_types/
codec.rs

1//! Centralized serialization and deserialization functions.
2//!
3//! This module provides a unified interface for encoding and decoding data
4//! using postcard serialization, with consistent error handling via snafu.
5
6use serde::{Serialize, de::DeserializeOwned};
7use snafu::{ResultExt, Snafu};
8
9/// Error type for codec operations.
10#[derive(Debug, Snafu)]
11pub enum CodecError {
12    /// Encoding failed.
13    #[snafu(display("Encoding failed: {source}"))]
14    Encode {
15        /// The underlying postcard error.
16        source: postcard::Error,
17        /// Location where the error occurred.
18        #[snafu(implicit)]
19        location: snafu::Location,
20    },
21
22    /// Decoding failed.
23    #[snafu(display("Decoding failed: {source}"))]
24    Decode {
25        /// The underlying postcard error.
26        source: postcard::Error,
27        /// Location where the error occurred.
28        #[snafu(implicit)]
29        location: snafu::Location,
30    },
31}
32
33/// Encodes a value to bytes using postcard serialization.
34///
35/// # Errors
36///
37/// Returns `CodecError::Encode` if serialization fails.
38pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {
39    postcard::to_allocvec(value).context(EncodeSnafu)
40}
41
42/// Decodes bytes to a value using postcard deserialization.
43///
44/// # Errors
45///
46/// Returns `CodecError::Decode` if deserialization fails.
47pub fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
48    postcard::from_bytes(bytes).context(DecodeSnafu)
49}
50
51#[cfg(test)]
52#[allow(clippy::unwrap_used, clippy::expect_used, clippy::disallowed_methods)]
53mod tests {
54    use serde::Deserialize;
55    use snafu::ResultExt;
56
57    use super::*;
58
59    // Test encode/decode roundtrip for primitive types
60    #[test]
61    fn test_roundtrip_primitive_u64() {
62        let original: u64 = 42;
63        let bytes = encode(&original).expect("encode u64");
64        let decoded: u64 = decode(&bytes).expect("decode u64");
65        assert_eq!(original, decoded);
66    }
67
68    #[test]
69    fn test_roundtrip_primitive_string() {
70        let original = "hello world".to_string();
71        let bytes = encode(&original).expect("encode string");
72        let decoded: String = decode(&bytes).expect("decode string");
73        assert_eq!(original, decoded);
74    }
75
76    #[test]
77    fn test_roundtrip_primitive_bool() {
78        for original in [true, false] {
79            let bytes = encode(&original).expect("encode bool");
80            let decoded: bool = decode(&bytes).expect("decode bool");
81            assert_eq!(original, decoded);
82        }
83    }
84
85    #[test]
86    fn test_roundtrip_primitive_vec() {
87        let original: Vec<u32> = vec![1, 2, 3, 4, 5];
88        let bytes = encode(&original).expect("encode vec");
89        let decoded: Vec<u32> = decode(&bytes).expect("decode vec");
90        assert_eq!(original, decoded);
91    }
92
93    // Test encode/decode roundtrip for complex structs
94    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95    struct ComplexStruct {
96        id: u64,
97        name: String,
98        data: Vec<u8>,
99        nested: Option<NestedStruct>,
100    }
101
102    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103    struct NestedStruct {
104        value: i32,
105        flag: bool,
106    }
107
108    #[test]
109    fn test_roundtrip_complex_struct() {
110        let original = ComplexStruct {
111            id: 12345,
112            name: "test entity".to_string(),
113            data: vec![0xDE, 0xAD, 0xBE, 0xEF],
114            nested: Some(NestedStruct { value: -42, flag: true }),
115        };
116        let bytes = encode(&original).expect("encode complex struct");
117        let decoded: ComplexStruct = decode(&bytes).expect("decode complex struct");
118        assert_eq!(original, decoded);
119    }
120
121    #[test]
122    fn test_roundtrip_complex_struct_with_none() {
123        let original = ComplexStruct { id: 0, name: String::new(), data: vec![], nested: None };
124        let bytes = encode(&original).expect("encode complex struct with None");
125        let decoded: ComplexStruct = decode(&bytes).expect("decode complex struct with None");
126        assert_eq!(original, decoded);
127    }
128
129    // Test error cases (malformed input)
130    #[test]
131    fn test_decode_malformed_input() {
132        let malformed_bytes = [0xFF, 0xFF, 0xFF, 0xFF];
133        let result: Result<ComplexStruct, _> = decode(&malformed_bytes);
134        assert!(result.is_err());
135        let err = result.unwrap_err();
136        assert!(matches!(err, CodecError::Decode { .. }));
137        // Verify error message contains useful info
138        let display = err.to_string();
139        assert!(display.contains("Decoding failed"));
140    }
141
142    #[test]
143    fn test_decode_truncated_data() {
144        // Encode a struct then truncate the bytes
145        let original = ComplexStruct {
146            id: 12345,
147            name: "test".to_string(),
148            data: vec![1, 2, 3],
149            nested: None,
150        };
151        let bytes = encode(&original).expect("encode");
152        // Truncate to just first 2 bytes
153        let truncated = &bytes[..2.min(bytes.len())];
154        let result: Result<ComplexStruct, _> = decode(truncated);
155        assert!(result.is_err());
156    }
157
158    // Test empty input handling
159    #[test]
160    fn test_decode_empty_input() {
161        let empty: &[u8] = &[];
162        let result: Result<u64, _> = decode(empty);
163        assert!(result.is_err());
164        let err = result.unwrap_err();
165        assert!(matches!(err, CodecError::Decode { .. }));
166    }
167
168    #[test]
169    fn test_encode_empty_vec() {
170        let empty_vec: Vec<u8> = vec![];
171        let bytes = encode(&empty_vec).expect("encode empty vec");
172        let decoded: Vec<u8> = decode(&bytes).expect("decode empty vec");
173        assert_eq!(empty_vec, decoded);
174    }
175
176    #[test]
177    fn test_encode_empty_string() {
178        let empty_string = String::new();
179        let bytes = encode(&empty_string).expect("encode empty string");
180        let decoded: String = decode(&bytes).expect("decode empty string");
181        assert_eq!(empty_string, decoded);
182    }
183
184    // Test error display implementations
185    #[test]
186    fn test_encode_error_display() {
187        // We can't easily trigger an encode error with postcard,
188        // but we can verify the error type structure
189        let malformed: &[u8] = &[0xFF];
190        let result: Result<String, _> = decode(malformed);
191        let err = result.unwrap_err();
192        let display = err.to_string();
193        assert!(display.starts_with("Decoding failed:"));
194    }
195
196    // Test edge cases
197    #[test]
198    fn test_roundtrip_max_u64() {
199        let original: u64 = u64::MAX;
200        let bytes = encode(&original).expect("encode max u64");
201        let decoded: u64 = decode(&bytes).expect("decode max u64");
202        assert_eq!(original, decoded);
203    }
204
205    #[test]
206    fn test_roundtrip_unicode_string() {
207        let original = "Hello 世界 🦀 émoji".to_string();
208        let bytes = encode(&original).expect("encode unicode");
209        let decoded: String = decode(&bytes).expect("decode unicode");
210        assert_eq!(original, decoded);
211    }
212
213    // =========================================================================
214    // Error conversion chain tests (Task 2: Consolidate Error Types)
215    // =========================================================================
216
217    // Test that CodecError::Encode has correct Display output
218    #[test]
219    fn test_codec_error_encode_display() {
220        // Create a decode error (easier to trigger than encode error)
221        let malformed: &[u8] = &[0xFF, 0xFF, 0xFF, 0xFF];
222        let result: Result<u64, _> = decode(malformed);
223        let err = result.unwrap_err();
224
225        // Verify display format matches expected pattern
226        let display = format!("{err}");
227        assert!(
228            display.starts_with("Decoding failed:"),
229            "Expected 'Decoding failed:', got: {display}"
230        );
231    }
232
233    // Test that CodecError::Decode has correct Display output
234    #[test]
235    fn test_codec_error_decode_display() {
236        let empty: &[u8] = &[];
237        let result: Result<String, _> = decode(empty);
238        let err = result.unwrap_err();
239
240        let display = format!("{err}");
241        assert!(
242            display.starts_with("Decoding failed:"),
243            "Expected 'Decoding failed:', got: {display}"
244        );
245    }
246
247    // Test error source chain - CodecError preserves underlying postcard error
248    #[test]
249    fn test_codec_error_source_chain() {
250        use std::error::Error;
251
252        let malformed: &[u8] = &[0xFF];
253        let result: Result<String, _> = decode(malformed);
254        let err = result.unwrap_err();
255
256        // Verify the error has a source (the underlying postcard error)
257        assert!(err.source().is_some(), "CodecError should have a source");
258
259        // The source should be a postcard::Error
260        let source = err.source().unwrap();
261        // Verify source has a Display impl (postcard::Error)
262        let source_display = format!("{source}");
263        assert!(!source_display.is_empty(), "Source should have non-empty display");
264    }
265
266    // Test Debug implementation contains useful info
267    #[test]
268    fn test_codec_error_debug() {
269        let malformed: &[u8] = &[0xFF, 0xFF];
270        let result: Result<u64, _> = decode(malformed);
271        let err = result.unwrap_err();
272
273        let debug = format!("{err:?}");
274        // Debug output should contain the variant name
275        assert!(debug.contains("Decode"), "Debug should contain 'Decode' variant name");
276        // Debug output should contain "source" field info
277        assert!(debug.contains("source"), "Debug should contain 'source' field: {debug}");
278    }
279
280    // Test that both error variants exist and are distinct
281    #[test]
282    fn test_codec_error_variants() {
283        // Decode error - use context selector to properly construct with location
284        let decode_result: Result<u64, CodecError> =
285            postcard::from_bytes::<u64>(&[0xFF, 0xFF, 0xFF]).context(super::DecodeSnafu);
286        let decode_err = decode_result.expect_err("should fail");
287
288        // Verify we can match on the variant
289        assert!(matches!(decode_err, CodecError::Decode { .. }));
290
291        // The Encode variant exists (verified at compile time by matching)
292        // Creating an actual encode error is difficult since postcard rarely fails encoding,
293        // but the variant is tested implicitly via the encode() function
294        assert!(!matches!(decode_err, CodecError::Encode { .. }));
295    }
296}