Skip to main content

canic_core/cdk/
serialize.rs

1//! Module: cdk::serialize
2//!
3//! Responsibility: serde-CBOR helpers for stable storage encoding.
4//! Does not own: individual stable schema bounds or migration policy.
5//! Boundary: maps serde encode/decode failures into typed Canic errors.
6
7use serde::{Serialize, de::DeserializeOwned};
8use thiserror::Error as ThisError;
9
10///
11/// SerializeError
12///
13/// Typed error returned by Canic stable serialization helpers.
14///
15
16#[derive(Debug, ThisError)]
17pub enum SerializeError {
18    #[error("serialize error: {0}")]
19    Serialize(String),
20
21    #[error("deserialize error: {0}")]
22    Deserialize(String),
23}
24
25/// Serialize one value to CBOR bytes.
26pub fn serialize<T>(value: &T) -> Result<Vec<u8>, SerializeError>
27where
28    T: Serialize,
29{
30    let mut bytes = Vec::new();
31    ciborium::ser::into_writer(value, &mut bytes)
32        .map_err(|err| SerializeError::Serialize(err.to_string()))?;
33    Ok(bytes)
34}
35
36/// Deserialize one value from CBOR bytes.
37pub fn deserialize<T>(bytes: &[u8]) -> Result<T, SerializeError>
38where
39    T: DeserializeOwned,
40{
41    ciborium::de::from_reader(bytes).map_err(|err| SerializeError::Deserialize(err.to_string()))
42}
43
44#[cfg(test)]
45mod tests {
46    use super::{deserialize, serialize};
47    use serde::{Deserialize, Serialize};
48    use std::collections::BTreeMap;
49
50    #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51    enum FixtureVariant {
52        Unit,
53        Tuple(u32, String),
54        Struct { enabled: bool },
55    }
56
57    #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
58    struct StableShapeFixture {
59        flag: bool,
60        count: u64,
61        signed: i64,
62        text: String,
63        #[serde(with = "serde_bytes")]
64        bytes: Vec<u8>,
65        array: [u8; 3],
66        values: Vec<u16>,
67        present: Option<u32>,
68        absent: Option<u32>,
69        variants: Vec<FixtureVariant>,
70        labels: BTreeMap<String, u64>,
71    }
72
73    #[test]
74    fn stable_serde_shape_has_exact_cbor_bytes() {
75        let fixture = StableShapeFixture {
76            flag: true,
77            count: 42,
78            signed: -7,
79            text: "canic".to_string(),
80            bytes: vec![0, 1, 255],
81            array: [2, 3, 4],
82            values: vec![5, 256],
83            present: Some(9),
84            absent: None,
85            variants: vec![
86                FixtureVariant::Unit,
87                FixtureVariant::Tuple(10, "ten".to_string()),
88                FixtureVariant::Struct { enabled: false },
89            ],
90            labels: BTreeMap::from([("a".to_string(), 1), ("b".to_string(), 2)]),
91        };
92        let bytes = serialize(&fixture).expect("serialize stable shape fixture");
93
94        assert_eq!(
95            bytes,
96            hex_fixture(
97                "ab64666c6167f565636f756e74182a667369676e65642664746578746563616e6963656279746573430001ff656172726179830203046676616c75657382051901006770726573656e740966616273656e74f66876617269616e74738364556e6974a1655475706c65820a6374656ea166537472756374a167656e61626c6564f4666c6162656c73a2616101616202"
98            )
99        );
100        assert_eq!(
101            deserialize::<StableShapeFixture>(&bytes).expect("deserialize stable shape fixture"),
102            fixture
103        );
104    }
105
106    fn hex_fixture(hex: &str) -> Vec<u8> {
107        hex.as_bytes()
108            .chunks_exact(2)
109            .map(|pair| {
110                let pair = std::str::from_utf8(pair).expect("fixture hex is UTF-8");
111                u8::from_str_radix(pair, 16).expect("fixture hex byte is valid")
112            })
113            .collect()
114    }
115}