Skip to main content

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}