reliar-core 0.4.1

Pure envelope/message model shared by every Reliar crate — no storage or transport dependency.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
//! Identity newtypes shared by every envelope (ADR 0011, ADR 0015, ADR 0045).

use core::fmt;

use uuid::Uuid;

/// Validation failures shared by every capped string identity newtype in `reliar-core`.
///
/// ```
/// use reliar_core::{CorrelationId, IdError};
///
/// let err = CorrelationId::parse("").unwrap_err();
/// assert_eq!(err, IdError::Empty);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum IdError {
    /// The value was empty.
    Empty,

    /// The value exceeded its type's maximum length.
    TooLong {
        /// The value's actual length in bytes.
        len: usize,
        /// The maximum allowed length in bytes.
        max: usize,
    },

    /// The value contained a control character (including CR/LF) — a header-injection
    /// surface once a mapper writes this value onto the wire.
    ControlCharacter,
}

impl fmt::Display for IdError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => f.write_str("value must not be empty"),
            Self::TooLong { len, max } => {
                write!(f, "value length {len} exceeds the maximum of {max}")
            }
            Self::ControlCharacter => f.write_str("value must not contain a control character"),
        }
    }
}

impl std::error::Error for IdError {}

/// Shared by every capped string identity newtype (and [`crate::Headers`]): `true` if `s`
/// contains a control character (including CR/LF), which would let a value smuggle extra
/// header/line-oriented content onto the wire once a transport mapper writes it verbatim.
///
/// `char::is_control` matches Unicode category `Cc` (`U+0000..=U+001F`, `U+007F`,
/// `U+0080..=U+009F`) — exactly the code points a line-oriented wire format (an HTTP-style
/// header, a CSV row) treats specially. It is deliberately not a wider "non-printable" or
/// "non-ASCII" check: rejecting e.g. combining marks or emoji would reject legitimate
/// human-readable data this type has no reason to forbid.
pub(crate) fn contains_control_char(s: &str) -> bool {
    s.chars().any(char::is_control)
}

