Skip to main content

reliar_core/
metadata.rs

1//! Canonical, typed framework metadata (ADR 0003, ADR 0004).
2
3use core::fmt;
4
5use crate::{
6    ContentType, ConversationId, CorrelationId, MessageId, RequestId,
7    ids::{IdError, contains_control_char},
8};
9
10/// Canonical, typed framework metadata: the single source of truth. No value here is ever
11/// duplicated into [`Headers`](crate::Headers) (ADR 0004).
12///
13/// ```
14/// use reliar_core::{CorrelationId, Envelope, Message};
15///
16/// #[derive(serde::Serialize, serde::Deserialize)]
17/// struct OrderCreated;
18/// impl Message for OrderCreated {
19///     const TYPE: &'static str = "orders.created";
20///     const VERSION: u16 = 1;
21/// }
22///
23/// let envelope = Envelope::builder(OrderCreated)
24///     .tenant("acme")
25///     .correlation_id(CorrelationId::parse("checkout-42")?)
26///     .build();
27///
28/// // `Metadata` is reachable directly — it is the one place these values live.
29/// assert_eq!(envelope.metadata.tenant_id.as_deref(), Some("acme"));
30/// assert!(envelope.metadata.correlation.correlation_id.is_some());
31/// // An un-correlated message roots its own conversation.
32/// assert_eq!(envelope.metadata.correlation.conversation_id.as_uuid(), envelope.id.as_uuid());
33/// # Ok::<(), reliar_core::IdError>(())
34/// ```
35#[derive(Clone, Debug, Default, PartialEq)]
36#[non_exhaustive]
37pub struct Metadata {
38    /// Correlation and conversation identity.
39    pub correlation: CorrelationMetadata,
40
41    /// W3C Trace Context, carried verbatim.
42    pub trace: TraceContext,
43
44    /// Transport-independent routing hints.
45    pub routing: RoutingMetadata,
46
47    /// Serialization and delivery hints.
48    pub delivery: DeliveryMetadata,
49
50    /// The owning tenant, if this deployment is multi-tenant.
51    pub tenant_id: Option<String>,
52}
53
54/// Correlation and conversation identity for one envelope.
55///
56/// ```
57/// use reliar_core::{CorrelationId, Envelope};
58/// # #[derive(serde::Serialize, serde::Deserialize)]
59/// # struct Ping;
60/// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
61/// let envelope = Envelope::builder(Ping)
62///     .correlation_id(CorrelationId::parse("checkout-42")?)
63///     .build();
64/// assert_eq!(envelope.metadata.correlation.correlation_id.unwrap().as_str(), "checkout-42");
65/// # Ok::<(), reliar_core::IdError>(())
66/// ```
67#[derive(Clone, Debug, PartialEq)]
68#[non_exhaustive]
69pub struct CorrelationMetadata {
70    /// Application/business workflow correlation, set by the caller.
71    pub correlation_id: Option<CorrelationId>,
72
73    /// Groups every message in one business conversation.
74    pub conversation_id: ConversationId,
75
76    /// The message that directly caused this one.
77    pub causation_id: Option<MessageId>,
78
79    /// The inbound request that (transitively) caused this one.
80    pub request_id: Option<RequestId>,
81}
82
83/// Sets `conversation_id` to the [`ConversationId::UNSET`] sentinel (the nil UUID) — **not** a
84/// fresh mint — so [`crate::EnvelopeBuilder::build`] can tell "not yet rooted" from a genuinely
85/// chosen value by comparing it, not by tracking which builder setter was called.
86/// `build` replaces `UNSET` with the envelope's own id, so an un-correlated message is the root
87/// of its own conversation, and leaves any other value alone. A `Metadata` that never passes
88/// through the builder (e.g. read straight off `Default`) keeps the placeholder verbatim.
89impl Default for CorrelationMetadata {
90    fn default() -> Self {
91        Self {
92            correlation_id: None,
93            conversation_id: ConversationId::UNSET,
94            causation_id: None,
95            request_id: None,
96        }
97    }
98}
99
100/// W3C Trace Context, carried verbatim. Reliar never invents or re-derives it (ADR 0004,
101/// ADR 0020): a transport mapper writes these from an active span and reads them back on
102/// decode.
103///
104/// ```
105/// use reliar_core::Envelope;
106/// # #[derive(serde::Serialize, serde::Deserialize)]
107/// # struct Ping;
108/// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
109/// let envelope = Envelope::builder(Ping)
110///     .trace("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", None)
111///     .build();
112/// assert!(envelope.metadata.trace.traceparent.is_some());
113/// assert!(envelope.metadata.trace.tracestate.is_none());
114/// ```
115#[derive(Clone, Debug, Default, PartialEq, Eq)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117#[non_exhaustive]
118pub struct TraceContext {
119    /// The W3C `traceparent` header value.
120    pub traceparent: Option<String>,
121
122    /// The W3C `tracestate` header value.
123    pub tracestate: Option<String>,
124}
125
126/// Transport-independent routing only. Kafka partition keys, `RabbitMQ` exchanges and NATS
127/// subject options are transport concepts and must never appear here — a transport crate derives
128/// its own wire-level routing from [`Self::destination`] instead, without adding its concept to
129/// this struct.
130///
131/// ```
132/// use reliar_core::{EndpointAddress, Metadata};
133///
134/// let mut metadata = Metadata::default();
135/// metadata.routing.destination = Some(EndpointAddress::parse("orders-service")?);
136/// assert_eq!(metadata.routing.destination.unwrap().as_str(), "orders-service");
137/// # Ok::<(), reliar_core::IdError>(())
138/// ```
139#[derive(Clone, Debug, Default, PartialEq, Eq)]
140#[non_exhaustive]
141pub struct RoutingMetadata {
142    /// The logical origin of this message.
143    pub source: Option<EndpointAddress>,
144
145    /// The logical destination of this message.
146    pub destination: Option<EndpointAddress>,
147
148    /// Where a reply to this message should be sent.
149    pub reply_to: Option<EndpointAddress>,
150}
151
152/// An opaque, transport-interpreted address string (a queue name, a subject, a service name —
153/// Reliar does not care which). Capped at [`Self::MAX_LEN`] bytes.
154///
155/// ```
156/// use reliar_core::EndpointAddress;
157///
158/// let address = EndpointAddress::parse("orders-service")?;
159/// assert_eq!(address.as_str(), "orders-service");
160/// # Ok::<(), reliar_core::IdError>(())
161/// ```
162#[derive(Clone, Debug, PartialEq, Eq, Hash)]
163pub struct EndpointAddress(String);
164
165impl EndpointAddress {
166    /// Maximum length in bytes.
167    pub const MAX_LEN: usize = 256;
168
169    /// Validates and wraps an endpoint address. Returns `Err` for an empty string, one
170    /// containing a control character (including CR/LF — a header-injection surface), or one
171    /// over [`Self::MAX_LEN`] bytes.
172    ///
173    /// # Errors
174    ///
175    /// Returns [`IdError::Empty`], [`IdError::ControlCharacter`], or [`IdError::TooLong`].
176    ///
177    /// ```
178    /// use reliar_core::EndpointAddress;
179    ///
180    /// let address = EndpointAddress::parse("orders-service")?;
181    /// assert_eq!(address.as_str(), "orders-service");
182    /// assert!(EndpointAddress::parse("").is_err());
183    /// # Ok::<(), reliar_core::IdError>(())
184    /// ```
185    pub fn parse(s: impl Into<String>) -> Result<Self, IdError> {
186        let s = s.into();
187
188        if s.is_empty() {
189            return Err(IdError::Empty);
190        }
191
192        if contains_control_char(&s) {
193            return Err(IdError::ControlCharacter);
194        }
195
196        if s.len() > Self::MAX_LEN {
197            return Err(IdError::TooLong {
198                len: s.len(),
199                max: Self::MAX_LEN,
200            });
201        }
202
203        Ok(Self(s))
204    }
205
206    /// Returns the address as a string slice.
207    ///
208    /// ```
209    /// use reliar_core::EndpointAddress;
210    ///
211    /// let address = EndpointAddress::parse("orders-service")?;
212    /// assert_eq!(address.as_str(), "orders-service");
213    /// # Ok::<(), reliar_core::IdError>(())
214    /// ```
215    #[must_use]
216    pub fn as_str(&self) -> &str {
217        &self.0
218    }
219}
220
221impl fmt::Display for EndpointAddress {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        f.write_str(&self.0)
224    }
225}
226
227#[cfg(feature = "serde")]
228#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
229impl serde::Serialize for EndpointAddress {
230    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
231        s.collect_str(&self.0)
232    }
233}
234
235#[cfg(feature = "serde")]
236#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
237impl<'de> serde::Deserialize<'de> for EndpointAddress {
238    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
239        let raw = String::deserialize(d)?;
240
241        Self::parse(raw).map_err(serde::de::Error::custom)
242    }
243}
244
245/// Serialization and delivery hints for one envelope.
246///
247/// ```
248/// use reliar_core::Envelope;
249/// # #[derive(serde::Serialize, serde::Deserialize)]
250/// # struct Ping;
251/// # impl reliar_core::Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
252/// let one_day = time::Duration::days(1);
253/// let envelope = Envelope::builder(Ping)
254///     .expires_at(time::OffsetDateTime::now_utc() + one_day)
255///     .build();
256/// assert!(envelope.metadata.delivery.expires_at.is_some());
257/// // Set by the serializer that produced the body, never chosen at the call site.
258/// assert_eq!(envelope.metadata.delivery.content_type.as_str(), "application/json");
259/// ```
260#[derive(Clone, Debug, PartialEq)]
261#[non_exhaustive]
262pub struct DeliveryMetadata {
263    /// **Authoritatively set by the store at enqueue** from `Serializer::content_type()`, and
264    /// read back from the provider's `content_type` column on rehydration. The `Default` value
265    /// below is a placeholder a call site never chooses (ADR 0010).
266    pub content_type: ContentType,
267
268    /// When the application handed this message to Reliar (app clock; never compared against a
269    /// DB timestamp).
270    pub sent_at: Option<time::OffsetDateTime>,
271
272    /// The time after which this message must not be published. Enforced in DB time by a
273    /// provider's claim predicate; an expired pending row goes dead without consuming a retry
274    /// attempt.
275    pub expires_at: Option<time::OffsetDateTime>,
276
277    /// A transport mapper's broker-specific dedup key (falling back to the message id). Reliar
278    /// never deduplicates on it in the database.
279    pub deduplication_id: Option<String>,
280}
281
282impl Default for DeliveryMetadata {
283    fn default() -> Self {
284        Self {
285            content_type: ContentType::JSON,
286            sent_at: None,
287            expires_at: None,
288            deduplication_id: None,
289        }
290    }
291}
292
293#[cfg(feature = "serde")]
294#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
295mod serde_impls {
296    //! `Serialize`/`Deserialize` for hosts that want to persist or log `Metadata` themselves.
297    //! Unrelated to `reliar-store-postgres`'s own private JSONB persistence contract (ADR 0012),
298    //! which defines its own `MetadataRest` shape with its own forward-compatibility rules.
299
300    use serde::{Deserialize, Serialize};
301
302    use super::{CorrelationMetadata, DeliveryMetadata, Metadata, RoutingMetadata, TraceContext};
303
304    // `#[serde(default)]` on every field: a persisted blob missing a whole sub-struct (an
305    // 0.2 field addition, or a value written before it existed) still deserializes, falling
306    // back to that sub-struct's own `Default` — forward compatibility for `reliar-core`'s own
307    // optional `Metadata` serde, independent of `reliar-store-postgres`'s own JSONB persistence
308    // (ADR 0012), which defines its own `MetadataRest` shape with its own rules.
309    #[derive(Serialize, Deserialize)]
310    #[serde(remote = "Metadata")]
311    struct MetadataDef {
312        #[serde(default)]
313        correlation: CorrelationMetadata,
314
315        #[serde(default)]
316        trace: TraceContext,
317
318        #[serde(default)]
319        routing: RoutingMetadata,
320
321        #[serde(default)]
322        delivery: DeliveryMetadata,
323
324        #[serde(default)]
325        tenant_id: Option<String>,
326    }
327
328    impl Serialize for Metadata {
329        fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
330            MetadataDef::serialize(self, s)
331        }
332    }
333    impl<'de> Deserialize<'de> for Metadata {
334        fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
335            MetadataDef::deserialize(d)
336        }
337    }
338
339    impl Serialize for CorrelationMetadata {
340        fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
341            // Field names mirror `CorrelationMetadata` on purpose, for wire compatibility.
342            #[derive(Serialize)]
343            #[allow(clippy::struct_field_names)]
344            struct Def<'a> {
345                correlation_id: &'a Option<super::CorrelationId>,
346
347                conversation_id: &'a super::ConversationId,
348
349                causation_id: &'a Option<super::MessageId>,
350
351                request_id: &'a Option<super::RequestId>,
352            }
353
354            Def {
355                correlation_id: &self.correlation_id,
356                conversation_id: &self.conversation_id,
357                causation_id: &self.causation_id,
358                request_id: &self.request_id,
359            }
360            .serialize(s)
361        }
362    }
363    impl<'de> Deserialize<'de> for CorrelationMetadata {
364        fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
365            // `ConversationId` has no `Default` (ADR 0038), so a bare `#[serde(default)]` would
366            // not compile here; this explicit fallback must still agree with
367            // `CorrelationMetadata::default()`, which uses the `UNSET` sentinel. A blob missing
368            // `conversation_id` (e.g. written before it existed) falls back to the same sentinel.
369            fn default_conversation_id() -> super::ConversationId {
370                super::ConversationId::UNSET
371            }
372
373            #[derive(Deserialize)]
374            #[allow(clippy::struct_field_names)]
375            struct Def {
376                #[serde(default)]
377                correlation_id: Option<super::CorrelationId>,
378
379                #[serde(default = "default_conversation_id")]
380                conversation_id: super::ConversationId,
381
382                #[serde(default)]
383                causation_id: Option<super::MessageId>,
384
385                #[serde(default)]
386                request_id: Option<super::RequestId>,
387            }
388            let def = Def::deserialize(d)?;
389
390            Ok(CorrelationMetadata {
391                correlation_id: def.correlation_id,
392                conversation_id: def.conversation_id,
393                causation_id: def.causation_id,
394                request_id: def.request_id,
395            })
396        }
397    }
398
399    impl Serialize for RoutingMetadata {
400        fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
401            #[derive(Serialize)]
402            struct Def<'a> {
403                source: &'a Option<super::EndpointAddress>,
404
405                destination: &'a Option<super::EndpointAddress>,
406
407                reply_to: &'a Option<super::EndpointAddress>,
408            }
409
410            Def {
411                source: &self.source,
412                destination: &self.destination,
413                reply_to: &self.reply_to,
414            }
415            .serialize(s)
416        }
417    }
418    impl<'de> Deserialize<'de> for RoutingMetadata {
419        fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
420            #[derive(Deserialize)]
421            struct Def {
422                #[serde(default)]
423                source: Option<super::EndpointAddress>,
424
425                #[serde(default)]
426                destination: Option<super::EndpointAddress>,
427
428                #[serde(default)]
429                reply_to: Option<super::EndpointAddress>,
430            }
431            let def = Def::deserialize(d)?;
432
433            Ok(RoutingMetadata {
434                source: def.source,
435                destination: def.destination,
436                reply_to: def.reply_to,
437            })
438        }
439    }
440
441    impl Serialize for DeliveryMetadata {
442        fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
443            #[derive(Serialize)]
444            struct Def<'a> {
445                content_type: &'a super::ContentType,
446
447                #[serde(with = "time::serde::rfc3339::option")]
448                sent_at: &'a Option<time::OffsetDateTime>,
449
450                #[serde(with = "time::serde::rfc3339::option")]
451                expires_at: &'a Option<time::OffsetDateTime>,
452
453                deduplication_id: &'a Option<String>,
454            }
455
456            Def {
457                content_type: &self.content_type,
458                sent_at: &self.sent_at,
459                expires_at: &self.expires_at,
460                deduplication_id: &self.deduplication_id,
461            }
462            .serialize(s)
463        }
464    }
465    impl<'de> Deserialize<'de> for DeliveryMetadata {
466        fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
467            // `ContentType` has no public `Default` (ADR 0010: a call site never chooses one),
468            // so its missing-field fallback is this private fn rather than a bare
469            // `#[serde(default)]` — it mirrors `DeliveryMetadata::default()`'s own placeholder.
470            fn default_content_type() -> super::ContentType {
471                super::ContentType::JSON
472            }
473
474            #[derive(Deserialize)]
475            struct Def {
476                #[serde(default = "default_content_type")]
477                content_type: super::ContentType,
478
479                #[serde(default, with = "time::serde::rfc3339::option")]
480                sent_at: Option<time::OffsetDateTime>,
481
482                #[serde(default, with = "time::serde::rfc3339::option")]
483                expires_at: Option<time::OffsetDateTime>,
484
485                #[serde(default)]
486                deduplication_id: Option<String>,
487            }
488            let def = Def::deserialize(d)?;
489
490            Ok(DeliveryMetadata {
491                content_type: def.content_type,
492                sent_at: def.sent_at,
493                expires_at: def.expires_at,
494                deduplication_id: def.deduplication_id,
495            })
496        }
497    }
498}