Skip to main content

inferadb_ledger_types/
codec.rs

1//! Centralized serialization and deserialization functions.
2//!
3//! Provides a unified interface for encoding and decoding data using postcard
4//! 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    /// Serialization to bytes 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    /// Deserialization from bytes failed (malformed, truncated, or type mismatch).
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    // Error conversion chain tests
214
215    // Test that CodecError::Encode has correct Display output
216    #[test]
217    fn test_codec_error_encode_display() {
218        // Create a decode error (easier to trigger than encode error)
219        let malformed: &[u8] = &[0xFF, 0xFF, 0xFF, 0xFF];
220        let result: Result<u64, _> = decode(malformed);
221        let err = result.unwrap_err();
222
223        // Verify display format matches expected pattern
224        let display = format!("{err}");
225        assert!(
226            display.starts_with("Decoding failed:"),
227            "Expected 'Decoding failed:', got: {display}"
228        );
229    }
230
231    // Test that CodecError::Decode has correct Display output
232    #[test]
233    fn test_codec_error_decode_display() {
234        let empty: &[u8] = &[];
235        let result: Result<String, _> = decode(empty);
236        let err = result.unwrap_err();
237
238        let display = format!("{err}");
239        assert!(
240            display.starts_with("Decoding failed:"),
241            "Expected 'Decoding failed:', got: {display}"
242        );
243    }
244
245    // Test error source chain - CodecError preserves underlying postcard error
246    #[test]
247    fn test_codec_error_source_chain() {
248        use std::error::Error;
249
250        let malformed: &[u8] = &[0xFF];
251        let result: Result<String, _> = decode(malformed);
252        let err = result.unwrap_err();
253
254        // Verify the error has a source (the underlying postcard error)
255        assert!(err.source().is_some(), "CodecError should have a source");
256
257        // The source should be a postcard::Error
258        let source = err.source().unwrap();
259        // Verify source has a Display impl (postcard::Error)
260        let source_display = format!("{source}");
261        assert!(!source_display.is_empty(), "Source should have non-empty display");
262    }
263
264    // ============================================
265    // Property-based roundtrip tests
266    // ============================================
267
268    mod proptest_roundtrip {
269        use proptest::prelude::*;
270
271        use crate::{
272            codec::{decode, encode},
273            types::{
274                BlockHeader, ChainCommitment, Entity, Operation, RegionBlock, Relationship,
275                SetCondition, Transaction, VaultBlock, VaultEntry,
276            },
277        };
278
279        /// Strategy for generating arbitrary `SetCondition`.
280        fn arb_set_condition() -> impl Strategy<Value = SetCondition> {
281            prop_oneof![
282                Just(SetCondition::MustNotExist),
283                Just(SetCondition::MustExist),
284                any::<u64>().prop_map(SetCondition::VersionEquals),
285                proptest::collection::vec(any::<u8>(), 0..32).prop_map(SetCondition::ValueEquals),
286            ]
287        }
288
289        /// Strategy for generating arbitrary `Operation`.
290        fn arb_operation() -> impl Strategy<Value = Operation> {
291            prop_oneof![
292                (
293                    "[a-z]{1,16}",
294                    proptest::collection::vec(any::<u8>(), 0..32),
295                    proptest::option::of(arb_set_condition()),
296                    proptest::option::of(any::<u64>()),
297                )
298                    .prop_map(|(key, value, condition, expires_at)| {
299                        Operation::SetEntity { key, value, condition, expires_at }
300                    }),
301                "[a-z]{1,16}".prop_map(|key| Operation::DeleteEntity { key }),
302                ("[a-z]{1,16}", any::<u64>())
303                    .prop_map(|(key, expired_at)| Operation::ExpireEntity { key, expired_at }),
304                ("[a-z]{1,16}", "[a-z]{1,8}", "[a-z]{1,16}").prop_map(
305                    |(resource, relation, subject)| {
306                        Operation::CreateRelationship { resource, relation, subject }
307                    }
308                ),
309                ("[a-z]{1,16}", "[a-z]{1,8}", "[a-z]{1,16}").prop_map(
310                    |(resource, relation, subject)| {
311                        Operation::DeleteRelationship { resource, relation, subject }
312                    }
313                ),
314            ]
315        }
316
317        /// Strategy for a 32-byte hash.
318        fn arb_hash() -> impl Strategy<Value = [u8; 32]> {
319            proptest::array::uniform32(any::<u8>())
320        }
321
322        /// Strategy for a `DateTime<Utc>`.
323        fn arb_timestamp() -> impl Strategy<Value = chrono::DateTime<chrono::Utc>> {
324            use chrono::{TimeZone, Utc};
325            (1_577_836_800i64..1_893_456_000i64).prop_map(|secs| {
326                Utc.timestamp_opt(secs, 0)
327                    .single()
328                    .unwrap_or_else(|| chrono::DateTime::<Utc>::from(std::time::UNIX_EPOCH))
329            })
330        }
331
332        /// Strategy for a Transaction.
333        fn arb_transaction() -> impl Strategy<Value = Transaction> {
334            (
335                proptest::array::uniform16(any::<u8>()),
336                "[a-z]{3,10}",
337                1u64..100_000,
338                proptest::collection::vec(arb_operation(), 1..5),
339                arb_timestamp(),
340            )
341                .prop_map(|(id, client_id, sequence, operations, timestamp)| {
342                    Transaction { id, client_id: client_id.into(), sequence, operations, timestamp }
343                })
344        }
345
346        /// Strategy for a BlockHeader.
347        fn arb_block_header() -> impl Strategy<Value = BlockHeader> {
348            (
349                any::<u64>(),
350                (1i64..10_000).prop_map(crate::types::OrganizationId::new),
351                (1i64..10_000).prop_map(crate::types::VaultId::new),
352                arb_hash(),
353                arb_hash(),
354                arb_hash(),
355                arb_timestamp(),
356                any::<u64>(),
357                any::<u64>(),
358            )
359                .prop_map(
360                    |(
361                        height,
362                        organization,
363                        vault,
364                        previous_hash,
365                        tx_merkle_root,
366                        state_root,
367                        timestamp,
368                        term,
369                        committed_index,
370                    )| {
371                        BlockHeader {
372                            height,
373                            organization,
374                            vault,
375                            previous_hash,
376                            tx_merkle_root,
377                            state_root,
378                            timestamp,
379                            term,
380                            committed_index,
381                        }
382                    },
383                )
384        }
385
386        proptest! {
387            /// Any `Operation` must survive postcard roundtrip.
388            #[test]
389            fn prop_operation_roundtrip(op in arb_operation()) {
390                let bytes = encode(&op).expect("encode operation");
391                let decoded: Operation = decode(&bytes).expect("decode operation");
392                prop_assert_eq!(op, decoded);
393            }
394
395            /// Any `SetCondition` must survive postcard roundtrip.
396            #[test]
397            fn prop_set_condition_roundtrip(cond in arb_set_condition()) {
398                let bytes = encode(&cond).expect("encode condition");
399                let decoded: SetCondition = decode(&bytes).expect("decode condition");
400                prop_assert_eq!(cond, decoded);
401            }
402
403            /// Any `Entity` must survive postcard roundtrip.
404            #[test]
405            fn prop_entity_roundtrip(
406                key in proptest::collection::vec(any::<u8>(), 0..64),
407                value in proptest::collection::vec(any::<u8>(), 0..64),
408                expires_at in any::<u64>(),
409                version in any::<u64>(),
410            ) {
411                let entity = Entity { key, value, expires_at, version };
412                let bytes = encode(&entity).expect("encode entity");
413                let decoded: Entity = decode(&bytes).expect("decode entity");
414                prop_assert_eq!(entity, decoded);
415            }
416
417            /// Any `Relationship` must survive postcard roundtrip.
418            #[test]
419            fn prop_relationship_roundtrip(
420                resource in "[a-z]{1,16}",
421                relation in "[a-z]{1,8}",
422                subject in "[a-z]{1,16}",
423            ) {
424                let rel = Relationship { resource, relation, subject };
425                let bytes = encode(&rel).expect("encode relationship");
426                let decoded: Relationship = decode(&bytes).expect("decode relationship");
427                prop_assert_eq!(rel, decoded);
428            }
429
430            /// Any `Transaction` must survive postcard roundtrip.
431            #[test]
432            fn prop_transaction_roundtrip(tx in arb_transaction()) {
433                let bytes = encode(&tx).expect("encode transaction");
434                let decoded: Transaction = decode(&bytes).expect("decode transaction");
435                prop_assert_eq!(tx, decoded);
436            }
437
438            /// Any `BlockHeader` must survive postcard roundtrip.
439            #[test]
440            fn prop_block_header_roundtrip(header in arb_block_header()) {
441                let bytes = encode(&header).expect("encode block header");
442                let decoded: BlockHeader = decode(&bytes).expect("decode block header");
443                prop_assert_eq!(header, decoded);
444            }
445
446            /// Any `VaultBlock` must survive postcard roundtrip.
447            #[test]
448            fn prop_vault_block_roundtrip(
449                header in arb_block_header(),
450                transactions in proptest::collection::vec(arb_transaction(), 0..3),
451            ) {
452                let block = VaultBlock { header, transactions };
453                let bytes = encode(&block).expect("encode vault block");
454                let decoded: VaultBlock = decode(&bytes).expect("decode vault block");
455                prop_assert_eq!(block, decoded);
456            }
457
458            /// Any `VaultEntry` must survive postcard roundtrip.
459            #[test]
460            fn prop_vault_entry_roundtrip(
461                ns_id in (1i64..10_000).prop_map(crate::types::OrganizationId::new),
462                vault_id in (1i64..10_000).prop_map(crate::types::VaultId::new),
463                vault_height in any::<u64>(),
464                previous_vault_hash in arb_hash(),
465                transactions in proptest::collection::vec(arb_transaction(), 0..2),
466                tx_merkle_root in arb_hash(),
467                state_root in arb_hash(),
468            ) {
469                let entry = VaultEntry {
470                    organization: ns_id,
471                    vault: vault_id,
472                    vault_height,
473                    previous_vault_hash,
474                    transactions,
475                    tx_merkle_root,
476                    state_root,
477                };
478                let bytes = encode(&entry).expect("encode vault entry");
479                let decoded: VaultEntry = decode(&bytes).expect("decode vault entry");
480                prop_assert_eq!(entry, decoded);
481            }
482
483            /// Any `RegionBlock` must survive postcard roundtrip.
484            #[test]
485            fn prop_region_block_roundtrip(
486                region in (0usize..crate::types::ALL_REGIONS.len()).prop_map(|i| crate::types::ALL_REGIONS[i]),
487                region_height in any::<u64>(),
488                previous_region_hash in arb_hash(),
489                timestamp in arb_timestamp(),
490                leader_id in "[a-z]{3,10}",
491                term in any::<u64>(),
492                committed_index in any::<u64>(),
493            ) {
494                let block = RegionBlock {
495                    region,
496                    region_height,
497                    previous_region_hash,
498                    vault_entries: vec![],
499                    timestamp,
500                    leader_id: leader_id.into(),
501                    term,
502                    committed_index,
503                };
504                let bytes = encode(&block).expect("encode region block");
505                let decoded: RegionBlock = decode(&bytes).expect("decode region block");
506                prop_assert_eq!(block, decoded);
507            }
508
509            /// Any `ChainCommitment` must survive postcard roundtrip.
510            #[test]
511            fn prop_chain_commitment_roundtrip(
512                accumulated in arb_hash(),
513                state_root_acc in arb_hash(),
514                from in any::<u64>(),
515                to in any::<u64>(),
516            ) {
517                let commitment = ChainCommitment {
518                    accumulated_header_hash: accumulated,
519                    state_root_accumulator: state_root_acc,
520                    from_height: from.min(to),
521                    to_height: from.max(to),
522                };
523                let bytes = encode(&commitment).expect("encode chain commitment");
524                let decoded: ChainCommitment =
525                    decode(&bytes).expect("decode chain commitment");
526                prop_assert_eq!(commitment, decoded);
527            }
528        }
529    }
530
531    // Test Debug implementation contains useful info
532    #[test]
533    fn test_codec_error_debug() {
534        let malformed: &[u8] = &[0xFF, 0xFF];
535        let result: Result<u64, _> = decode(malformed);
536        let err = result.unwrap_err();
537
538        let debug = format!("{err:?}");
539        // Debug output should contain the variant name
540        assert!(debug.contains("Decode"), "Debug should contain 'Decode' variant name");
541        // Debug output should contain "source" field info
542        assert!(debug.contains("source"), "Debug should contain 'source' field: {debug}");
543    }
544
545    // Test that both error variants exist and are distinct
546    #[test]
547    fn test_codec_error_variants() {
548        // Decode error - use context selector to properly construct with location
549        let decode_result: Result<u64, CodecError> =
550            postcard::from_bytes::<u64>(&[0xFF, 0xFF, 0xFF]).context(super::DecodeSnafu);
551        let decode_err = decode_result.expect_err("should fail");
552
553        // Verify we can match on the variant
554        assert!(matches!(decode_err, CodecError::Decode { .. }));
555
556        // The Encode variant exists (verified at compile time by matching)
557        // Creating an actual encode error is difficult since postcard rarely fails encoding,
558        // but the variant is tested implicitly via the encode() function
559        assert!(!matches!(decode_err, CodecError::Encode { .. }));
560    }
561}