Skip to main content

ruststream/codec/
mod.rs

1//! Pluggable serialization codecs.
2//!
3//! A [`Codec`] turns a value into wire bytes and back. It is not tied to the broker: on the
4//! consume side the pipeline is `bytes -> Codec -> typed payload -> handler`, and on the publish
5//! side the same runs in reverse. The codec is fixed where the handler is mounted, so it costs
6//! nothing on the delivery path.
7//!
8//! # Cargo features
9//!
10//! Codecs are additive cargo features: enable only what you need. A message type derives
11//! `serde::Deserialize`, and a reply type `Serialize` as well.
12//!
13//! * `json` (default): [`JsonCodec`] via `serde_json`, `application/json`.
14//! * `msgpack`: [`MsgpackCodec`] via `rmp-serde`, `application/msgpack`.
15//! * `cbor`: [`CborCodec`] via `ciborium`, `application/cbor`.
16//!
17//! [`DefaultCodec`] is an alias picked by the enabled features: `json`, else `cbor`, else
18//! `msgpack`. It is what `include(def)` decodes with and what `.out_reply(policy)` encodes with
19//! when nothing names a codec. With no codec feature at all there is nothing to name, so a
20//! mounting that needs the default becomes a compile error listing the ways out: enable a
21//! feature, name a codec, or move the message to a byte lane.
22//!
23//! # Where the codec comes from
24//!
25//! The decode codec comes from the most specific level that names one, and `include` takes no
26//! codec argument:
27//!
28//! 1. per handler, `Router::new().with_codec(codec).include(def)`;
29//! 2. per scope, `with_broker_codec(broker, codec, |b| ..)`;
30//! 3. [`DefaultCodec`].
31//!
32//! The publish side mirrors it at the mount site: `.out_reply(policy).codec(codec)` names the
33//! reply codec, `.out(marker, policy).codec(codec)` a slot's, and `.with_codec(codec)` on the
34//! builder one call's. The request decodes with the scope's codec while the reply travels on the
35//! wiring the chain built, so the two formats differ freely. The codec is a property of the
36//! mounting, never of the message type.
37//!
38//! ```
39//! # #[cfg(all(feature = "macros", feature = "memory", feature = "json", feature = "cbor"))]
40//! # mod demo {
41//! use ruststream::codec::{CborCodec, JsonCodec};
42//! use ruststream::memory::prelude::*;
43//! use serde::Deserialize;
44//!
45//! # #[derive(Deserialize)]
46//! # struct Order {
47//! #     id: u64,
48//! # }
49//! # #[subscriber("orders")]
50//! # async fn handle(order: &Order) -> HandlerOutcome {
51//! #     HandlerOutcome::ack()
52//! # }
53//! # #[subscriber("audit")]
54//! # async fn audit(order: &Order) -> HandlerOutcome {
55//! #     HandlerOutcome::ack()
56//! # }
57//! fn app() -> RustStream {
58//!     RustStream::new(AppInfo::new("codecs", "0.1.0"))
59//!         // per scope: every handler mounted here decodes with CBOR
60//!         .with_broker_codec(MemoryBroker::new(), CborCodec, |b| {
61//!             b.include(handle);
62//!             // per handler: this mounting decodes with JSON
63//!             b.include_router(Router::new().with_codec(JsonCodec).include(audit));
64//!         })
65//! }
66//! # }
67//! # fn main() {}
68//! ```
69//!
70//! A payload that does not decode is settled by the subscriber's `on_failure(decode = ..)`
71//! policy, a drop by default; see [`FailurePolicy`](crate::runtime::FailurePolicy).
72//!
73//! # Byte lanes
74//!
75//! A value that is its own encoding takes no codec position. A newtype over `&'a [u8]` deriving
76//! [`Deserialized`](macro@crate::Deserialized) receives the delivery's bytes as they arrived,
77//! and a type deriving [`Serialized`](macro@crate::Serialized) publishes the bytes it produces.
78//! A generated Protobuf message goes on both lanes with `#[wire(prost)]` next to the derives;
79//! the general form names the functions, `#[wire(encode = <path>, decode = <path>)]`, and serves
80//! Cap'n Proto, `FlatBuffers` and a hand-rolled frame alike. A service on the lanes alone runs
81//! with no codec feature.
82//!
83//! # A codec of your own
84//!
85//! Implement [`Codec`] and pass the value wherever a built-in one goes. A codec generic over
86//! another composes: the inner one decides the format, the wrapper transforms the bytes around
87//! it, which is the shape of a versioned envelope, a registry header or an encrypting layer.
88//! Both sides return [`CodecError`]; an inner error passes up with `?`, and a failure of the
89//! wrapper becomes [`CodecError::Decode`] or [`CodecError::Encode`] with your error as its
90//! source. [`Codec::CONTENT_TYPE`] names the media type the generated document reports.
91//!
92//! ```
93//! # #[cfg(not(feature = "json"))]
94//! # fn main() {}
95//! # #[cfg(feature = "json")]
96//! # fn main() {
97//! use std::fmt;
98//!
99//! use ruststream::BytesMut;
100//! use ruststream::codec::{Codec, CodecError, JsonCodec};
101//! use serde::Serialize;
102//! use serde::de::DeserializeOwned;
103//!
104//! /// Frames another codec's bytes with a one-byte format version.
105//! #[derive(Clone, Copy)]
106//! struct Versioned<C>(C);
107//!
108//! const VERSION: u8 = 1;
109//!
110//! #[derive(Debug)]
111//! struct BadVersion(u8);
112//!
113//! impl fmt::Display for BadVersion {
114//!     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115//!         write!(f, "envelope version {} is not the version this build reads", self.0)
116//!     }
117//! }
118//!
119//! impl std::error::Error for BadVersion {}
120//!
121//! impl<C: Codec> Codec for Versioned<C> {
122//!     const CONTENT_TYPE: &'static str = C::CONTENT_TYPE;
123//!
124//!     fn encode<T: Serialize>(&self, value: &T) -> Result<BytesMut, CodecError> {
125//!         let mut framed = BytesMut::new();
126//!         self.encode_into(value, &mut framed)?;
127//!         Ok(framed)
128//!     }
129//!
130//!     fn encode_into<T: Serialize>(&self, value: &T, buf: &mut BytesMut) -> Result<(), CodecError> {
131//!         buf.extend_from_slice(&[VERSION]);
132//!         self.0.encode_into(value, buf)
133//!     }
134//!
135//!     fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, CodecError> {
136//!         match bytes {
137//!             [VERSION, payload @ ..] => self.0.decode(payload),
138//!             [version, ..] => Err(CodecError::Decode(Box::new(BadVersion(*version)))),
139//!             [] => Err(CodecError::Decode(Box::new(BadVersion(0)))),
140//!         }
141//!     }
142//! }
143//!
144//! let codec = Versioned(JsonCodec);
145//! let bytes = codec.encode(&7u32).unwrap();
146//! assert_eq!(bytes[0], VERSION);
147//! let back: u32 = codec.decode(&bytes).unwrap();
148//! assert_eq!(back, 7);
149//! # }
150//! ```
151//!
152//! [`Codec::encode_into`] is the buffer-taking sibling of `encode`, and the one the publish path
153//! calls for a broker that only reads the payload: inside a dispatch loop that buffer is the
154//! loop's own, so a codec that writes into it leaves the publish nothing to allocate. It is
155//! defaulted (encode into a buffer of its own, then copy), so a codec compiles without it and is
156//! faster with it. The wrapper above writes its version byte into the caller's buffer and hands
157//! the rest to the inner codec, which is why its `encode` is the one line that calls it.
158//!
159//! `encode` and `decode` are synchronous, and that is the boundary of what fits: only what a
160//! constant and the bytes at hand decide. An integration that needs I/O to serialize (a schema
161//! registry, a key service) goes around the codec, on the async edges: the broker's delivery
162//! path on the way in, a [`PublishLayer`](crate::runtime::PublishLayer) on the way out.
163
164/// The block an encode buffer starts with, in bytes.
165///
166/// A serializer writes a document in fragments, so a buffer that starts empty is grown - and
167/// reallocated - more than once before even the smallest message is out. This is the capacity
168/// `serde_json`'s own `to_vec` reserves, and it puts a single block between a value and the wire
169/// at every size: a small document fits the first write, and a larger one grows from here exactly
170/// as a `Vec` would.
171#[cfg(any(feature = "json", feature = "msgpack", feature = "cbor"))]
172pub(crate) const ENCODE_CAPACITY: usize = 128;
173
174#[cfg(feature = "json")]
175mod json;
176
177#[cfg(feature = "msgpack")]
178mod msgpack;
179
180#[cfg(feature = "cbor")]
181mod cbor;
182
183#[cfg(feature = "cbor")]
184pub use self::cbor::CborCodec;
185#[cfg(feature = "json")]
186pub use json::JsonCodec;
187#[cfg(feature = "msgpack")]
188pub use msgpack::MsgpackCodec;
189
190/// The codec used when an `include` / publisher call does not name one explicitly.
191///
192/// Resolved at compile time by feature priority: `json`, then `cbor`, then `msgpack`. It exists
193/// only when at least one codec feature is enabled; with none, every call site must name a codec.
194#[cfg(feature = "json")]
195pub type DefaultCodec = JsonCodec;
196/// The codec used when an `include` / publisher call does not name one explicitly. See the `json`
197/// variant for details.
198#[cfg(all(not(feature = "json"), feature = "cbor"))]
199pub type DefaultCodec = CborCodec;
200/// The codec used when an `include` / publisher call does not name one explicitly. See the `json`
201/// variant for details.
202#[cfg(all(not(feature = "json"), not(feature = "cbor"), feature = "msgpack"))]
203pub type DefaultCodec = MsgpackCodec;
204
205use std::error::Error as StdError;
206#[cfg(any(feature = "json", feature = "msgpack", feature = "cbor"))]
207use std::io;
208
209use bytes::BytesMut;
210use serde::{Serialize, de::DeserializeOwned};
211use thiserror::Error;
212
213/// The [`io::Write`] a serializer writes an encode buffer through: every write is an append.
214///
215/// `bytes`' own writer answers `write_all` with the trait's default loop over `write`, which the
216/// optimizer inlines or not depending on what else the binary holds; the difference is several
217/// hundred instructions per encoded message. These methods are appends marked for inlining, so
218/// the encode path costs the same whatever else changes around it.
219#[cfg(any(feature = "json", feature = "msgpack", feature = "cbor"))]
220pub(crate) struct BufWriter<'a>(pub(crate) &'a mut BytesMut);
221
222#[cfg(any(feature = "json", feature = "msgpack", feature = "cbor"))]
223impl io::Write for BufWriter<'_> {
224    #[inline]
225    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
226        self.0.extend_from_slice(buf);
227        Ok(buf.len())
228    }
229
230    #[inline]
231    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
232        self.0.extend_from_slice(buf);
233        Ok(())
234    }
235
236    #[inline]
237    fn flush(&mut self) -> io::Result<()> {
238        Ok(())
239    }
240}
241
242/// Errors returned by codec implementations.
243#[derive(Debug, Error)]
244#[non_exhaustive]
245pub enum CodecError {
246    /// The codec failed to encode a Rust value into bytes.
247    #[error("encode failed: {0}")]
248    Encode(#[source] Box<dyn StdError + Send + Sync>),
249
250    /// The codec failed to decode bytes into a Rust value.
251    #[error("decode failed: {0}")]
252    Decode(#[source] Box<dyn StdError + Send + Sync>),
253}
254
255/// A serializer that converts Rust values to and from bytes.
256///
257/// Implementations are stateless and cheap to clone. The trait uses generic methods rather
258/// than associated types so a single codec instance can handle any `Serialize` /
259/// `DeserializeOwned` value. This means `dyn Codec` is not object-safe; use generics or
260/// boxed concrete codecs at the call site.
261///
262/// # Examples
263///
264/// ```
265/// # #[cfg(feature = "json")]
266/// # fn main() -> Result<(), ruststream::codec::CodecError> {
267/// use ruststream::codec::{Codec, JsonCodec};
268/// # use serde::{Serialize, Deserialize};
269///
270/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
271/// struct Order { id: u32, total: f64 }
272///
273/// let codec = JsonCodec;
274/// let bytes = codec.encode(&Order { id: 1, total: 9.99 })?;
275/// let back: Order = codec.decode(&bytes)?;
276/// assert_eq!(back, Order { id: 1, total: 9.99 });
277/// # Ok(())
278/// # }
279/// # #[cfg(not(feature = "json"))]
280/// # fn main() {}
281/// ```
282pub trait Codec: Send + Sync {
283    /// The media type of the bytes this codec produces, reported as the `contentType` of every
284    /// message it encodes or decodes in the generated `AsyncAPI` document.
285    ///
286    /// The built-in codecs name `application/json`, `application/cbor` (RFC 8949) and
287    /// `application/msgpack`. `MessagePack` has no registered media type; of the two spellings
288    /// in use, `application/msgpack` is the one this crate reports.
289    ///
290    /// The default is `application/octet-stream`: what a codec whose output carries no media
291    /// type of its own should keep.
292    ///
293    /// # Examples
294    ///
295    /// ```
296    /// # #[cfg(feature = "json")]
297    /// # fn main() {
298    /// use ruststream::codec::{Codec, JsonCodec};
299    ///
300    /// assert_eq!(JsonCodec::CONTENT_TYPE, "application/json");
301    /// # }
302    /// # #[cfg(not(feature = "json"))]
303    /// # fn main() {}
304    /// ```
305    const CONTENT_TYPE: &'static str = "application/octet-stream";
306
307    /// Encodes `value` into a mutable byte buffer.
308    ///
309    /// Returning [`BytesMut`] lets the encoded buffer move into the publish pipeline (an
310    /// [`Outgoing`](crate::runtime::Outgoing) payload) without a copy, while still allowing
311    /// publish middleware to mutate it in place. It is what a publish to a transport that keeps
312    /// the payload ([`Take`](crate::Take)) calls, once per message.
313    ///
314    /// # Errors
315    ///
316    /// Returns [`CodecError::Encode`] when the underlying serializer fails.
317    fn encode<T: Serialize>(&self, value: &T) -> Result<BytesMut, CodecError>;
318
319    /// Encodes `value` into the end of a buffer the caller owns.
320    ///
321    /// What a publish to a transport that only reads the payload ([`Lend`](crate::Lend)) calls:
322    /// inside a dispatch loop that buffer is the loop's own, reused for every message, so a
323    /// codec that writes into it leaves the publish path with nothing to allocate. Implement it
324    /// wherever the serializer can write into a sink; the default encodes into a buffer of its
325    /// own and copies, which is correct but pays the allocation this exists to avoid.
326    ///
327    /// # Errors
328    ///
329    /// Returns [`CodecError::Encode`] when the underlying serializer fails. The buffer's
330    /// contents are then unspecified: the publish path discards what a failed encode wrote.
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// # #[cfg(feature = "json")]
336    /// # fn main() -> Result<(), ruststream::codec::CodecError> {
337    /// use ruststream::BytesMut;
338    /// use ruststream::codec::{Codec, JsonCodec};
339    ///
340    /// let mut buf = BytesMut::new();
341    /// JsonCodec.encode_into(&7u8, &mut buf)?;
342    /// assert_eq!(&buf[..], b"7");
343    /// # Ok(())
344    /// # }
345    /// # #[cfg(not(feature = "json"))]
346    /// # fn main() {}
347    /// ```
348    fn encode_into<T: Serialize>(&self, value: &T, buf: &mut BytesMut) -> Result<(), CodecError> {
349        buf.extend_from_slice(&self.encode(value)?);
350        Ok(())
351    }
352
353    /// Decodes `bytes` into a Rust value of type `T`.
354    ///
355    /// # Errors
356    ///
357    /// Returns [`CodecError::Decode`] when the underlying deserializer fails.
358    fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, CodecError>;
359}