Skip to main content

reliar_core/
envelope.rs

1//! `Envelope<T>` / `SerializedEnvelope` and their builder (ADR 0003, ADR 0011).
2
3use core::fmt;
4
5use bytes::Bytes;
6
7use crate::{
8    ConversationId, CorrelationId, CorrelationMetadata, HeaderError, Headers, Message, MessageId,
9    MessageType, Metadata,
10};
11
12/// An envelope: a typed or serialized body plus the metadata Reliar understands and the custom
13/// headers it does not. `Envelope != OutboxRecord != InboxRecord` — nothing here
14/// carries delivery state (attempts, leases, dead-letter bookkeeping).
15///
16/// ```
17/// use reliar_core::Envelope;
18///
19/// #[derive(serde::Serialize, serde::Deserialize)]
20/// struct Ping;
21/// impl reliar_core::Message for Ping {
22///     const TYPE: &'static str = "ping";
23///     const VERSION: u16 = 1;
24/// }
25///
26/// let envelope = Envelope::builder(Ping).build();
27/// assert_eq!(envelope.message_type.to_string(), "ping.v1");
28/// ```
29#[non_exhaustive]
30pub struct Envelope<T> {
31    /// The envelope's own identity.
32    pub id: MessageId,
33
34    /// The message's stable contract identity — `T::TYPE`/`T::VERSION`, never chosen ad hoc.
35    pub message_type: MessageType,
36
37    /// The message body: typed on the application side, `bytes::Bytes` once serialized.
38    pub body: T,
39
40    /// Canonical, typed framework metadata — the single source of truth (ADR 0004).
41    pub metadata: Metadata,
42
43    /// Private: preserves [`Headers`]' validation invariants — mutate only through
44    /// [`Self::headers_mut`]/[`Self::set_headers`].
45    pub(crate) headers: Option<Headers>,
46}
47
48/// The persistence/transport form: an envelope whose body has already been serialized to bytes.
49///
50/// ```
51/// use bytes::Bytes;
52/// use reliar_core::{Envelope, SerializedEnvelope};
53/// # #[derive(serde::Serialize, serde::Deserialize)]
54/// # struct Ping;
55/// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
56///
57/// let typed = Envelope::builder(Ping).build();
58/// let wire: SerializedEnvelope = typed.map_body(|_| Bytes::from_static(b"{}"));
59/// assert_eq!(wire.body.as_ref(), b"{}");
60/// ```
61pub type SerializedEnvelope = Envelope<Bytes>;
62
63impl<T> Envelope<T> {
64    /// The envelope's custom headers, if any were set.
65    ///
66    /// ```
67    /// use reliar_core::Envelope;
68    /// # #[derive(serde::Serialize, serde::Deserialize)]
69    /// # struct Ping;
70    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
71    /// let envelope = Envelope::builder(Ping).build();
72    /// assert!(envelope.headers().is_none());
73    /// ```
74    #[must_use]
75    pub fn headers(&self) -> Option<&Headers> {
76        self.headers.as_ref()
77    }
78
79    /// Mutably accesses the envelope's custom headers, lazily allocating an empty [`Headers`]
80    /// the first time this is called.
81    ///
82    /// ```
83    /// use reliar_core::Envelope;
84    /// # #[derive(serde::Serialize, serde::Deserialize)]
85    /// # struct Ping;
86    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
87    /// let mut envelope = Envelope::builder(Ping).build();
88    /// envelope.headers_mut().insert("x-a", "1")?;
89    /// assert_eq!(envelope.headers().unwrap().get("x-a"), Some("1"));
90    /// # Ok::<(), reliar_core::HeaderError>(())
91    /// ```
92    pub fn headers_mut(&mut self) -> &mut Headers {
93        self.headers.get_or_insert_with(Headers::default)
94    }
95
96    /// Replaces the whole header map. The rehydration path for providers and transport mappers,
97    /// which read back an already-validated map rather than inserting key by key.
98    ///
99    /// ```
100    /// use reliar_core::{Envelope, Headers};
101    /// # #[derive(serde::Serialize, serde::Deserialize)]
102    /// # struct Ping;
103    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
104    /// let mut envelope = Envelope::builder(Ping).build();
105    /// let mut headers = Headers::default();
106    /// headers.insert("x-a", "1")?;
107    /// envelope.set_headers(Some(headers));
108    /// assert_eq!(envelope.headers().unwrap().get("x-a"), Some("1"));
109    /// # Ok::<(), reliar_core::HeaderError>(())
110    /// ```
111    pub fn set_headers(&mut self, headers: Option<Headers>) {
112        self.headers = headers;
113    }
114
115    /// Converts the body, keeping every other field. The only conversion between typed and
116    /// serialized envelopes — no field is ever re-declared, so none can be dropped in the
117    /// process (ADR 0003).
118    ///
119    /// ```
120    /// use bytes::Bytes;
121    /// use reliar_core::Envelope;
122    /// # #[derive(serde::Serialize, serde::Deserialize)]
123    /// # struct Ping;
124    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
125    /// let envelope = Envelope::builder(Ping).build();
126    /// let serialized: Envelope<Bytes> = envelope.map_body(|_| Bytes::from_static(b"{}"));
127    /// assert_eq!(serialized.body.as_ref(), b"{}");
128    /// ```
129    #[must_use]
130    pub fn map_body<U>(self, f: impl FnOnce(T) -> U) -> Envelope<U> {
131        Envelope {
132            id: self.id,
133            message_type: self.message_type,
134            body: f(self.body),
135            metadata: self.metadata,
136            headers: self.headers,
137        }
138    }
139
140    /// Fallible variant of [`Self::map_body`], for `SerializedEnvelope -> Envelope<T>` via a
141    /// [`Serializer`](crate::Serializer).
142    ///
143    /// # Errors
144    ///
145    /// Returns whatever error `f` returns, unchanged.
146    ///
147    /// ```
148    /// use bytes::Bytes;
149    /// use reliar_core::Envelope;
150    /// # #[derive(serde::Serialize, serde::Deserialize)]
151    /// # struct Ping;
152    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
153    /// let wire = Envelope::builder(Ping)
154    ///     .build()
155    ///     .map_body(|_| Bytes::from_static(b"{}"));
156    ///
157    /// let typed: Envelope<Ping> = wire.try_map_body(|_body| Ok::<_, std::convert::Infallible>(Ping))?;
158    /// assert_eq!(typed.message_type.to_string(), "ping.v1");
159    /// # Ok::<(), std::convert::Infallible>(())
160    /// ```
161    pub fn try_map_body<U, E>(self, f: impl FnOnce(T) -> Result<U, E>) -> Result<Envelope<U>, E> {
162        Ok(Envelope {
163            id: self.id,
164            message_type: self.message_type,
165            body: f(self.body)?,
166            metadata: self.metadata,
167            headers: self.headers,
168        })
169    }
170}
171
172impl<T: Message> Envelope<T> {
173    /// Starts building an envelope for `body`. `message_type` is derived from `T::TYPE`/
174    /// `T::VERSION` and cannot be passed in (ADR 0010).
175    ///
176    /// ```
177    /// use reliar_core::Envelope;
178    ///
179    /// #[derive(serde::Serialize, serde::Deserialize)]
180    /// struct OrderCreated { order_id: u64 }
181    ///
182    /// impl reliar_core::Message for OrderCreated {
183    ///     const TYPE: &'static str = "orders.created";
184    ///     const VERSION: u16 = 1;
185    /// }
186    ///
187    /// let envelope = Envelope::builder(OrderCreated { order_id: 42 })
188    ///     .tenant("acme")
189    ///     .header("x-import-batch", "2026-09-04")?
190    ///     .build();
191    ///
192    /// assert_eq!(envelope.message_type.to_string(), "orders.created.v1");
193    /// assert_eq!(envelope.metadata.tenant_id.as_deref(), Some("acme"));
194    /// # Ok::<(), reliar_core::HeaderError>(())
195    /// ```
196    pub fn builder(body: T) -> EnvelopeBuilder<T> {
197        EnvelopeBuilder::new(body)
198    }
199}
200
201impl SerializedEnvelope {
202    /// Rehydration entry point for providers and transport mappers, which have a `MessageType`
203    /// read from storage or the wire rather than from a Rust type (ADR 0011).
204    ///
205    /// ```
206    /// use bytes::Bytes;
207    /// use reliar_core::{Metadata, MessageId, MessageType, SerializedEnvelope};
208    ///
209    /// let envelope = SerializedEnvelope::from_parts(
210    ///     MessageId::new(),
211    ///     MessageType::from_parts("orders.created".to_string(), 1),
212    ///     Bytes::from_static(b"{}"),
213    ///     Metadata::default(),
214    ///     None,
215    /// );
216    /// assert_eq!(envelope.message_type.name(), "orders.created");
217    /// ```
218    #[must_use]
219    pub fn from_parts(
220        id: MessageId,
221        message_type: MessageType,
222        body: Bytes,
223        metadata: Metadata,
224        headers: Option<Headers>,
225    ) -> Self {
226        Self {
227            id,
228            message_type,
229            body,
230            metadata,
231            headers,
232        }
233    }
234}
235
236/// Elides the body unconditionally: a typed body may be arbitrary application data and a
237/// serialized one is raw payload bytes, and neither belongs in a log line (ADR 0003).
238impl<T> fmt::Debug for Envelope<T> {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        f.debug_struct("Envelope")
241            .field("id", &self.id)
242            .field("message_type", &self.message_type)
243            .field("body", &"<elided>")
244            .field("metadata", &self.metadata)
245            .field("headers", &self.headers)
246            .finish()
247    }
248}
249
250impl<T: PartialEq> PartialEq for Envelope<T> {
251    fn eq(&self, other: &Self) -> bool {
252        self.id == other.id
253            && self.message_type == other.message_type
254            && self.body == other.body
255            && self.metadata == other.metadata
256            && self.headers == other.headers
257    }
258}
259
260/// `Clone` only where `T: Clone` — nothing in Reliar requires it, since a dispatcher moves owned
261/// records into publish tasks rather than cloning them; the impl exists for tests and host code.
262impl<T: Clone> Clone for Envelope<T> {
263    fn clone(&self) -> Self {
264        Self {
265            id: self.id,
266            message_type: self.message_type.clone(),
267            body: self.body.clone(),
268            metadata: self.metadata.clone(),
269            headers: self.headers.clone(),
270        }
271    }
272}
273
274/// Builds an [`Envelope<T>`]. Obtained from [`Envelope::builder`].
275///
276/// ```
277/// use reliar_core::Envelope;
278///
279/// #[derive(serde::Serialize, serde::Deserialize)]
280/// struct OrderCreated { order_id: u64 }
281/// impl reliar_core::Message for OrderCreated {
282///     const TYPE: &'static str = "orders.created";
283///     const VERSION: u16 = 1;
284/// }
285///
286/// let envelope = Envelope::builder(OrderCreated { order_id: 42 })
287///     .tenant("acme")
288///     .build();
289/// assert_eq!(envelope.metadata.tenant_id.as_deref(), Some("acme"));
290/// ```
291#[must_use]
292pub struct EnvelopeBuilder<T> {
293    id: Option<MessageId>,
294
295    body: T,
296
297    metadata: Metadata,
298
299    headers: Option<Headers>,
300}
301
302/// Elides the body: an in-progress envelope's body is arbitrary application data.
303impl<T> fmt::Debug for EnvelopeBuilder<T> {
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        f.debug_struct("EnvelopeBuilder")
306            .field("id", &self.id)
307            .field("body", &"<elided>")
308            .field("metadata", &self.metadata)
309            .field("headers", &self.headers)
310            .finish()
311    }
312}
313
314impl<T: Message> EnvelopeBuilder<T> {
315    fn new(body: T) -> Self {
316        Self {
317            id: None,
318            body,
319            metadata: Metadata::default(),
320            headers: None,
321        }
322    }
323
324    /// Overrides the generated id. Defaults to a fresh `UUIDv7`.
325    ///
326    /// ```
327    /// use reliar_core::{Envelope, MessageId};
328    /// # #[derive(serde::Serialize, serde::Deserialize)]
329    /// # struct Ping;
330    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
331    /// let id = MessageId::new();
332    /// let envelope = Envelope::builder(Ping).id(id).build();
333    /// assert_eq!(envelope.id, id);
334    /// ```
335    pub fn id(mut self, id: MessageId) -> Self {
336        self.id = Some(id);
337
338        self
339    }
340
341    /// Replaces the whole metadata struct, including its correlation metadata. Conversation
342    /// rooting is decided by *value*, not by call order: if the replacement's `conversation_id`
343    /// is still [`crate::ConversationId::UNSET`], [`Self::build`] roots it at the envelope's own
344    /// id regardless of an earlier [`Self::conversation`] call; a non-`UNSET` value (including
345    /// one copied from a causing message) is kept.
346    ///
347    /// ```
348    /// use reliar_core::{Envelope, Metadata};
349    /// # #[derive(serde::Serialize, serde::Deserialize)]
350    /// # struct Ping;
351    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
352    /// let mut metadata = Metadata::default();
353    /// metadata.tenant_id = Some("acme".to_string());
354    /// let envelope = Envelope::builder(Ping).metadata(metadata).build();
355    /// assert_eq!(envelope.metadata.tenant_id.as_deref(), Some("acme"));
356    /// ```
357    pub fn metadata(mut self, metadata: Metadata) -> Self {
358        self.metadata = metadata;
359
360        self
361    }
362
363    /// Replaces the correlation metadata (correlation id, conversation id, causation, request
364    /// id) as a group. Same value-decides-rooting rule as [`Self::metadata`].
365    ///
366    /// ```
367    /// use reliar_core::{CorrelationMetadata, Envelope};
368    /// # #[derive(serde::Serialize, serde::Deserialize)]
369    /// # struct Ping;
370    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
371    /// let mut correlation = CorrelationMetadata::default();
372    /// correlation.causation_id = Some(reliar_core::MessageId::new());
373    /// let envelope = Envelope::builder(Ping).correlation(correlation).build();
374    /// assert!(envelope.metadata.correlation.causation_id.is_some());
375    /// ```
376    pub fn correlation(mut self, correlation: CorrelationMetadata) -> Self {
377        self.metadata.correlation = correlation;
378
379        self
380    }
381
382    /// Sets the business correlation id.
383    ///
384    /// ```
385    /// use reliar_core::{CorrelationId, Envelope};
386    /// # #[derive(serde::Serialize, serde::Deserialize)]
387    /// # struct Ping;
388    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
389    /// let envelope = Envelope::builder(Ping)
390    ///     .correlation_id(CorrelationId::parse("checkout-42")?)
391    ///     .build();
392    /// assert_eq!(envelope.metadata.correlation.correlation_id.unwrap().as_str(), "checkout-42");
393    /// # Ok::<(), reliar_core::IdError>(())
394    /// ```
395    pub fn correlation_id(mut self, id: CorrelationId) -> Self {
396        self.metadata.correlation.correlation_id = Some(id);
397
398        self
399    }
400
401    /// Joins an existing conversation — typically the causing message's own `conversation_id`.
402    /// [`Self::build`] keeps this value as long as nothing later replaces it with
403    /// [`Self::metadata`] or [`Self::correlation`] (setter order matters only in that sense: the
404    /// last write to `conversation_id` wins, same as any other field).
405    ///
406    /// ```
407    /// use reliar_core::{ConversationId, Envelope};
408    /// use uuid::Uuid;
409    /// # #[derive(serde::Serialize, serde::Deserialize)]
410    /// # struct Ping;
411    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
412    /// let parent_conversation = ConversationId::from_uuid(Uuid::now_v7());
413    /// let envelope = Envelope::builder(Ping).conversation(parent_conversation).build();
414    /// assert_eq!(envelope.metadata.correlation.conversation_id, parent_conversation);
415    /// ```
416    pub fn conversation(mut self, id: ConversationId) -> Self {
417        self.metadata.correlation.conversation_id = id;
418
419        self
420    }
421
422    /// Records the message that caused this one.
423    ///
424    /// ```
425    /// use reliar_core::{Envelope, MessageId};
426    /// # #[derive(serde::Serialize, serde::Deserialize)]
427    /// # struct Ping;
428    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
429    /// let parent = MessageId::new();
430    /// let envelope = Envelope::builder(Ping).causation(parent).build();
431    /// assert_eq!(envelope.metadata.correlation.causation_id, Some(parent));
432    /// ```
433    pub fn causation(mut self, parent: MessageId) -> Self {
434        self.metadata.correlation.causation_id = Some(parent);
435
436        self
437    }
438
439    /// Sets the owning tenant.
440    ///
441    /// ```
442    /// use reliar_core::Envelope;
443    /// # #[derive(serde::Serialize, serde::Deserialize)]
444    /// # struct Ping;
445    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
446    /// let envelope = Envelope::builder(Ping).tenant("acme").build();
447    /// assert_eq!(envelope.metadata.tenant_id.as_deref(), Some("acme"));
448    /// ```
449    pub fn tenant(mut self, tenant_id: impl Into<String>) -> Self {
450        self.metadata.tenant_id = Some(tenant_id.into());
451
452        self
453    }
454
455    /// Sets the time after which the message must not be published.
456    ///
457    /// ```
458    /// use reliar_core::Envelope;
459    /// # #[derive(serde::Serialize, serde::Deserialize)]
460    /// # struct Ping;
461    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
462    /// let one_day = time::Duration::days(1);
463    /// let envelope = Envelope::builder(Ping)
464    ///     .expires_at(time::OffsetDateTime::now_utc() + one_day)
465    ///     .build();
466    /// assert!(envelope.metadata.delivery.expires_at.is_some());
467    /// ```
468    pub fn expires_at(mut self, at: time::OffsetDateTime) -> Self {
469        self.metadata.delivery.expires_at = Some(at);
470
471        self
472    }
473
474    /// Sets the W3C Trace Context to carry verbatim. Reliar never invents or re-derives it
475    /// (ADR 0004, ADR 0020).
476    ///
477    /// ```
478    /// use reliar_core::Envelope;
479    /// # #[derive(serde::Serialize, serde::Deserialize)]
480    /// # struct Ping;
481    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
482    /// let envelope = Envelope::builder(Ping)
483    ///     .trace("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", None)
484    ///     .build();
485    /// assert!(envelope.metadata.trace.traceparent.is_some());
486    /// ```
487    pub fn trace(mut self, traceparent: impl Into<String>, tracestate: Option<String>) -> Self {
488        self.metadata.trace.traceparent = Some(traceparent.into());
489        self.metadata.trace.tracestate = tracestate;
490
491        self
492    }
493
494    /// Sets one custom header. Returns `Err` if `k` uses the reserved `reliar-` prefix or
495    /// breaches a cap (see [`Headers::insert`]).
496    ///
497    /// # Errors
498    ///
499    /// Returns [`HeaderError`] under the same conditions as [`Headers::insert`].
500    ///
501    /// ```
502    /// use reliar_core::Envelope;
503    /// # #[derive(serde::Serialize, serde::Deserialize)]
504    /// # struct Ping;
505    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
506    /// let envelope = Envelope::builder(Ping).header("x-import-batch", "2026-09-04")?.build();
507    /// assert_eq!(envelope.headers().unwrap().get("x-import-batch"), Some("2026-09-04"));
508    /// # Ok::<(), reliar_core::HeaderError>(())
509    /// ```
510    pub fn header(
511        mut self,
512        k: impl Into<String>,
513        v: impl Into<String>,
514    ) -> Result<Self, HeaderError> {
515        self.headers
516            .get_or_insert_with(Headers::default)
517            .insert(k, v)?;
518
519        Ok(self)
520    }
521
522    /// Builds the envelope. `message_type` is `MessageType::of::<T>()`. `conversation_id`:
523    /// **iff** it is still [`crate::ConversationId::UNSET`], it becomes this envelope's own id
524    /// (an un-correlated message roots its own conversation); any other value — set via
525    /// [`Self::conversation`], [`Self::correlation`], or [`Self::metadata`] — is kept verbatim.
526    /// Rooting is decided by the value alone, never by which setter was called or in what order
527    /// (ADR 0011).
528    ///
529    /// ```
530    /// use reliar_core::Envelope;
531    /// # #[derive(serde::Serialize, serde::Deserialize)]
532    /// # struct Ping;
533    /// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
534    /// let envelope = Envelope::builder(Ping).build();
535    /// // An un-correlated message roots its own conversation.
536    /// assert_eq!(envelope.metadata.correlation.conversation_id.as_uuid(), envelope.id.as_uuid());
537    /// ```
538    #[must_use]
539    pub fn build(mut self) -> Envelope<T> {
540        let id = self.id.unwrap_or_default();
541
542        if self.metadata.correlation.conversation_id.is_unset() {
543            self.metadata.correlation.conversation_id = ConversationId::from_uuid(id.as_uuid());
544        }
545
546        Envelope {
547            id,
548            message_type: MessageType::of::<T>(),
549            body: self.body,
550            metadata: self.metadata,
551            headers: self.headers,
552        }
553    }
554}