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}