1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8pub const MAX_NAME_LEN: usize = 200;
10
11pub const MAX_KEY_LEN: usize = 512;
13
14const IDEMPOTENCY_NAMESPACE: Uuid = Uuid::from_u128(0x5b2f_9c1e_7a4d_4e0b_9f3a_6c8d_1e2f_4a70);
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
25#[serde(transparent)]
26pub struct EffectId(Uuid);
27
28impl EffectId {
29 pub fn new() -> Self {
31 Self(Uuid::now_v7())
32 }
33
34 pub const fn from_uuid(uuid: Uuid) -> Self {
36 Self(uuid)
37 }
38
39 pub const fn as_uuid(&self) -> &Uuid {
41 &self.0
42 }
43}
44
45impl Default for EffectId {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl fmt::Display for EffectId {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 self.0.fmt(f)
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
59pub enum IdentityError {
60 #[error("{0} must not be empty")]
62 Empty(&'static str),
63 #[error("{what} is {len} bytes, maximum is {max}")]
65 TooLong {
66 what: &'static str,
68 len: usize,
70 max: usize,
72 },
73 #[error("{0} must not contain control characters")]
75 ControlCharacter(&'static str),
76}
77
78fn validate(what: &'static str, value: &str, max: usize) -> Result<(), IdentityError> {
79 if value.is_empty() {
80 return Err(IdentityError::Empty(what));
81 }
82 if value.len() > max {
83 return Err(IdentityError::TooLong {
84 what,
85 len: value.len(),
86 max,
87 });
88 }
89 if value.chars().any(char::is_control) {
90 return Err(IdentityError::ControlCharacter(what));
91 }
92 Ok(())
93}
94
95#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
97#[serde(try_from = "String", into = "String")]
98pub struct EffectName(String);
99
100impl EffectName {
101 pub fn new(name: impl Into<String>) -> Result<Self, IdentityError> {
108 let name = name.into();
109 validate("effect name", &name, MAX_NAME_LEN)?;
110 Ok(Self(name))
111 }
112
113 pub fn as_str(&self) -> &str {
115 &self.0
116 }
117}
118
119impl TryFrom<String> for EffectName {
120 type Error = IdentityError;
121
122 fn try_from(value: String) -> Result<Self, Self::Error> {
123 Self::new(value)
124 }
125}
126
127impl From<EffectName> for String {
128 fn from(value: EffectName) -> Self {
129 value.0
130 }
131}
132
133impl fmt::Display for EffectName {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.write_str(&self.0)
136 }
137}
138
139#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
142#[serde(try_from = "String", into = "String")]
143pub struct LogicalKey(String);
144
145impl LogicalKey {
146 pub fn new(key: impl Into<String>) -> Result<Self, IdentityError> {
153 let key = key.into();
154 validate("logical key", &key, MAX_KEY_LEN)?;
155 Ok(Self(key))
156 }
157
158 pub fn as_str(&self) -> &str {
160 &self.0
161 }
162}
163
164impl TryFrom<String> for LogicalKey {
165 type Error = IdentityError;
166
167 fn try_from(value: String) -> Result<Self, Self::Error> {
168 Self::new(value)
169 }
170}
171
172impl From<LogicalKey> for String {
173 fn from(value: LogicalKey) -> Self {
174 value.0
175 }
176}
177
178impl fmt::Display for LogicalKey {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 f.write_str(&self.0)
181 }
182}
183
184#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
188pub struct EffectKey {
189 pub name: EffectName,
191 pub key: LogicalKey,
193}
194
195impl EffectKey {
196 pub const fn new(name: EffectName, key: LogicalKey) -> Self {
198 Self { name, key }
199 }
200
201 pub fn idempotency_key(&self) -> IdempotencyKey {
207 let material = format!("{}:{}:{}", self.name.0.len(), self.name.0, self.key.0);
209 IdempotencyKey(Uuid::new_v5(&IDEMPOTENCY_NAMESPACE, material.as_bytes()))
210 }
211
212 pub fn compensation_idempotency_key(&self) -> IdempotencyKey {
217 let material = format!(
218 "compensate:{}:{}:{}",
219 self.name.0.len(),
220 self.name.0,
221 self.key.0
222 );
223 IdempotencyKey(Uuid::new_v5(&IDEMPOTENCY_NAMESPACE, material.as_bytes()))
224 }
225}
226
227impl fmt::Display for EffectKey {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 write!(f, "{}:{}", self.name, self.key)
230 }
231}
232
233#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
236#[serde(transparent)]
237pub struct IdempotencyKey(Uuid);
238
239impl IdempotencyKey {
240 pub const fn as_uuid(&self) -> &Uuid {
242 &self.0
243 }
244}
245
246impl fmt::Display for IdempotencyKey {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 self.0.fmt(f)
249 }
250}
251
252#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
254#[serde(transparent)]
255pub struct WorkerId(String);
256
257impl WorkerId {
258 pub fn new(id: impl Into<String>) -> Self {
260 Self(id.into())
261 }
262
263 pub fn random() -> Self {
265 Self(Uuid::now_v7().to_string())
266 }
267
268 pub fn as_str(&self) -> &str {
270 &self.0
271 }
272}
273
274impl fmt::Display for WorkerId {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 f.write_str(&self.0)
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 fn key(name: &str, key: &str) -> EffectKey {
285 EffectKey::new(
286 EffectName::new(name).unwrap(),
287 LogicalKey::new(key).unwrap(),
288 )
289 }
290
291 #[test]
292 fn idempotency_key_is_stable() {
293 assert_eq!(
295 key("payment.charge", "order_5824")
296 .idempotency_key()
297 .to_string(),
298 "ab211c89-566b-5012-bd33-3a7f81351a08"
299 );
300 }
301
302 #[test]
303 fn compensation_key_is_stable_and_distinct() {
304 let effect = key("payment.charge", "order_5824");
305 assert_eq!(
307 effect.compensation_idempotency_key().to_string(),
308 "46877ef3-8997-5e94-a1ac-4be0bd2782b4"
309 );
310 assert_ne!(
311 effect.compensation_idempotency_key(),
312 effect.idempotency_key()
313 );
314 }
315
316 #[test]
317 fn idempotency_key_separates_ambiguous_splits() {
318 assert_ne!(
319 key("a:b", "c").idempotency_key(),
320 key("a", "b:c").idempotency_key()
321 );
322 }
323
324 #[test]
325 fn rejects_invalid_identity() {
326 assert_eq!(
327 EffectName::new(""),
328 Err(IdentityError::Empty("effect name"))
329 );
330 assert!(matches!(
331 LogicalKey::new("x".repeat(MAX_KEY_LEN + 1)),
332 Err(IdentityError::TooLong { .. })
333 ));
334 assert_eq!(
335 LogicalKey::new("a\nb"),
336 Err(IdentityError::ControlCharacter("logical key"))
337 );
338 }
339
340 #[test]
341 fn deserialization_validates() {
342 assert!(serde_json::from_str::<EffectName>("\"\"").is_err());
343 let name: EffectName = serde_json::from_str("\"payment.charge\"").unwrap();
344 assert_eq!(name.as_str(), "payment.charge");
345 }
346
347 #[test]
348 fn effect_ids_are_time_ordered() {
349 let a = EffectId::new();
350 let b = EffectId::new();
351 assert!(a < b);
352 }
353}