Skip to main content

mnesis_store/
codec.rs

1//! Serialization traits — [`Encode<E>`] and [`Decode<E>`].
2//!
3//! Two traits, not three. The previous shape carried a third trait
4//! (`BorrowingDecode`) as a workaround for a lifetime cliff in the old
5//! borrowed-cursor read path: when the envelope was borrowed from a
6//! cursor row that died on the next `.next()`, the same logical
7//! operation needed two different trait shapes (one returning `E`, one
8//! returning `&'a E`).
9//!
10//! The owned-[`bytes::Bytes`] envelope from the 2026-05-27 refactor
11//! removed that cliff. `PersistedEnvelope` is now cheap-to-clone
12//! (`Bytes` is Arc-counted; range copies are 8 bytes each) and has no
13//! lifetime parameter, so `'a` ties cleanly to the envelope itself.
14//! Both shapes — owning and borrowing — collapse into a single trait
15//! with a generic associated type:
16//!
17//! ```ignore
18//! pub trait Decode<E: ?Sized>: Send + Sync + 'static {
19//!     type Output<'a> where Self: 'a;
20//!     type Error: core::error::Error + Send + Sync + 'static;
21//!     fn decode<'a>(&'a self, env: &'a PersistedEnvelope)
22//!         -> Result<Self::Output<'a>, Self::Error>;
23//! }
24//! ```
25//!
26//! - Owning codec (`SerdeCodec<Json>`): `type Output<'a> = E`.
27//! - Archived zero-copy (`rkyv::RkyvCodec`): `type Output<'a> = &'a E::Archived`.
28//! - POD zero-copy (`bytemuck::BytemuckCodec`): `type Output<'a> = &'a E`.
29//!
30//! [`Encode<E>`] stays separate from [`Decode<E>`] so write-only
31//! adapters (shippers) and read-only adapters (replicas) need not
32//! implement the other half. [`Encode::encode`] returns
33//! [`bytes::Bytes`] (not `Vec<u8>`) so the encoded payload flows
34//! end-to-end through the store and wire layer without an intermediate
35//! `Vec<u8> → Bytes` copy.
36//!
37//! Both `Encode` and `Decode` take their event type with `E: ?Sized` so
38//! the unsized archived types (`Archived<MyEvent>`, `[u8]`, `str`) are
39//! representable.
40
41use crate::envelope::PersistedEnvelope;
42
43// ═══════════════════════════════════════════════════════════════════════════
44// Encode<E> — serialize a typed value to bytes
45// ═══════════════════════════════════════════════════════════════════════════
46
47/// Serialize a typed value to bytes.
48///
49/// The write half of the codec story. Independent from [`Decode`] so
50/// write-only adapters need not implement decoding, and so the encode path
51/// can be bounded without forcing a decode strategy on the caller.
52///
53/// `E: ?Sized` allows unsized event types (e.g. `Archived<MyEvent>`).
54pub trait Encode<E: ?Sized>: Send + Sync + 'static {
55    /// The error type for serialization failures.
56    type Error: core::error::Error + Send + Sync + 'static;
57
58    /// Serialize a typed value to bytes.
59    ///
60    /// Returns [`bytes::Bytes`] so the encoded payload can flow through
61    /// the store and wire layer without an intermediate `Vec<u8> → Bytes`
62    /// copy. Codecs that produce a `Vec<u8>` internally adapt via
63    /// `Bytes::from(vec)` (zero-copy ownership transfer of the backing
64    /// allocation); codecs that build incrementally may use
65    /// `BytesMut::freeze()`.
66    ///
67    /// # Errors
68    ///
69    /// Returns `Self::Error` if the value cannot be serialized.
70    fn encode(&self, event: &E) -> Result<bytes::Bytes, Self::Error>;
71}
72
73// ═══════════════════════════════════════════════════════════════════════════
74// Decode<E> — deserialize a persisted envelope to a typed value (or borrow into it)
75// ═══════════════════════════════════════════════════════════════════════════
76
77/// Decode a persisted envelope to a typed value (or borrow into it).
78///
79/// One trait covers both owning and borrowing codecs via the
80/// [`Output<'a>`](Self::Output) GAT:
81///
82/// - **Owning codecs** (serde JSON/bincode/postcard): `type Output<'a> = E`.
83///   `decode` returns an owned `E` that lives past the envelope.
84/// - **Borrowing codecs** (rkyv): `type Output<'a> = &'a <E as Archive>::Archived`.
85///   `decode` returns a reference into the envelope's payload bytes — no
86///   allocation, no copy.
87/// - **Plain-old-data codecs** (bytemuck): `type Output<'a> = &'a E`.
88///   Reinterprets the payload as `&E` directly. Relies on the wire-format
89///   16-byte payload-alignment invariant for safety.
90///
91/// The two-trait split (previously `Decode` + `BorrowingDecode`) was a
92/// workaround for the borrowed-cursor lifetime cliff: when the envelope
93/// was borrowed from a cursor row that died on `.next()`, the same
94/// operation needed two trait shapes. The owned-`Bytes` envelope removes
95/// the cliff — `'a` now ties to the envelope itself, which is cheap-to-
96/// clone and lifetime-independent.
97///
98/// `E: ?Sized` allows unsized event types: `Decode<[u8]>`, `Decode<str>`,
99/// `Decode<<E as Archive>::Archived>`.
100///
101/// Independent from [`Encode`]: a codec may implement only `Decode`
102/// (read-only replica), only `Encode` (write-only shipper), or both.
103pub trait Decode<E: ?Sized>: Send + Sync + 'static {
104    /// What [`decode`](Self::decode) returns.
105    ///
106    /// `E` for owning codecs, `&'a E` (or `&'a Archived<E>`) for
107    /// zero-copy codecs. The `where Self: 'a` bound is the standard GAT
108    /// shape and adds no real constraint for `'static` codecs.
109    type Output<'a>
110    where
111        Self: 'a;
112
113    /// The error type for deserialization failures.
114    type Error: core::error::Error + Send + Sync + 'static;
115
116    /// Decode the envelope's payload to [`Output<'a>`](Self::Output).
117    ///
118    /// The envelope is the input: its [`event_type()`](PersistedEnvelope::event_type)
119    /// is the variant discriminant, [`payload()`](PersistedEnvelope::payload)
120    /// is the serialized bytes. Codecs reach into the envelope for whatever
121    /// they need — no pre-extracted arguments.
122    ///
123    /// # Errors
124    ///
125    /// Returns `Self::Error` if the payload is invalid (e.g. failed archive
126    /// validation for rkyv) or does not match the type discriminant.
127    fn decode<'a>(&'a self, env: &'a PersistedEnvelope) -> Result<Self::Output<'a>, Self::Error>;
128}
129
130/// An **owning** codec: `decode` yields a fully-owned `E` that borrows nothing
131/// from the codec beyond the call.
132///
133/// This is the [`Decode`] analogue of serde's [`DeserializeOwned`] — a *name*
134/// for the higher-ranked bound `for<'a> Decode<E, Output<'a> = E>`, so a
135/// generic caller writes `C: OwningCodec<E>` instead of spelling the `for<'a>`
136/// itself. The blanket impl covers every codec that satisfies the bound, so it
137/// is a transparent alias, never something to implement by hand.
138///
139/// Zero-copy codecs (rkyv, bytemuck), whose `Output<'a>` borrows (`&'a E` /
140/// `&'a Archived<E>`), deliberately do **not** satisfy it — code bounded on
141/// `OwningCodec` accepts only carry-away decoding, the same steer as
142/// [`DecodedStreamExt::decoded`](crate::DecodedStreamExt::decoded).
143///
144/// [`DeserializeOwned`]: https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html
145pub trait OwningCodec<E: ?Sized>: for<'a> Decode<E, Output<'a> = E> {}
146
147impl<C, E: ?Sized> OwningCodec<E> for C where C: for<'a> Decode<E, Output<'a> = E> {}
148
149// ═══════════════════════════════════════════════════════════════════════════
150// Serde adapter — feature-gated Encode/Decode impls driven by a SerdeFormat
151// ═══════════════════════════════════════════════════════════════════════════
152
153#[cfg(feature = "serde")]
154pub mod serde {
155    use alloc::vec::Vec;
156
157    use ::serde::{Serialize, de::DeserializeOwned};
158
159    use super::{Decode, Encode};
160    use crate::envelope::PersistedEnvelope;
161
162    /// Format-agnostic serialization strategy for serde-compatible events.
163    ///
164    /// Implementors provide the wire format (JSON, bincode, postcard, etc.)
165    /// while [`SerdeCodec`] handles the plumbing to satisfy [`Encode`] and
166    /// [`Decode`].
167    ///
168    /// # Implementor contract
169    ///
170    /// - `serialize` and `deserialize` must be inverses: for any `T`,
171    ///   `deserialize(serialize(t)?) == t`.
172    /// - Errors must accurately describe the failure (not erase the cause).
173    pub trait SerdeFormat: Send + Sync + 'static {
174        /// The error type for serialization/deserialization failures.
175        type Error: core::error::Error + Send + Sync + 'static;
176
177        /// Serialize a value to bytes.
178        ///
179        /// # Errors
180        ///
181        /// Returns `Self::Error` if the value cannot be serialized.
182        fn serialize<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, Self::Error>;
183
184        /// Deserialize bytes back to a typed value.
185        ///
186        /// # Errors
187        ///
188        /// Returns `Self::Error` if the payload cannot be deserialized.
189        fn deserialize<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, Self::Error>;
190    }
191
192    /// Generic serde-based codec parameterized by a [`SerdeFormat`].
193    ///
194    /// Wraps any `SerdeFormat` implementation and bridges it to the
195    /// store's [`Encode`] and [`Decode`] traits. Both directions share
196    /// the same underlying format and thus the same error type.
197    ///
198    /// # Variant dispatch
199    ///
200    /// `SerdeCodec` ignores [`env.event_type()`](PersistedEnvelope::event_type)
201    /// on `decode`. Serde formats embed variant discriminants in the payload
202    /// (e.g. `{"Credited": {...}}` in JSON), so the codec delegates dispatch
203    /// to serde itself.
204    ///
205    /// # Examples
206    ///
207    /// ```ignore
208    /// use mnesis_store::{JsonCodec, Json, SerdeCodec};
209    ///
210    /// // Via the JsonCodec alias:
211    /// let codec = JsonCodec::default();
212    ///
213    /// // Or construct manually with any SerdeFormat:
214    /// let codec = SerdeCodec::new(Json);
215    /// ```
216    pub struct SerdeCodec<F> {
217        format: F,
218    }
219
220    impl<F> SerdeCodec<F> {
221        /// Create a new `SerdeCodec` wrapping the given format.
222        pub const fn new(format: F) -> Self {
223            Self { format }
224        }
225    }
226
227    impl<F: Default> Default for SerdeCodec<F> {
228        fn default() -> Self {
229            Self::new(F::default())
230        }
231    }
232
233    impl<E, F> Encode<E> for SerdeCodec<F>
234    where
235        E: Serialize + Send + Sync + 'static,
236        F: SerdeFormat,
237    {
238        type Error = F::Error;
239
240        fn encode(&self, event: &E) -> Result<bytes::Bytes, Self::Error> {
241            self.format.serialize(event).map(bytes::Bytes::from)
242        }
243    }
244
245    impl<E, F> Decode<E> for SerdeCodec<F>
246    where
247        E: DeserializeOwned + Send + Sync + 'static,
248        F: SerdeFormat,
249    {
250        type Output<'a>
251            = E
252        where
253            Self: 'a;
254        type Error = F::Error;
255
256        fn decode<'a>(
257            &'a self,
258            env: &'a PersistedEnvelope,
259        ) -> Result<Self::Output<'a>, Self::Error> {
260            self.format.deserialize(env.payload())
261        }
262    }
263
264    /// Seal `Debug` — show the format type, not its internals.
265    impl<F> core::fmt::Debug for SerdeCodec<F> {
266        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
267            f.debug_struct("SerdeCodec")
268                .field("format", &core::any::type_name::<F>())
269                .finish()
270        }
271    }
272
273    #[cfg(feature = "json")]
274    pub mod json {
275        use alloc::vec::Vec;
276
277        use ::serde::{Serialize, de::DeserializeOwned};
278
279        use super::{SerdeCodec, SerdeFormat};
280
281        /// JSON wire format backed by `serde_json`.
282        ///
283        /// Use directly with [`SerdeCodec`] or via the [`JsonCodec`] alias.
284        #[derive(Debug, Clone, Copy, Default)]
285        pub struct Json;
286
287        impl SerdeFormat for Json {
288            type Error = serde_json::Error;
289
290            fn serialize<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, Self::Error> {
291                serde_json::to_vec(value)
292            }
293
294            fn deserialize<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, Self::Error> {
295                serde_json::from_slice(bytes)
296            }
297        }
298
299        /// Convenience alias: a [`SerdeCodec`] using [`Json`] format.
300        pub type JsonCodec = SerdeCodec<Json>;
301    }
302}
303
304// ═══════════════════════════════════════════════════════════════════════════
305// Bytemuck — plain-old-data zero-copy codec
306// ═══════════════════════════════════════════════════════════════════════════
307
308#[cfg(feature = "bytemuck")]
309pub mod bytemuck {
310    use ::bytemuck::{AnyBitPattern, NoUninit, PodCastError};
311    use bytes::Bytes;
312    use thiserror::Error;
313
314    use super::{Decode, Encode};
315    use crate::envelope::PersistedEnvelope;
316
317    /// Wrapper around [`PodCastError`] that satisfies the `core::error::Error` bound.
318    ///
319    /// Upstream `PodCastError` does not implement `core::error::Error`
320    /// (`bytemuck` is `no_std` by default and skips the impl), so
321    /// `Decode::Error` requires a wrapper.
322    ///
323    /// Captures the upstream error's `Debug` representation as a
324    /// stack-allocated [`ArrayString`](arrayvec::ArrayString) — no heap
325    /// allocation on the error path.
326    #[derive(Debug, Error)]
327    #[error("bytemuck cast error: {0}")]
328    pub struct BytemuckError(arrayvec::ArrayString<64>);
329
330    impl From<PodCastError> for BytemuckError {
331        fn from(value: PodCastError) -> Self {
332            use core::fmt::Write;
333            let mut buf = arrayvec::ArrayString::<64>::new();
334            let _ = write!(buf, "{value:?}");
335            Self(buf)
336        }
337    }
338
339    /// Codec for plain-old-data types.
340    ///
341    /// Zero-copy on the read path: [`Decode::Output<'a>`] is `&'a E`,
342    /// pointing directly into the envelope's payload bytes (which are
343    /// 16-byte aligned by the wire-format invariant — see
344    /// [`crate::wire`]).
345    ///
346    /// `E` must implement [`AnyBitPattern`] (every bit pattern is a valid
347    /// value of `E`) and [`NoUninit`] (no padding/uninitialized bytes).
348    /// In practice this means `#[repr(C)]` POD types with no padding.
349    ///
350    /// # Example
351    ///
352    /// ```ignore
353    /// #[repr(C)]
354    /// #[derive(Clone, Copy, bytemuck::AnyBitPattern, bytemuck::NoUninit)]
355    /// struct Pos { x: f32, y: f32, z: f32, _pad: f32 }
356    ///
357    /// let codec = BytemuckCodec;
358    /// let bytes = codec.encode(&Pos { x: 1.0, y: 2.0, z: 3.0, _pad: 0.0 })?;
359    /// // ... persist to store, read back ...
360    /// let pos: &Pos = codec.decode(&env)?;  // borrowed, no allocation
361    /// ```
362    #[derive(Debug, Default, Clone, Copy)]
363    pub struct BytemuckCodec;
364
365    impl<E> Encode<E> for BytemuckCodec
366    where
367        E: NoUninit + Send + Sync + 'static,
368    {
369        type Error = core::convert::Infallible;
370
371        fn encode(&self, event: &E) -> Result<Bytes, Self::Error> {
372            Ok(Bytes::copy_from_slice(::bytemuck::bytes_of(event)))
373        }
374    }
375
376    impl<E> Decode<E> for BytemuckCodec
377    where
378        E: AnyBitPattern + NoUninit + Send + Sync + 'static,
379    {
380        type Output<'a>
381            = &'a E
382        where
383            Self: 'a;
384        type Error = BytemuckError;
385
386        fn decode<'a>(&'a self, env: &'a PersistedEnvelope) -> Result<&'a E, Self::Error> {
387            ::bytemuck::try_from_bytes(env.payload()).map_err(BytemuckError::from)
388        }
389    }
390
391    #[cfg(test)]
392    mod tests {
393        use super::*;
394        use crate::wire;
395
396        #[repr(C)]
397        #[derive(
398            Clone, Copy, Debug, PartialEq, ::bytemuck::AnyBitPattern, ::bytemuck::NoUninit,
399        )]
400        struct Pos {
401            x: f32,
402            y: f32,
403            z: f32,
404            _pad: f32,
405        }
406
407        fn build_test_envelope(payload: &[u8]) -> PersistedEnvelope {
408            let et = crate::value::EventType::from_static_str("Pos");
409            let pl = crate::value::Payload::from_bytes(bytes::Bytes::copy_from_slice(payload))
410                .expect("valid payload");
411            let sv = crate::value::SchemaVersion::INITIAL;
412            let frame = wire::encode_frame(sv, &et, &pl, None).expect("wire encode_frame ok");
413            PersistedEnvelope::try_new(
414                mnesis::Version::INITIAL,
415                frame.value,
416                sv,
417                frame.offsets.event_type,
418                frame.offsets.payload,
419                None,
420            )
421            .expect("envelope construction ok")
422        }
423
424        #[test]
425        fn round_trip_yields_equal_value() {
426            let codec = BytemuckCodec;
427            let original = Pos {
428                x: 1.0,
429                y: 2.0,
430                z: 3.0,
431                _pad: 0.0,
432            };
433
434            let bytes = codec.encode(&original).unwrap();
435            let env = build_test_envelope(&bytes);
436            let decoded: &Pos = codec.decode(&env).unwrap();
437
438            assert_eq!(decoded, &original);
439        }
440
441        #[test]
442        fn decode_borrows_from_envelope_payload() {
443            // Zero-copy assertion: the decoded &Pos points to the same
444            // bytes the envelope owns. The pointer equality proves there
445            // was no copy.
446            let codec = BytemuckCodec;
447            let original = Pos {
448                x: 1.0,
449                y: 2.0,
450                z: 3.0,
451                _pad: 0.0,
452            };
453
454            let bytes = codec.encode(&original).unwrap();
455            let env = build_test_envelope(&bytes);
456            let decoded: &Pos = codec.decode(&env).unwrap();
457
458            let env_payload_ptr = env.payload().as_ptr();
459            let decoded_ptr: *const u8 = std::ptr::from_ref::<Pos>(decoded).cast();
460            assert_eq!(
461                env_payload_ptr, decoded_ptr,
462                "BytemuckCodec must borrow from envelope payload, not copy"
463            );
464        }
465
466        #[test]
467        fn decode_rejects_wrong_size() {
468            let codec = BytemuckCodec;
469            // 8 bytes instead of 16 (size_of::<Pos>())
470            let env = build_test_envelope(&[0u8; 8]);
471            let result: Result<&Pos, _> = codec.decode(&env);
472            let err = result
473                .copied()
474                .expect_err("wrong-size payload must be rejected");
475            // The error must carry the upstream PodCastError detail — a size
476            // mismatch renders as `SizeMismatch`. Asserting the content (not just
477            // `is_err`) pins the `From<PodCastError>` conversion: an impl that
478            // dropped the cause would render an empty inner string.
479            assert!(
480                err.to_string().contains("SizeMismatch"),
481                "error must carry the PodCastError cause, got: {err}"
482            );
483        }
484    }
485}
486
487// ═══════════════════════════════════════════════════════════════════════════
488// Rkyv — archived-data zero-copy codec
489// ═══════════════════════════════════════════════════════════════════════════
490
491#[cfg(feature = "rkyv")]
492pub mod rkyv {
493    use ::rkyv::{
494        Archive, Serialize,
495        api::high::{HighSerializer, HighValidator, to_bytes_in},
496        bytecheck::CheckBytes,
497        rancor,
498        ser::allocator::ArenaHandle,
499        util::AlignedVec,
500    };
501    use bytes::Bytes;
502
503    use super::{Decode, Encode};
504    use crate::envelope::PersistedEnvelope;
505
506    /// Zero-copy codec backed by rkyv 0.8.
507    ///
508    /// - **Encode**: serializes `E` to its archived bytes via
509    ///   [`rkyv::to_bytes`]. Adapts `AlignedVec` → `Bytes` via
510    ///   `Bytes::from(vec.into_vec())`. (`AlignedVec`'s alignment doesn't
511    ///   survive the conversion, but the wire-format aligns the payload
512    ///   when the row is built, so the envelope's payload regains
513    ///   alignment.)
514    /// - **Decode**: validates + accesses the envelope payload as
515    ///   `&Archived<E>` via [`rkyv::access`]. Zero-copy on the read
516    ///   path; the returned reference borrows directly from
517    ///   `env.payload()`.
518    ///
519    /// `Output<'a> = &'a <E as Archive>::Archived`.
520    #[derive(Debug, Default, Clone, Copy)]
521    pub struct RkyvCodec;
522
523    impl<E> Encode<E> for RkyvCodec
524    where
525        E: for<'a> Serialize<HighSerializer<AlignedVec, ArenaHandle<'a>, rancor::Error>>
526            + Send
527            + Sync
528            + 'static,
529    {
530        type Error = rancor::Error;
531
532        fn encode(&self, event: &E) -> Result<Bytes, Self::Error> {
533            let aligned = to_bytes_in::<_, rancor::Error>(event, AlignedVec::new())?;
534            // AlignedVec::into_vec → Vec<u8> → Bytes. Alignment is lost
535            // here, but wire::encode_frame re-aligns the payload on its way
536            // into the envelope.
537            Ok(Bytes::from(aligned.into_vec()))
538        }
539    }
540
541    impl<E> Decode<E> for RkyvCodec
542    where
543        E: Archive + Send + Sync + 'static,
544        E::Archived: for<'a> CheckBytes<HighValidator<'a, rancor::Error>>,
545    {
546        type Output<'a>
547            = &'a E::Archived
548        where
549            Self: 'a;
550        type Error = rancor::Error;
551
552        fn decode<'a>(
553            &'a self,
554            env: &'a PersistedEnvelope,
555        ) -> Result<&'a E::Archived, Self::Error> {
556            ::rkyv::access::<E::Archived, rancor::Error>(env.payload())
557        }
558    }
559
560    #[cfg(test)]
561    mod tests {
562        use super::*;
563        use crate::wire;
564
565        #[derive(::rkyv::Archive, ::rkyv::Serialize, ::rkyv::Deserialize, Debug, PartialEq, Eq)]
566        struct Move {
567            steps: u32,
568            dir: u8,
569        }
570
571        fn build_test_envelope(payload: &[u8]) -> PersistedEnvelope {
572            let et = crate::value::EventType::from_static_str("Move");
573            let pl = crate::value::Payload::from_bytes(bytes::Bytes::copy_from_slice(payload))
574                .expect("valid payload");
575            let sv = crate::value::SchemaVersion::INITIAL;
576            let frame = wire::encode_frame(sv, &et, &pl, None).expect("wire encode_frame ok");
577            PersistedEnvelope::try_new(
578                mnesis::Version::INITIAL,
579                frame.value,
580                sv,
581                frame.offsets.event_type,
582                frame.offsets.payload,
583                None,
584            )
585            .expect("envelope construction ok")
586        }
587
588        #[test]
589        fn round_trip_yields_equal_archived_fields() {
590            let codec = RkyvCodec;
591            let original = Move { steps: 42, dir: 3 };
592
593            let bytes = codec.encode(&original).unwrap();
594            let env = build_test_envelope(&bytes);
595            let archived: &ArchivedMove =
596                <RkyvCodec as Decode<Move>>::decode(&codec, &env).unwrap();
597
598            assert_eq!(archived.steps, 42);
599            assert_eq!(archived.dir, 3);
600        }
601    }
602}