Skip to main content

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}