cache_kit/serialization/
mod.rs

1//! Postcard-based cache serialization with versioned envelopes.
2//!
3//! This module provides the canonical serialization format for all cache storage
4//! in cache-kit. It uses Postcard for performance and wraps all cache entries in
5//! versioned envelopes for schema evolution safety.
6//!
7//! # Architecture
8//!
9//! Every cache entry follows this format:
10//! ```text
11//! ┌─────────────────┬─────────────────┬──────────────────────────┐
12//! │  MAGIC (4 bytes)│VERSION (4 bytes)│POSTCARD PAYLOAD (N bytes)│
13//! └─────────────────┴─────────────────┴──────────────────────────┘
14//!   "CKIT"              u32 (LE)           postcard::to_allocvec(T)
15//! ```
16//!
17//! # Safety Guarantees
18//!
19//! - **Deterministic:** Same value always produces identical bytes
20//! - **Validated:** Magic and version checked on every deserialization
21//! - **Versioned:** Schema changes force cache eviction, not silent migration
22//! - **Type-safe:** Postcard preserves exact Rust types
23//!
24//! # Example
25//!
26//! ```rust
27//! use cache_kit::serialization::{serialize_for_cache, deserialize_from_cache};
28//! use serde::{Serialize, Deserialize};
29//!
30//! #[derive(Serialize, Deserialize, PartialEq, Debug)]
31//! struct User {
32//!     id: u64,
33//!     name: String,
34//! }
35//!
36//! # fn main() -> cache_kit::Result<()> {
37//! let user = User { id: 1, name: "Alice".to_string() };
38//!
39//! // Serialize with envelope
40//! let bytes = serialize_for_cache(&user)?;
41//!
42//! // Deserialize with validation
43//! let deserialized: User = deserialize_from_cache(&bytes)?;
44//! assert_eq!(user, deserialized);
45//! # Ok(())
46//! # }
47//! ```
48
49use crate::error::{Error, Result};
50use serde::{Deserialize, Serialize};
51
52/// Magic header for cache-kit entries: b"CKIT"
53///
54/// This 4-byte signature identifies valid cache-kit cache entries.
55/// Any entry without this magic is rejected during deserialization.
56pub const CACHE_MAGIC: [u8; 4] = *b"CKIT";
57
58/// Current schema version.
59///
60/// **CRITICAL:** Increment this constant when making breaking changes to cached types:
61/// - Adding/removing struct fields
62/// - Changing field types
63/// - Reordering fields
64/// - Changing enum variants
65///
66/// When deployed with a new version, old cache entries will be automatically
67/// evicted and recomputed from the source of truth.
68pub const CURRENT_SCHEMA_VERSION: u32 = 1;
69
70/// Versioned envelope for cache entries.
71///
72/// Every cache entry is wrapped in this envelope to enable:
73/// - **Corruption detection:** Invalid magic → reject entry
74/// - **Schema evolution:** Version mismatch → evict and recompute
75/// - **Observability:** Track version mismatches in metrics
76///
77/// # Format
78///
79/// ```text
80/// ┌─────────────────┬─────────────────┬──────────────────────────┐
81/// │  magic: [u8; 4] │ version: u32    │  payload: T              │
82/// └─────────────────┴─────────────────┴──────────────────────────┘
83/// ```
84///
85/// # Example
86///
87/// ```rust
88/// use cache_kit::serialization::CacheEnvelope;
89///
90/// let envelope = CacheEnvelope::new("data");
91/// assert_eq!(envelope.magic, *b"CKIT");
92/// ```
93#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
94pub struct CacheEnvelope<T> {
95    /// Magic header: must be b"CKIT"
96    pub magic: [u8; 4],
97    /// Schema version: must match CURRENT_SCHEMA_VERSION
98    pub version: u32,
99    /// The actual cached data
100    pub payload: T,
101}
102
103impl<T> CacheEnvelope<T> {
104    /// Create a new envelope with current magic and version.
105    ///
106    /// # Example
107    ///
108    /// ```rust
109    /// use cache_kit::serialization::CacheEnvelope;
110    ///
111    /// let envelope = CacheEnvelope::new(42);
112    /// assert_eq!(envelope.payload, 42);
113    /// ```
114    pub fn new(payload: T) -> Self {
115        Self {
116            magic: CACHE_MAGIC,
117            version: CURRENT_SCHEMA_VERSION,
118            payload,
119        }
120    }
121}
122
123/// Serialize a value with envelope for cache storage.
124///
125/// This is the canonical way to serialize data for cache storage in cache-kit.
126/// All cache backends (InMemory, Redis, Memcached) use this function.
127///
128/// # Format
129///
130/// ```text
131/// [MAGIC: 4 bytes] [VERSION: 4 bytes] [POSTCARD PAYLOAD: N bytes]
132/// ```
133///
134/// # Performance
135///
136/// Postcard serialization is approximately:
137/// - **8-12x faster** than JSON serialization
138/// - **50-70% smaller** than JSON payloads
139///
140/// # Example
141///
142/// ```rust
143/// use cache_kit::serialization::serialize_for_cache;
144/// use serde::Serialize;
145///
146/// #[derive(Serialize)]
147/// struct User { id: u64, name: String }
148///
149/// # fn main() -> cache_kit::Result<()> {
150/// let user = User { id: 1, name: "Alice".to_string() };
151/// let bytes = serialize_for_cache(&user)?;
152///
153/// // Verify envelope structure
154/// assert_eq!(&bytes[0..4], b"CKIT");
155/// # Ok(())
156/// # }
157/// ```
158///
159/// # Errors
160///
161/// Returns `Error::SerializationError` if Postcard serialization fails.
162pub fn serialize_for_cache<T: Serialize>(value: &T) -> Result<Vec<u8>> {
163    let envelope = CacheEnvelope::new(value);
164    postcard::to_allocvec(&envelope).map_err(|e| {
165        log::error!("Cache serialization failed: {}", e);
166        Error::SerializationError(e.to_string())
167    })
168}
169
170/// Deserialize a value from cache storage with validation.
171///
172/// This function performs strict validation:
173/// 1. Checks magic header matches b"CKIT"
174/// 2. Checks version matches CURRENT_SCHEMA_VERSION
175/// 3. Deserializes Postcard payload
176///
177/// # Validation Strategy
178///
179/// **On magic mismatch:** Returns `Error::InvalidCacheEntry`
180/// - Indicates corrupted cache entry or non-cache-kit data
181/// - Cache entry should be evicted
182///
183/// **On version mismatch:** Returns `Error::VersionMismatch`
184/// - Indicates schema change between code versions
185/// - Cache entry should be evicted and recomputed
186///
187/// **On Postcard error:** Returns `Error::DeserializationError`
188/// - Indicates corrupted payload
189/// - Cache entry should be evicted
190///
191/// # Example
192///
193/// ```rust
194/// use cache_kit::serialization::{serialize_for_cache, deserialize_from_cache};
195/// use serde::{Serialize, Deserialize};
196///
197/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
198/// struct User { id: u64, name: String }
199///
200/// # fn main() -> cache_kit::Result<()> {
201/// let user = User { id: 1, name: "Alice".to_string() };
202/// let bytes = serialize_for_cache(&user)?;
203///
204/// let deserialized: User = deserialize_from_cache(&bytes)?;
205/// assert_eq!(user, deserialized);
206/// # Ok(())
207/// # }
208/// ```
209///
210/// # Errors
211///
212/// - `Error::InvalidCacheEntry`: Invalid magic header
213/// - `Error::VersionMismatch`: Schema version mismatch
214/// - `Error::DeserializationError`: Corrupted Postcard payload
215pub fn deserialize_from_cache<'de, T: Deserialize<'de>>(bytes: &'de [u8]) -> Result<T> {
216    // Attempt to deserialize envelope
217    let envelope: CacheEnvelope<T> = postcard::from_bytes(bytes).map_err(|e| {
218        log::error!("Cache deserialization failed: {}", e);
219        Error::DeserializationError(e.to_string())
220    })?;
221
222    // Validate magic header
223    if envelope.magic != CACHE_MAGIC {
224        log::warn!(
225            "Invalid cache entry: expected magic {:?}, got {:?}",
226            CACHE_MAGIC,
227            envelope.magic
228        );
229        return Err(Error::InvalidCacheEntry(format!(
230            "Invalid magic: expected {:?}, got {:?}",
231            CACHE_MAGIC, envelope.magic
232        )));
233    }
234
235    // Validate schema version
236    if envelope.version != CURRENT_SCHEMA_VERSION {
237        log::warn!(
238            "Cache version mismatch: expected {}, got {}",
239            CURRENT_SCHEMA_VERSION,
240            envelope.version
241        );
242        return Err(Error::VersionMismatch {
243            expected: CURRENT_SCHEMA_VERSION,
244            found: envelope.version,
245        });
246    }
247
248    Ok(envelope.payload)
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
256    struct TestData {
257        id: u64,
258        name: String,
259        active: bool,
260    }
261
262    #[test]
263    fn test_roundtrip() {
264        let data = TestData {
265            id: 123,
266            name: "test".to_string(),
267            active: true,
268        };
269
270        let bytes = serialize_for_cache(&data).unwrap();
271        let deserialized: TestData = deserialize_from_cache(&bytes).unwrap();
272
273        assert_eq!(data, deserialized);
274    }
275
276    #[test]
277    fn test_envelope_structure() {
278        let data = TestData {
279            id: 123,
280            name: "test".to_string(),
281            active: true,
282        };
283
284        let bytes = serialize_for_cache(&data).unwrap();
285
286        // Deserialize the envelope to verify its structure
287        // (postcard uses variable-length encoding, so we can't rely on fixed byte positions)
288        let envelope: CacheEnvelope<TestData> = postcard::from_bytes(&bytes).unwrap();
289
290        // Verify envelope magic
291        assert_eq!(envelope.magic, CACHE_MAGIC);
292
293        // Verify envelope version
294        assert_eq!(envelope.version, CURRENT_SCHEMA_VERSION);
295
296        // Verify payload matches original data
297        assert_eq!(envelope.payload, data);
298    }
299
300    #[test]
301    fn test_envelope_new() {
302        let envelope = CacheEnvelope::new(42);
303        assert_eq!(envelope.magic, CACHE_MAGIC);
304        assert_eq!(envelope.version, CURRENT_SCHEMA_VERSION);
305        assert_eq!(envelope.payload, 42);
306    }
307
308    #[test]
309    fn test_invalid_magic_rejected() {
310        let mut bytes = vec![0u8; 100];
311        bytes[0..4].copy_from_slice(b"XXXX"); // Wrong magic
312        bytes[4..8].copy_from_slice(&1u32.to_le_bytes()); // Valid version
313
314        let result: Result<TestData> = deserialize_from_cache(&bytes);
315        assert!(result.is_err());
316        match result.unwrap_err() {
317            Error::InvalidCacheEntry(_) => {} // Expected
318            e => panic!("Expected InvalidCacheEntry, got {:?}", e),
319        }
320    }
321
322    #[test]
323    fn test_version_mismatch_rejected() {
324        let data = TestData {
325            id: 123,
326            name: "test".to_string(),
327            active: true,
328        };
329
330        let mut envelope = CacheEnvelope::new(&data);
331        envelope.version = 999; // Future version
332
333        let bytes = postcard::to_allocvec(&envelope).unwrap();
334        let result: Result<TestData> = deserialize_from_cache(&bytes);
335
336        assert!(result.is_err());
337        match result.unwrap_err() {
338            Error::VersionMismatch { expected, found } => {
339                assert_eq!(expected, CURRENT_SCHEMA_VERSION);
340                assert_eq!(found, 999);
341            }
342            e => panic!("Expected VersionMismatch, got {:?}", e),
343        }
344    }
345
346    #[test]
347    fn test_deterministic_serialization() {
348        let data1 = TestData {
349            id: 123,
350            name: "test".to_string(),
351            active: true,
352        };
353        let data2 = data1.clone();
354
355        let bytes1 = serialize_for_cache(&data1).unwrap();
356        let bytes2 = serialize_for_cache(&data2).unwrap();
357
358        // Must produce identical bytes
359        assert_eq!(bytes1, bytes2);
360    }
361
362    #[test]
363    fn test_corrupted_payload_rejected() {
364        // Create a valid envelope first
365        let data = TestData {
366            id: 123,
367            name: "test".to_string(),
368            active: true,
369        };
370        let mut bytes = serialize_for_cache(&data).unwrap();
371
372        // Corrupt the payload by truncating aggressively
373        // With postcard's compact encoding, we need to truncate enough to ensure
374        // the data structure is incomplete
375        let original_len = bytes.len();
376        bytes.truncate(original_len / 2); // Truncate to half the size
377
378        let result: Result<TestData> = deserialize_from_cache(&bytes);
379        assert!(result.is_err());
380        match result.unwrap_err() {
381            Error::DeserializationError(_) => {} // Expected
382            e => panic!("Expected DeserializationError, got {:?}", e),
383        }
384    }
385
386    #[test]
387    fn test_empty_data_roundtrip() {
388        let data = TestData {
389            id: 0,
390            name: String::new(),
391            active: false,
392        };
393
394        let bytes = serialize_for_cache(&data).unwrap();
395        let deserialized: TestData = deserialize_from_cache(&bytes).unwrap();
396
397        assert_eq!(data, deserialized);
398    }
399
400    #[test]
401    fn test_large_data_roundtrip() {
402        let data = TestData {
403            id: u64::MAX,
404            name: "x".repeat(10000),
405            active: true,
406        };
407
408        let bytes = serialize_for_cache(&data).unwrap();
409        let deserialized: TestData = deserialize_from_cache(&bytes).unwrap();
410
411        assert_eq!(data, deserialized);
412    }
413
414    #[test]
415    fn test_postcard_smaller_than_json() {
416        let data = TestData {
417            id: 123,
418            name: "test".to_string(),
419            active: true,
420        };
421
422        let postcard_bytes = serialize_for_cache(&data).unwrap();
423        let json_bytes = serde_json::to_vec(&data).unwrap();
424
425        // Postcard should be smaller (including envelope overhead)
426        assert!(
427            postcard_bytes.len() < json_bytes.len(),
428            "Postcard ({} bytes) should be smaller than JSON ({} bytes)",
429            postcard_bytes.len(),
430            json_bytes.len()
431        );
432    }
433}