reliar_core/ids.rs
1//! Identity newtypes shared by every envelope (ADR 0011, ADR 0015, ADR 0045).
2
3use core::fmt;
4
5use uuid::Uuid;
6
7/// Validation failures shared by every capped string identity newtype in `reliar-core`.
8///
9/// ```
10/// use reliar_core::{CorrelationId, IdError};
11///
12/// let err = CorrelationId::parse("").unwrap_err();
13/// assert_eq!(err, IdError::Empty);
14/// ```
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum IdError {
18 /// The value was empty.
19 Empty,
20
21 /// The value exceeded its type's maximum length.
22 TooLong {
23 /// The value's actual length in bytes.
24 len: usize,
25 /// The maximum allowed length in bytes.
26 max: usize,
27 },
28
29 /// The value contained a control character (including CR/LF) — a header-injection
30 /// surface once a mapper writes this value onto the wire.
31 ControlCharacter,
32}
33
34impl fmt::Display for IdError {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Self::Empty => f.write_str("value must not be empty"),
38 Self::TooLong { len, max } => {
39 write!(f, "value length {len} exceeds the maximum of {max}")
40 }
41 Self::ControlCharacter => f.write_str("value must not contain a control character"),
42 }
43 }
44}
45
46impl std::error::Error for IdError {}
47
48/// Shared by every capped string identity newtype (and [`crate::Headers`]): `true` if `s`
49/// contains a control character (including CR/LF), which would let a value smuggle extra
50/// header/line-oriented content onto the wire once a transport mapper writes it verbatim.
51///
52/// `char::is_control` matches Unicode category `Cc` (`U+0000..=U+001F`, `U+007F`,
53/// `U+0080..=U+009F`) — exactly the code points a line-oriented wire format (an HTTP-style
54/// header, a CSV row) treats specially. It is deliberately not a wider "non-printable" or
55/// "non-ASCII" check: rejecting e.g. combining marks or emoji would reject legitimate
56/// human-readable data this type has no reason to forbid.
57pub(crate) fn contains_control_char(s: &str) -> bool {
58 s.chars().any(char::is_control)
59}
60
61/// Declares a UUID-backed identity newtype: `Clone + Copy + Debug + Eq + Hash + Ord`, `from_uuid`/
62/// `as_uuid`, and a `Display` that renders the inner UUID verbatim. Every id Reliar mints is a
63/// `UUIDv7` (ADR 0015); an application may supply any UUID and Reliar SHALL NOT inspect or reject
64/// its version.
65///
66/// Three forms, in decreasing minting power (ADR 0038, ADR 0045):
67///
68/// - `uuid_id!($name in $krate)` — mints: a `new()` that generates a fresh `UUIDv7` plus a
69/// `Default` built on it. For an id Reliar originates on demand (`MessageId`).
70/// - `uuid_id!($name in $krate, no_default)` — mints (`new()`), but no `Default`. For an id a
71/// caller mints deliberately at a specific moment, never as a stand-in default value (a row id
72/// minted client-side, e.g. `InboxRecordId`).
73/// - `uuid_id!($name in $krate, no_mint)` — no `new()`, no `Default`. For an id that is always
74/// **derived** from another value or **host-supplied** (`ConversationId`, `RequestId`, a
75/// database-assigned row id) — there is no constructor that hands back a value nobody asked for.
76///
77/// `$krate` is the path the declared type is re-exported from (`reliar_core`, `reliar_outbox`,
78/// `reliar_inbox`, …) — it is required, not optional, so the generated methods' doctests `use`
79/// the type from where a caller actually finds it, never from `reliar-core` regardless of which
80/// crate invoked the macro.
81///
82/// The declared type is always `pub`; a private id newtype is not what this macro is for.
83///
84/// The macro's internal `@base`/`@mint` arms are reachable from outside once exported, but they
85/// are **not** public API — only the three forms documented above are contract (ADR 0045 §4).
86///
87/// ```
88/// reliar_core::uuid_id!(
89/// /// A row id assigned by the database.
90/// MyRowId in reliar_core,
91/// no_mint
92/// );
93///
94/// let raw = reliar_core::uuid::Uuid::now_v7();
95/// assert_eq!(MyRowId::from_uuid(raw).as_uuid(), raw);
96/// assert_eq!(MyRowId::from_uuid(raw).to_string(), raw.to_string());
97/// ```
98#[macro_export]
99macro_rules! uuid_id {
100 ($(#[$meta:meta])* $name:ident in $krate:ident) => {
101 $crate::uuid_id!(@base $(#[$meta])* $name in $krate);
102 $crate::uuid_id!(@mint $name in $krate);
103
104 impl ::core::default::Default for $name {
105 fn default() -> Self {
106 Self::new()
107 }
108 }
109 };
110 ($(#[$meta:meta])* $name:ident in $krate:ident, no_default) => {
111 $crate::uuid_id!(@base $(#[$meta])* $name in $krate);
112 $crate::uuid_id!(@mint $name in $krate);
113 };
114 ($(#[$meta:meta])* $name:ident in $krate:ident, no_mint) => {
115 $crate::uuid_id!(@base $(#[$meta])* $name in $krate);
116 };
117
118 (@base $(#[$meta:meta])* $name:ident in $krate:ident) => {
119 $(#[$meta])*
120 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
121 pub struct $name($crate::uuid::Uuid);
122
123 impl $name {
124 /// Wraps an existing UUID without inspecting or rejecting its version.
125 ///
126 #[doc = concat!("```\nuse ", stringify!($krate), "::", stringify!($name), ";")]
127 /// use reliar_core::uuid::Uuid;
128 ///
129 /// let raw = Uuid::now_v7();
130 #[doc = concat!("assert_eq!(", stringify!($name), "::from_uuid(raw).as_uuid(), raw);")]
131 /// ```
132 #[must_use]
133 pub const fn from_uuid(id: $crate::uuid::Uuid) -> Self {
134 Self(id)
135 }
136
137 /// Returns the inner UUID, unchanged.
138 ///
139 #[doc = concat!("```\nuse ", stringify!($krate), "::", stringify!($name), ";")]
140 /// use reliar_core::uuid::Uuid;
141 ///
142 /// let raw = Uuid::now_v7();
143 #[doc = concat!("assert_eq!(", stringify!($name), "::from_uuid(raw).as_uuid(), raw);")]
144 /// ```
145 #[must_use]
146 pub const fn as_uuid(&self) -> $crate::uuid::Uuid {
147 self.0
148 }
149 }
150
151 impl ::core::fmt::Display for $name {
152 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
153 ::core::fmt::Display::fmt(&self.0, f)
154 }
155 }
156 };
157
158 (@mint $name:ident in $krate:ident) => {
159 #[allow(
160 clippy::new_without_default,
161 reason = "`Default` is opt-in: the bare `uuid_id!` form adds it, `no_default` does not"
162 )]
163 impl $name {
164 /// Generates a fresh `UUIDv7` id.
165 ///
166 #[doc = concat!("```\nuse ", stringify!($krate), "::", stringify!($name), ";\n")]
167 #[doc = concat!("assert!(!", stringify!($name), "::new().as_uuid().is_nil());")]
168 /// ```
169 #[must_use]
170 pub fn new() -> Self {
171 Self($crate::uuid::Uuid::now_v7())
172 }
173 }
174 };
175}
176
177/// Implements `serde::Serialize`/`Deserialize` for a [`uuid_id!`]-declared type: the canonical
178/// hyphenated UUID string, written with `collect_str` and read with `Uuid::parse_str` — never
179/// through `uuid`'s own `serde` feature, so enabling a caller's `serde` feature never has to unify
180/// `uuid`'s feature set.
181///
182/// The **caller** writes the `#[cfg(feature = "serde")]` on the invocation; the predicate cannot
183/// live inside this macro, since it would resolve against `reliar-core`'s feature table rather
184/// than the caller's (ADR 0045). Must be invoked in the declared type's own module (or a
185/// descendant) — it reads the type's private tuple field.
186///
187/// Each generated impl carries `#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]`, so a crate that
188/// invokes this macro must keep its own `#![cfg_attr(docsrs, feature(doc_cfg))]` at the crate root
189/// (as `reliar-core`, `-outbox` and `-inbox` already do) — without it, a docs.rs build (`--cfg
190/// docsrs`) fails on the unrecognized `doc(cfg(..))` attribute.
191///
192/// ```
193/// # #[cfg(feature = "serde")] {
194/// reliar_core::uuid_id!(
195/// /// probe
196/// MyId in reliar_core,
197/// no_mint
198/// );
199/// reliar_core::uuid_id_serde!(MyId);
200///
201/// let raw = reliar_core::uuid::Uuid::now_v7();
202/// let id = MyId::from_uuid(raw);
203///
204/// // Canonical hyphenated string, both ways.
205/// let json = serde_json::to_string(&id).unwrap();
206/// assert_eq!(json, format!("\"{raw}\""));
207/// assert_eq!(serde_json::from_str::<MyId>(&json).unwrap(), id);
208/// # }
209/// ```
210#[macro_export]
211macro_rules! uuid_id_serde {
212 ($name:ident) => {
213 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
214 impl ::serde::Serialize for $name {
215 fn serialize<S: ::serde::Serializer>(
216 &self,
217 s: S,
218 ) -> ::core::result::Result<S::Ok, S::Error> {
219 s.collect_str(&self.0)
220 }
221 }
222
223 #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
224 impl<'de> ::serde::Deserialize<'de> for $name {
225 fn deserialize<D: ::serde::Deserializer<'de>>(
226 d: D,
227 ) -> ::core::result::Result<Self, D::Error> {
228 let raw = <::std::string::String as ::serde::Deserialize>::deserialize(d)?;
229
230 $crate::uuid::Uuid::parse_str(&raw)
231 .map(Self)
232 .map_err(<D::Error as ::serde::de::Error>::custom)
233 }
234 }
235 };
236}
237
238uuid_id!(
239 /// Uniquely identifies one envelope end-to-end: enqueue, storage row, wire message, and,
240 /// if it fails permanently, the dead entry.
241 ///
242 /// ```
243 /// use reliar_core::MessageId;
244 ///
245 /// // Every id Reliar mints is a fresh UUIDv7 — monotonic-ish and time-ordered.
246 /// let id = MessageId::new();
247 /// assert_eq!(id, MessageId::from_uuid(id.as_uuid()));
248 /// ```
249 MessageId in reliar_core
250);
251#[cfg(feature = "serde")]
252uuid_id_serde!(MessageId);
253
254uuid_id!(
255 /// Groups every message in one business conversation. Always **derived**, never minted: an
256 /// un-correlated message roots its own conversation at its [`MessageId`] (see
257 /// [`crate::EnvelopeBuilder::build`]), and any other value is inherited from storage or the
258 /// wire via [`Self::from_uuid`]. There is deliberately no `new()`/`Default` — a random,
259 /// unrooted conversation id would silently drop a caller out of the conversation it supplied
260 /// (ADR 0038); a host that wants to start one names the UUID explicitly.
261 ///
262 /// ```
263 /// use reliar_core::ConversationId;
264 /// use uuid::Uuid;
265 ///
266 /// // Wrap an id read back from storage or the wire without inspecting its version.
267 /// let existing = Uuid::now_v7();
268 /// let conversation = ConversationId::from_uuid(existing);
269 /// assert_eq!(conversation.as_uuid(), existing);
270 /// assert!(!conversation.is_unset());
271 /// ```
272 ConversationId in reliar_core,
273 no_mint
274);
275#[cfg(feature = "serde")]
276uuid_id_serde!(ConversationId);
277
278uuid_id!(
279 /// Correlates an envelope back to the inbound request (HTTP call, RPC, CLI invocation) that
280 /// caused it, so an outbound message can be traced to its trigger. Always **host-supplied**:
281 /// there is deliberately no `new()`/`Default`, since a minted request id would claim an
282 /// inbound request exists when none does (ADR 0038). A host wraps the id it already has with
283 /// [`Self::from_uuid`].
284 ///
285 /// ```
286 /// use reliar_core::RequestId;
287 /// use uuid::Uuid;
288 ///
289 /// // Wrap the inbound request's own id — never minted.
290 /// let inbound = Uuid::now_v7();
291 /// let request_id = RequestId::from_uuid(inbound);
292 /// assert_eq!(request_id, RequestId::from_uuid(request_id.as_uuid()));
293 /// ```
294 RequestId in reliar_core,
295 no_mint
296);
297#[cfg(feature = "serde")]
298uuid_id_serde!(RequestId);
299
300impl ConversationId {
301 /// The reserved "not yet rooted" sentinel: the **nil** UUID. [`CorrelationMetadata`]'s
302 /// default uses it, and [`EnvelopeBuilder::build`] replaces it with the envelope's own id —
303 /// conversation rooting is decided by *this value*, not by which builder setter was called.
304 /// [`Self::from_uuid`] of any non-nil `UUIDv7` is therefore never `UNSET`. An application
305 /// SHALL NOT use the nil UUID as a real conversation id.
306 ///
307 /// [`CorrelationMetadata`]: crate::CorrelationMetadata
308 /// [`EnvelopeBuilder::build`]: crate::EnvelopeBuilder::build
309 ///
310 /// ```
311 /// use reliar_core::ConversationId;
312 /// use uuid::Uuid;
313 ///
314 /// assert!(ConversationId::UNSET.is_unset());
315 /// assert!(!ConversationId::from_uuid(Uuid::now_v7()).is_unset());
316 /// ```
317 pub const UNSET: Self = Self::from_uuid(Uuid::nil());
318
319 /// `true` when this id is [`Self::UNSET`].
320 ///
321 /// ```
322 /// use reliar_core::ConversationId;
323 /// use uuid::Uuid;
324 ///
325 /// assert!(ConversationId::UNSET.is_unset());
326 /// assert!(!ConversationId::from_uuid(Uuid::now_v7()).is_unset());
327 /// ```
328 #[must_use]
329 pub const fn is_unset(&self) -> bool {
330 self.0.is_nil()
331 }
332}
333
334/// Application/business workflow correlation id — distinct from [`ConversationId`] (Reliar's own
335/// conversation root) and a `causation_id` (the direct parent message). Capped at
336/// [`Self::MAX_LEN`] bytes: it lands in a `text` column read on every claim.
337///
338/// ```
339/// use reliar_core::CorrelationId;
340///
341/// let id = CorrelationId::parse("checkout-42")?;
342/// assert_eq!(id.as_str(), "checkout-42");
343/// # Ok::<(), reliar_core::IdError>(())
344/// ```
345#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
346pub struct CorrelationId(String);
347
348impl CorrelationId {
349 /// Maximum length in bytes.
350 pub const MAX_LEN: usize = 256;
351
352 /// Validates and wraps a correlation id. Returns `Err` for an empty string, one containing
353 /// a control character (including CR/LF — a header-injection surface), or one over
354 /// [`Self::MAX_LEN`] bytes.
355 ///
356 /// # Errors
357 ///
358 /// Returns [`IdError::Empty`], [`IdError::ControlCharacter`], or [`IdError::TooLong`].
359 ///
360 /// ```
361 /// use reliar_core::CorrelationId;
362 ///
363 /// let id = CorrelationId::parse("checkout-42")?;
364 /// assert_eq!(id.as_str(), "checkout-42");
365 /// assert!(CorrelationId::parse("").is_err());
366 /// # Ok::<(), reliar_core::IdError>(())
367 /// ```
368 pub fn parse(s: impl Into<String>) -> Result<Self, IdError> {
369 let s = s.into();
370
371 if s.is_empty() {
372 return Err(IdError::Empty);
373 }
374
375 if contains_control_char(&s) {
376 return Err(IdError::ControlCharacter);
377 }
378
379 if s.len() > Self::MAX_LEN {
380 return Err(IdError::TooLong {
381 len: s.len(),
382 max: Self::MAX_LEN,
383 });
384 }
385
386 Ok(Self(s))
387 }
388
389 /// Returns the correlation id as a string slice.
390 ///
391 /// ```
392 /// use reliar_core::CorrelationId;
393 ///
394 /// let id = CorrelationId::parse("checkout-42")?;
395 /// assert_eq!(id.as_str(), "checkout-42");
396 /// # Ok::<(), reliar_core::IdError>(())
397 /// ```
398 #[must_use]
399 pub fn as_str(&self) -> &str {
400 &self.0
401 }
402}
403
404impl fmt::Display for CorrelationId {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 f.write_str(&self.0)
407 }
408}
409
410// `CorrelationId` is a capped string, not a `uuid_id!` newtype, so its serde impl stays
411// hand-written here rather than through `uuid_id_serde!`.
412#[cfg(feature = "serde")]
413#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
414mod serde_impls {
415 use serde::{Deserialize, Serialize, de::Error as _};
416
417 use super::CorrelationId;
418
419 impl Serialize for CorrelationId {
420 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
421 s.collect_str(&self.0)
422 }
423 }
424 impl<'de> Deserialize<'de> for CorrelationId {
425 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
426 let raw = String::deserialize(d)?;
427
428 Self::parse(raw).map_err(D::Error::custom)
429 }
430 }
431}