reliar_core/ids.rs
1//! Identity newtypes shared by every envelope (ADR 0011, ADR 0015).
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 one Reliar generates is
63/// `UUIDv7` (ADR 0015); applications may supply any UUID and Reliar SHALL NOT inspect or reject
64/// its version.
65///
66/// The bare form (`uuid_id!($name)`) additionally mints: a `new()` that generates a fresh
67/// `UUIDv7` and a `Default` built on it. That half is for ids Reliar itself originates
68/// (`MessageId`). An id that is always **derived** from another value (`ConversationId`, rooted
69/// at the envelope's message id) or always **host-supplied** (`RequestId`, read off the inbound
70/// request) opts out with `uuid_id!($name, no_mint)` — there is no constructor that hands back a
71/// value nobody asked for (ADR 0038).
72macro_rules! uuid_id {
73 ($(#[$meta:meta])* $name:ident) => {
74 uuid_id!(@base $(#[$meta])* $name);
75 uuid_id!(@mint $name);
76 };
77 ($(#[$meta:meta])* $name:ident, no_mint) => {
78 uuid_id!(@base $(#[$meta])* $name);
79 };
80 (@base $(#[$meta:meta])* $name:ident) => {
81 $(#[$meta])*
82 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
83 pub struct $name(Uuid);
84
85 impl $name {
86 /// Wraps an existing UUID without inspecting or rejecting its version.
87 ///
88 /// ```
89 /// // Illustrative for every id type this macro declares.
90 /// use reliar_core::MessageId;
91 /// use uuid::Uuid;
92 ///
93 /// let raw = Uuid::now_v7();
94 /// assert_eq!(MessageId::from_uuid(raw).as_uuid(), raw);
95 /// ```
96 #[must_use]
97 pub const fn from_uuid(id: Uuid) -> Self {
98 Self(id)
99 }
100
101 /// Returns the inner UUID.
102 ///
103 /// ```
104 /// // Illustrative for every id type this macro declares.
105 /// use reliar_core::MessageId;
106 /// use uuid::Uuid;
107 ///
108 /// let raw = Uuid::now_v7();
109 /// assert_eq!(MessageId::from_uuid(raw).as_uuid(), raw);
110 /// ```
111 #[must_use]
112 pub const fn as_uuid(&self) -> Uuid {
113 self.0
114 }
115 }
116
117 impl fmt::Display for $name {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 fmt::Display::fmt(&self.0, f)
120 }
121 }
122 };
123 (@mint $name:ident) => {
124 impl $name {
125 /// Generates a fresh `UUIDv7` id.
126 ///
127 /// ```
128 /// // Illustrative for every id type this macro mints.
129 /// use reliar_core::MessageId;
130 ///
131 /// let id = MessageId::new();
132 /// assert!(!id.as_uuid().is_nil());
133 /// ```
134 #[must_use]
135 pub fn new() -> Self {
136 Self(Uuid::now_v7())
137 }
138 }
139
140 impl Default for $name {
141 fn default() -> Self {
142 Self::new()
143 }
144 }
145 };
146}
147
148uuid_id!(
149 /// Uniquely identifies one envelope end-to-end: enqueue, storage row, wire message, and,
150 /// if it fails permanently, the dead entry.
151 ///
152 /// ```
153 /// use reliar_core::MessageId;
154 ///
155 /// // Every id Reliar mints is a fresh UUIDv7 — monotonic-ish and time-ordered.
156 /// let id = MessageId::new();
157 /// assert_eq!(id, MessageId::from_uuid(id.as_uuid()));
158 /// ```
159 MessageId
160);
161uuid_id!(
162 /// Groups every message in one business conversation. Always **derived**, never minted: an
163 /// un-correlated message roots its own conversation at its [`MessageId`] (see
164 /// [`crate::EnvelopeBuilder::build`]), and any other value is inherited from storage or the
165 /// wire via [`Self::from_uuid`]. There is deliberately no `new()`/`Default` — a random,
166 /// unrooted conversation id would silently drop a caller out of the conversation it supplied
167 /// (ADR 0038); a host that wants to start one names the UUID explicitly.
168 ///
169 /// ```
170 /// use reliar_core::ConversationId;
171 /// use uuid::Uuid;
172 ///
173 /// // Wrap an id read back from storage or the wire without inspecting its version.
174 /// let existing = Uuid::now_v7();
175 /// let conversation = ConversationId::from_uuid(existing);
176 /// assert_eq!(conversation.as_uuid(), existing);
177 /// assert!(!conversation.is_unset());
178 /// ```
179 ConversationId,
180 no_mint
181);
182uuid_id!(
183 /// Correlates an envelope back to the inbound request (HTTP call, RPC, CLI invocation) that
184 /// caused it, so an outbound message can be traced to its trigger. Always **host-supplied**:
185 /// there is deliberately no `new()`/`Default`, since a minted request id would claim an
186 /// inbound request exists when none does (ADR 0038). A host wraps the id it already has with
187 /// [`Self::from_uuid`].
188 ///
189 /// ```
190 /// use reliar_core::RequestId;
191 /// use uuid::Uuid;
192 ///
193 /// // Wrap the inbound request's own id — never minted.
194 /// let inbound = Uuid::now_v7();
195 /// let request_id = RequestId::from_uuid(inbound);
196 /// assert_eq!(request_id, RequestId::from_uuid(request_id.as_uuid()));
197 /// ```
198 RequestId,
199 no_mint
200);
201
202impl ConversationId {
203 /// The reserved "not yet rooted" sentinel: the **nil** UUID. [`CorrelationMetadata`]'s
204 /// default uses it, and [`EnvelopeBuilder::build`] replaces it with the envelope's own id —
205 /// conversation rooting is decided by *this value*, not by which builder setter was called.
206 /// [`Self::from_uuid`] of any non-nil `UUIDv7` is therefore never `UNSET`. An application
207 /// SHALL NOT use the nil UUID as a real conversation id.
208 ///
209 /// [`CorrelationMetadata`]: crate::CorrelationMetadata
210 /// [`EnvelopeBuilder::build`]: crate::EnvelopeBuilder::build
211 ///
212 /// ```
213 /// use reliar_core::ConversationId;
214 /// use uuid::Uuid;
215 ///
216 /// assert!(ConversationId::UNSET.is_unset());
217 /// assert!(!ConversationId::from_uuid(Uuid::now_v7()).is_unset());
218 /// ```
219 pub const UNSET: Self = Self::from_uuid(Uuid::nil());
220
221 /// `true` when this id is [`Self::UNSET`].
222 ///
223 /// ```
224 /// use reliar_core::ConversationId;
225 /// use uuid::Uuid;
226 ///
227 /// assert!(ConversationId::UNSET.is_unset());
228 /// assert!(!ConversationId::from_uuid(Uuid::now_v7()).is_unset());
229 /// ```
230 #[must_use]
231 pub const fn is_unset(&self) -> bool {
232 self.0.is_nil()
233 }
234}
235
236/// Application/business workflow correlation id — distinct from [`ConversationId`] (Reliar's own
237/// conversation root) and a `causation_id` (the direct parent message). Capped at
238/// [`Self::MAX_LEN`] bytes: it lands in a `text` column read on every claim.
239///
240/// ```
241/// use reliar_core::CorrelationId;
242///
243/// let id = CorrelationId::parse("checkout-42")?;
244/// assert_eq!(id.as_str(), "checkout-42");
245/// # Ok::<(), reliar_core::IdError>(())
246/// ```
247#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
248pub struct CorrelationId(String);
249
250impl CorrelationId {
251 /// Maximum length in bytes.
252 pub const MAX_LEN: usize = 256;
253
254 /// Validates and wraps a correlation id. Returns `Err` for an empty string, one containing
255 /// a control character (including CR/LF — a header-injection surface), or one over
256 /// [`Self::MAX_LEN`] bytes.
257 ///
258 /// # Errors
259 ///
260 /// Returns [`IdError::Empty`], [`IdError::ControlCharacter`], or [`IdError::TooLong`].
261 ///
262 /// ```
263 /// use reliar_core::CorrelationId;
264 ///
265 /// let id = CorrelationId::parse("checkout-42")?;
266 /// assert_eq!(id.as_str(), "checkout-42");
267 /// assert!(CorrelationId::parse("").is_err());
268 /// # Ok::<(), reliar_core::IdError>(())
269 /// ```
270 pub fn parse(s: impl Into<String>) -> Result<Self, IdError> {
271 let s = s.into();
272
273 if s.is_empty() {
274 return Err(IdError::Empty);
275 }
276
277 if contains_control_char(&s) {
278 return Err(IdError::ControlCharacter);
279 }
280
281 if s.len() > Self::MAX_LEN {
282 return Err(IdError::TooLong {
283 len: s.len(),
284 max: Self::MAX_LEN,
285 });
286 }
287
288 Ok(Self(s))
289 }
290
291 /// Returns the correlation id as a string slice.
292 ///
293 /// ```
294 /// use reliar_core::CorrelationId;
295 ///
296 /// let id = CorrelationId::parse("checkout-42")?;
297 /// assert_eq!(id.as_str(), "checkout-42");
298 /// # Ok::<(), reliar_core::IdError>(())
299 /// ```
300 #[must_use]
301 pub fn as_str(&self) -> &str {
302 &self.0
303 }
304}
305
306impl fmt::Display for CorrelationId {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 f.write_str(&self.0)
309 }
310}
311
312#[cfg(feature = "serde")]
313#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
314mod serde_impls {
315 use serde::{Deserialize, Serialize, de::Error as _};
316 use uuid::Uuid;
317
318 use super::{ConversationId, CorrelationId, MessageId, RequestId};
319
320 // Serialized as the canonical hyphenated UUID string rather than via `uuid`'s own `serde`
321 // feature, so enabling `reliar-core/serde` never has to unify `uuid`'s feature set.
322 macro_rules! uuid_id_serde {
323 ($name:ident) => {
324 impl Serialize for $name {
325 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
326 s.collect_str(&self.0)
327 }
328 }
329 impl<'de> Deserialize<'de> for $name {
330 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
331 let raw = String::deserialize(d)?;
332 Uuid::parse_str(&raw).map(Self).map_err(D::Error::custom)
333 }
334 }
335 };
336 }
337 uuid_id_serde!(MessageId);
338 uuid_id_serde!(ConversationId);
339 uuid_id_serde!(RequestId);
340
341 impl Serialize for CorrelationId {
342 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
343 s.collect_str(&self.0)
344 }
345 }
346 impl<'de> Deserialize<'de> for CorrelationId {
347 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
348 let raw = String::deserialize(d)?;
349
350 Self::parse(raw).map_err(D::Error::custom)
351 }
352 }
353}