reliar_core/ids.rs
1//! Identity newtypes shared by every envelope (SRS §11, 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#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum IdError {
11 /// The value was empty.
12 Empty,
13 /// The value exceeded its type's maximum length.
14 TooLong {
15 /// The value's actual length in bytes.
16 len: usize,
17 /// The maximum allowed length in bytes.
18 max: usize,
19 },
20 /// The value contained a control character (including CR/LF) — a header-injection
21 /// surface once a mapper writes this value onto the wire.
22 ControlCharacter,
23}
24
25impl fmt::Display for IdError {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Self::Empty => f.write_str("value must not be empty"),
29 Self::TooLong { len, max } => {
30 write!(f, "value length {len} exceeds the maximum of {max}")
31 }
32 Self::ControlCharacter => f.write_str("value must not contain a control character"),
33 }
34 }
35}
36
37impl std::error::Error for IdError {}
38
39/// Shared by every capped string identity newtype (and [`crate::Headers`]): `true` if `s`
40/// contains a control character (including CR/LF), which would let a value smuggle extra
41/// header/line-oriented content onto the wire once a transport mapper writes it verbatim.
42///
43/// `char::is_control` matches Unicode category `Cc` (`U+0000..=U+001F`, `U+007F`,
44/// `U+0080..=U+009F`) — exactly the code points a line-oriented wire format (an HTTP-style
45/// header, a CSV row) treats specially. It is deliberately not a wider "non-printable" or
46/// "non-ASCII" check: rejecting e.g. combining marks or emoji would reject legitimate
47/// human-readable data this type has no reason to forbid.
48pub(crate) fn contains_control_char(s: &str) -> bool {
49 s.chars().any(char::is_control)
50}
51
52/// Declares a UUID-backed identity newtype: `Clone + Copy + Debug + Eq + Hash + Ord`, a
53/// `new()` that mints a fresh `UUIDv7`, and a `Display` that renders the inner UUID verbatim.
54/// Every one Reliar generates is `UUIDv7` (ADR 0015); applications may supply any UUID and
55/// Reliar SHALL NOT inspect or reject its version.
56macro_rules! uuid_id {
57 ($(#[$meta:meta])* $name:ident) => {
58 $(#[$meta])*
59 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
60 pub struct $name(Uuid);
61
62 impl $name {
63 /// Generates a fresh `UUIDv7` id.
64 #[must_use]
65 pub fn new() -> Self {
66 Self(Uuid::now_v7())
67 }
68
69 /// Wraps an existing UUID without inspecting or rejecting its version.
70 #[must_use]
71 pub const fn from_uuid(id: Uuid) -> Self {
72 Self(id)
73 }
74
75 /// Returns the inner UUID.
76 #[must_use]
77 pub const fn as_uuid(&self) -> Uuid {
78 self.0
79 }
80 }
81
82 impl Default for $name {
83 fn default() -> Self {
84 Self::new()
85 }
86 }
87
88 impl fmt::Display for $name {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 fmt::Display::fmt(&self.0, f)
91 }
92 }
93 };
94}
95
96uuid_id!(
97 /// Uniquely identifies one envelope end-to-end: enqueue, storage row, wire message, and,
98 /// if it fails permanently, the dead entry.
99 MessageId
100);
101uuid_id!(
102 /// Groups every message in one business conversation. Defaults to the id of the message
103 /// that starts it (see [`crate::EnvelopeBuilder::build`]), so an un-correlated message is
104 /// the root of its own conversation.
105 ConversationId
106);
107uuid_id!(
108 /// Correlates an envelope back to the inbound request (HTTP call, RPC, CLI invocation) that
109 /// caused it, so an outbound message can be traced to its trigger.
110 RequestId
111);
112
113impl ConversationId {
114 /// The reserved "not yet rooted" sentinel: the **nil** UUID. [`CorrelationMetadata`]'s
115 /// default uses it, and [`EnvelopeBuilder::build`] replaces it with the envelope's own id —
116 /// conversation rooting is decided by *this value*, not by which builder setter was called.
117 /// [`Self::new`]/[`Self::default`] mint a fresh `UUIDv7` and are therefore never `UNSET`. An
118 /// application SHALL NOT use the nil UUID as a real conversation id.
119 ///
120 /// [`CorrelationMetadata`]: crate::CorrelationMetadata
121 /// [`EnvelopeBuilder::build`]: crate::EnvelopeBuilder::build
122 pub const UNSET: Self = Self::from_uuid(Uuid::nil());
123
124 /// `true` when this id is [`Self::UNSET`].
125 #[must_use]
126 pub const fn is_unset(&self) -> bool {
127 self.0.is_nil()
128 }
129}
130
131/// Application/business workflow correlation id — distinct from [`ConversationId`] (Reliar's own
132/// conversation root) and a `causation_id` (the direct parent message). Capped at
133/// [`Self::MAX_LEN`] bytes: it lands in a `text` column read on every claim (§11).
134#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
135pub struct CorrelationId(String);
136
137impl CorrelationId {
138 /// Maximum length in bytes.
139 pub const MAX_LEN: usize = 256;
140
141 /// Validates and wraps a correlation id. Returns `Err` for an empty string, one containing
142 /// a control character (including CR/LF — a header-injection surface), or one over
143 /// [`Self::MAX_LEN`] bytes.
144 ///
145 /// # Errors
146 ///
147 /// Returns [`IdError::Empty`], [`IdError::ControlCharacter`], or [`IdError::TooLong`].
148 pub fn parse(s: impl Into<String>) -> Result<Self, IdError> {
149 let s = s.into();
150 if s.is_empty() {
151 return Err(IdError::Empty);
152 }
153 if contains_control_char(&s) {
154 return Err(IdError::ControlCharacter);
155 }
156 if s.len() > Self::MAX_LEN {
157 return Err(IdError::TooLong {
158 len: s.len(),
159 max: Self::MAX_LEN,
160 });
161 }
162 Ok(Self(s))
163 }
164
165 /// Returns the correlation id as a string slice.
166 #[must_use]
167 pub fn as_str(&self) -> &str {
168 &self.0
169 }
170}
171
172impl fmt::Display for CorrelationId {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 f.write_str(&self.0)
175 }
176}
177
178#[cfg(feature = "serde")]
179mod serde_impls {
180 use serde::{Deserialize, Serialize, de::Error as _};
181 use uuid::Uuid;
182
183 use super::{ConversationId, CorrelationId, MessageId, RequestId};
184
185 // Serialized as the canonical hyphenated UUID string rather than via `uuid`'s own `serde`
186 // feature, so enabling `reliar-core/serde` never has to unify `uuid`'s feature set.
187 macro_rules! uuid_id_serde {
188 ($name:ident) => {
189 impl Serialize for $name {
190 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
191 s.collect_str(&self.0)
192 }
193 }
194 impl<'de> Deserialize<'de> for $name {
195 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
196 let raw = String::deserialize(d)?;
197 Uuid::parse_str(&raw).map(Self).map_err(D::Error::custom)
198 }
199 }
200 };
201 }
202 uuid_id_serde!(MessageId);
203 uuid_id_serde!(ConversationId);
204 uuid_id_serde!(RequestId);
205
206 impl Serialize for CorrelationId {
207 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
208 s.collect_str(&self.0)
209 }
210 }
211 impl<'de> Deserialize<'de> for CorrelationId {
212 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
213 let raw = String::deserialize(d)?;
214 Self::parse(raw).map_err(D::Error::custom)
215 }
216 }
217}