/// Declares a UUID-backed identity newtype: `Clone + Copy + Debug + Eq + Hash + Ord`, `from_uuid`/
/// `as_uuid`, and a `Display` that renders the inner UUID verbatim. Every id Reliar mints is a
/// `UUIDv7` (ADR 0015); an application may supply any UUID and Reliar SHALL NOT inspect or reject
/// its version.
///
/// Three forms, in decreasing minting power (ADR 0038, ADR 0045):
///
/// - `uuid_id!($name in $krate)` — mints: a `new()` that generates a fresh `UUIDv7` plus a
///   `Default` built on it. For an id Reliar originates on demand (`MessageId`).
/// - `uuid_id!($name in $krate, no_default)` — mints (`new()`), but no `Default`. For an id a
///   caller mints deliberately at a specific moment, never as a stand-in default value (a row id
///   minted client-side, e.g. `InboxRecordId`).
/// - `uuid_id!($name in $krate, no_mint)` — no `new()`, no `Default`. For an id that is always
///   **derived** from another value or **host-supplied** (`ConversationId`, `RequestId`, a
///   database-assigned row id) — there is no constructor that hands back a value nobody asked for.
///
/// `$krate` is the path the declared type is re-exported from (`reliar_core`, `reliar_outbox`,
/// `reliar_inbox`, …) — it is required, not optional, so the generated methods' doctests `use`
/// the type from where a caller actually finds it, never from `reliar-core` regardless of which
/// crate invoked the macro.
///
/// The declared type is always `pub`; a private id newtype is not what this macro is for.
///
/// The macro's internal `@base`/`@mint` arms are reachable from outside once exported, but they
/// are **not** public API — only the three forms documented above are contract (ADR 0045 §4).
///
/// ```
/// reliar_core::uuid_id!(
///     /// A row id assigned by the database.
///     MyRowId in reliar_core,
///     no_mint
/// );
///
/// let raw = reliar_core::uuid::Uuid::now_v7();
/// assert_eq!(MyRowId::from_uuid(raw).as_uuid(), raw);
/// assert_eq!(MyRowId::from_uuid(raw).to_string(), raw.to_string());
/// ```
#[macro_export]
macro_rules! uuid_id {
    ($(#[$meta:meta])* $name:ident in $krate:ident) => {
        $crate::uuid_id!(@base $(#[$meta])* $name in $krate);
        $crate::uuid_id!(@mint $name in $krate);

        impl ::core::default::Default for $name {
            fn default() -> Self {
                Self::new()
            }
        }
    };
    ($(#[$meta:meta])* $name:ident in $krate:ident, no_default) => {
        $crate::uuid_id!(@base $(#[$meta])* $name in $krate);
        $crate::uuid_id!(@mint $name in $krate);
    };
    ($(#[$meta:meta])* $name:ident in $krate:ident, no_mint) => {
        $crate::uuid_id!(@base $(#[$meta])* $name in $krate);
    };

    (@base $(#[$meta:meta])* $name:ident in $krate:ident) => {
        $(#[$meta])*
        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
        pub struct $name($crate::uuid::Uuid);

        impl $name {
            /// Wraps an existing UUID without inspecting or rejecting its version.
            ///
            #[doc = concat!("```\nuse ", stringify!($krate), "::", stringify!($name), ";")]
            /// use reliar_core::uuid::Uuid;
            ///
            /// let raw = Uuid::now_v7();
            #[doc = concat!("assert_eq!(", stringify!($name), "::from_uuid(raw).as_uuid(), raw);")]
            /// ```
            #[must_use]
            pub const fn from_uuid(id: $crate::uuid::Uuid) -> Self {
                Self(id)
            }

            /// Returns the inner UUID, unchanged.
            ///
            #[doc = concat!("```\nuse ", stringify!($krate), "::", stringify!($name), ";")]
            /// use reliar_core::uuid::Uuid;
            ///
            /// let raw = Uuid::now_v7();
            #[doc = concat!("assert_eq!(", stringify!($name), "::from_uuid(raw).as_uuid(), raw);")]
            /// ```
            #[must_use]
            pub const fn as_uuid(&self) -> $crate::uuid::Uuid {
                self.0
            }
        }

        impl ::core::fmt::Display for $name {
            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
                ::core::fmt::Display::fmt(&self.0, f)
            }
        }
    };

    (@mint $name:ident in $krate:ident) => {
        #[allow(
            clippy::new_without_default,
            reason = "`Default` is opt-in: the bare `uuid_id!` form adds it, `no_default` does not"
        )]
        impl $name {
            /// Generates a fresh `UUIDv7` id.
            ///
            #[doc = concat!("```\nuse ", stringify!($krate), "::", stringify!($name), ";\n")]
            #[doc = concat!("assert!(!", stringify!($name), "::new().as_uuid().is_nil());")]
            /// ```
            #[must_use]
            pub fn new() -> Self {
                Self($crate::uuid::Uuid::now_v7())
            }
        }
    };
}

/// Implements `serde::Serialize`/`Deserialize` for a [`uuid_id!`]-declared type: the canonical
/// hyphenated UUID string, written with `collect_str` and read with `Uuid::parse_str` — never
/// through `uuid`'s own `serde` feature, so enabling a caller's `serde` feature never has to unify
/// `uuid`'s feature set.
///
/// The **caller** writes the `#[cfg(feature = "serde")]` on the invocation; the predicate cannot
/// live inside this macro, since it would resolve against `reliar-core`'s feature table rather
/// than the caller's (ADR 0045). Must be invoked in the declared type's own module (or a
/// descendant) — it reads the type's private tuple field.
///
/// Each generated impl carries `#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]`, so a crate that
/// invokes this macro must keep its own `#![cfg_attr(docsrs, feature(doc_cfg))]` at the crate root
/// (as `reliar-core`, `-outbox` and `-inbox` already do) — without it, a docs.rs build (`--cfg
/// docsrs`) fails on the unrecognized `doc(cfg(..))` attribute.
///
/// ```
/// # #[cfg(feature = "serde")] {
/// reliar_core::uuid_id!(
///     /// probe
///     MyId in reliar_core,
///     no_mint
/// );
/// reliar_core::uuid_id_serde!(MyId);
///
/// let raw = reliar_core::uuid::Uuid::now_v7();
/// let id = MyId::from_uuid(raw);
///
/// // Canonical hyphenated string, both ways.
/// let json = serde_json::to_string(&id).unwrap();
/// assert_eq!(json, format!("\"{raw}\""));
/// assert_eq!(serde_json::from_str::<MyId>(&json).unwrap(), id);
/// # }
/// ```
#[macro_export]
macro_rules! uuid_id_serde {
    ($name:ident) => {
        #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
        impl ::serde::Serialize for $name {
            fn serialize<S: ::serde::Serializer>(
                &self,
                s: S,
            ) -> ::core::result::Result<S::Ok, S::Error> {
                s.collect_str(&self.0)
            }
        }

        #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
        impl<'de> ::serde::Deserialize<'de> for $name {
            fn deserialize<D: ::serde::Deserializer<'de>>(
                d: D,
            ) -> ::core::result::Result<Self, D::Error> {
                let raw = <::std::string::String as ::serde::Deserialize>::deserialize(d)?;

                $crate::uuid::Uuid::parse_str(&raw)
                    .map(Self)
                    .map_err(<D::Error as ::serde::de::Error>::custom)
            }
        }
    };
}

uuid_id!(
    /// Uniquely identifies one envelope end-to-end: enqueue, storage row, wire message, and,
    /// if it fails permanently, the dead entry.
    ///
    /// ```
    /// use reliar_core::MessageId;
    ///
    /// // Every id Reliar mints is a fresh UUIDv7 — monotonic-ish and time-ordered.
    /// let id = MessageId::new();
    /// assert_eq!(id, MessageId::from_uuid(id.as_uuid()));
    /// ```
    MessageId in reliar_core
);
#[cfg(feature = "serde")]
uuid_id_serde!(MessageId);

uuid_id!(
    /// Groups every message in one business conversation. Always **derived**, never minted: an
    /// un-correlated message roots its own conversation at its [`MessageId`] (see
    /// [`crate::EnvelopeBuilder::build`]), and any other value is inherited from storage or the
    /// wire via [`Self::from_uuid`]. There is deliberately no `new()`/`Default` — a random,
    /// unrooted conversation id would silently drop a caller out of the conversation it supplied
    /// (ADR 0038); a host that wants to start one names the UUID explicitly.
    ///
    /// ```
    /// use reliar_core::ConversationId;
    /// use uuid::Uuid;
    ///
    /// // Wrap an id read back from storage or the wire without inspecting its version.
    /// let existing = Uuid::now_v7();
    /// let conversation = ConversationId::from_uuid(existing);
    /// assert_eq!(conversation.as_uuid(), existing);
    /// assert!(!conversation.is_unset());
    /// ```
    ConversationId in reliar_core,
    no_mint
);
#[cfg(feature = "serde")]
uuid_id_serde!(ConversationId);

uuid_id!(
    /// Correlates an envelope back to the inbound request (HTTP call, RPC, CLI invocation) that
    /// caused it, so an outbound message can be traced to its trigger. Always **host-supplied**:
    /// there is deliberately no `new()`/`Default`, since a minted request id would claim an
    /// inbound request exists when none does (ADR 0038). A host wraps the id it already has with
    /// [`Self::from_uuid`].
    ///
    /// ```
    /// use reliar_core::RequestId;
    /// use uuid::Uuid;
    ///
    /// // Wrap the inbound request's own id — never minted.
    /// let inbound = Uuid::now_v7();
    /// let request_id = RequestId::from_uuid(inbound);
    /// assert_eq!(request_id, RequestId::from_uuid(request_id.as_uuid()));
    /// ```
    RequestId in reliar_core,
    no_mint
);
#[cfg(feature = "serde")]
uuid_id_serde!(RequestId);

impl ConversationId {
    /// The reserved "not yet rooted" sentinel: the **nil** UUID. [`CorrelationMetadata`]'s
    /// default uses it, and [`EnvelopeBuilder::build`] replaces it with the envelope's own id —
    /// conversation rooting is decided by *this value*, not by which builder setter was called.
    /// [`Self::from_uuid`] of any non-nil `UUIDv7` is therefore never `UNSET`. An application
    /// SHALL NOT use the nil UUID as a real conversation id.
    ///
    /// [`CorrelationMetadata`]: crate::CorrelationMetadata
    /// [`EnvelopeBuilder::build`]: crate::EnvelopeBuilder::build
    ///
    /// ```
    /// use reliar_core::ConversationId;
    /// use uuid::Uuid;
    ///
    /// assert!(ConversationId::UNSET.is_unset());
    /// assert!(!ConversationId::from_uuid(Uuid::now_v7()).is_unset());
    /// ```
    pub const UNSET: Self = Self::from_uuid(Uuid::nil());

    /// `true` when this id is [`Self::UNSET`].
    ///
    /// ```
    /// use reliar_core::ConversationId;
    /// use uuid::Uuid;
    ///
    /// assert!(ConversationId::UNSET.is_unset());
    /// assert!(!ConversationId::from_uuid(Uuid::now_v7()).is_unset());
    /// ```
    #[must_use]
    pub const fn is_unset(&self) -> bool {
        self.0.is_nil()
    }
}

/// Application/business workflow correlation id — distinct from [`ConversationId`] (Reliar's own
/// conversation root) and a `causation_id` (the direct parent message). Capped at
/// [`Self::MAX_LEN`] bytes: it lands in a `text` column read on every claim.
///
/// ```
/// use reliar_core::CorrelationId;
///
/// let id = CorrelationId::parse("checkout-42")?;
/// assert_eq!(id.as_str(), "checkout-42");
/// # Ok::<(), reliar_core::IdError>(())
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CorrelationId(String);

impl CorrelationId {
    /// Maximum length in bytes.
    pub const MAX_LEN: usize = 256;

    /// Validates and wraps a correlation id. Returns `Err` for an empty string, one containing
    /// a control character (including CR/LF — a header-injection surface), or one over
    /// [`Self::MAX_LEN`] bytes.
    ///
    /// # Errors
    ///
    /// Returns [`IdError::Empty`], [`IdError::ControlCharacter`], or [`IdError::TooLong`].
    ///
    /// ```
    /// use reliar_core::CorrelationId;
    ///
    /// let id = CorrelationId::parse("checkout-42")?;
    /// assert_eq!(id.as_str(), "checkout-42");
    /// assert!(CorrelationId::parse("").is_err());
    /// # Ok::<(), reliar_core::IdError>(())
    /// ```
    pub fn parse(s: impl Into<String>) -> Result<Self, IdError> {
        let s = s.into();

        if s.is_empty() {
            return Err(IdError::Empty);
        }

        if contains_control_char(&s) {
            return Err(IdError::ControlCharacter);
        }

        if s.len() > Self::MAX_LEN {
            return Err(IdError::TooLong {
                len: s.len(),
                max: Self::MAX_LEN,
            });
        }

        Ok(Self(s))
    }

    /// Returns the correlation id as a string slice.
    ///
    /// ```
    /// use reliar_core::CorrelationId;
    ///
    /// let id = CorrelationId::parse("checkout-42")?;
    /// assert_eq!(id.as_str(), "checkout-42");
    /// # Ok::<(), reliar_core::IdError>(())
    /// ```
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for CorrelationId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

// `CorrelationId` is a capped string, not a `uuid_id!` newtype, so its serde impl stays
// hand-written here rather than through `uuid_id_serde!`.
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
mod serde_impls {
    use serde::{Deserialize, Serialize, de::Error as _};

    use super::CorrelationId;

    impl Serialize for CorrelationId {
        fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
            s.collect_str(&self.0)
        }
    }
    impl<'de> Deserialize<'de> for CorrelationId {
        fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
            let raw = String::deserialize(d)?;

            Self::parse(raw).map_err(D::Error::custom)
        }
    }
}