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}