1#![forbid(unsafe_code)]
9
10use super::error::DomainError;
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12use std::fmt;
13use uuid::{Uuid, Variant, Version};
14
15#[repr(transparent)]
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18pub struct CorrelationId(Uuid);
19
20#[repr(transparent)]
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub struct BatchRunId(Uuid);
24
25fn reject_sentinel(u: Uuid) -> Result<Uuid, DomainError> {
26 if u.is_nil() {
27 return Err(DomainError::new("uuid", "nil UUID is a forbidden sentinel"));
28 }
29 if u.as_u128() == u128::MAX {
30 return Err(DomainError::new("uuid", "max UUID is a forbidden sentinel"));
31 }
32 match u.get_variant() {
33 Variant::RFC4122 => {}
34 _ => {
35 return Err(DomainError::new(
36 "uuid",
37 "UUID variant must be RFC 4122/9562",
38 ));
39 }
40 }
41 Ok(u)
42}
43
44impl CorrelationId {
45 #[must_use]
47 pub fn new() -> Self {
48 Self(Uuid::new_v4())
49 }
50
51 pub fn try_new(raw: impl AsRef<str>) -> Result<Self, DomainError> {
53 let u = Uuid::parse_str(raw.as_ref().trim())
54 .map_err(|e| DomainError::new("correlation_id", e.to_string()))?;
55 let u = reject_sentinel(u)?;
56 if let Some(v) = u.get_version() {
57 if v != Version::Random {
58 return Err(DomainError::new(
59 "correlation_id",
60 format!("expected UUID v4, got {v:?}"),
61 ));
62 }
63 }
64 Ok(Self(u))
65 }
66
67 #[must_use]
69 pub const fn as_uuid(&self) -> &Uuid {
70 &self.0
71 }
72
73 #[must_use]
75 pub fn to_string_canonical(&self) -> String {
76 self.0.to_string()
77 }
78}
79
80impl Default for CorrelationId {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl fmt::Display for CorrelationId {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 write!(f, "{}", self.0)
89 }
90}
91
92impl Serialize for CorrelationId {
93 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
94 s.serialize_str(&self.0.to_string())
95 }
96}
97
98impl<'de> Deserialize<'de> for CorrelationId {
99 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
100 let s = String::deserialize(d)?;
101 Self::try_new(s).map_err(serde::de::Error::custom)
102 }
103}
104
105impl BatchRunId {
106 #[must_use]
108 pub fn new() -> Self {
109 Self(Uuid::now_v7())
110 }
111
112 pub fn try_new(raw: impl AsRef<str>) -> Result<Self, DomainError> {
114 let u = Uuid::parse_str(raw.as_ref().trim())
115 .map_err(|e| DomainError::new("batch_run_id", e.to_string()))?;
116 let u = reject_sentinel(u)?;
117 if let Some(v) = u.get_version() {
118 if v != Version::SortRand {
119 return Err(DomainError::new(
120 "batch_run_id",
121 format!("expected UUID v7, got {v:?}"),
122 ));
123 }
124 }
125 Ok(Self(u))
126 }
127
128 #[must_use]
130 pub const fn as_uuid(&self) -> &Uuid {
131 &self.0
132 }
133
134 #[must_use]
136 pub fn to_string_canonical(&self) -> String {
137 self.0.to_string()
138 }
139}
140
141impl Default for BatchRunId {
142 fn default() -> Self {
143 Self::new()
144 }
145}
146
147impl fmt::Display for BatchRunId {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 write!(f, "{}", self.0)
150 }
151}
152
153impl Serialize for BatchRunId {
154 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
155 s.serialize_str(&self.0.to_string())
156 }
157}
158
159impl<'de> Deserialize<'de> for BatchRunId {
160 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
161 let s = String::deserialize(d)?;
162 Self::try_new(s).map_err(serde::de::Error::custom)
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn correlation_v4_roundtrip() {
172 let id = CorrelationId::new();
173 let s = id.to_string_canonical();
174 let back = CorrelationId::try_new(&s).unwrap();
175 assert_eq!(id, back);
176 assert_eq!(id.as_uuid().get_version(), Some(Version::Random));
177 }
178
179 #[test]
180 fn batch_v7_roundtrip() {
181 let a = BatchRunId::new();
182 let b = BatchRunId::new();
183 let s = a.to_string_canonical();
185 let back = BatchRunId::try_new(&s).unwrap();
186 assert_eq!(a, back);
187 assert_eq!(a.as_uuid().get_version(), Some(Version::SortRand));
188 let _ = b;
190 }
191
192 #[test]
193 fn rejects_nil() {
194 assert!(CorrelationId::try_new("00000000-0000-0000-0000-000000000000").is_err());
195 assert!(BatchRunId::try_new("00000000-0000-0000-0000-000000000000").is_err());
196 }
197
198 #[test]
199 fn distinct_types_not_interchangeable() {
200 let c = CorrelationId::new();
201 assert!(BatchRunId::try_new(c.to_string_canonical()).is_err());
203 }
204}