mako_engine/ids.rs
1//! Typed identifier newtypes for all engine-layer concepts.
2//!
3//! All identifiers are UUID v4 wrappers to guarantee global uniqueness without
4//! coordination. They are distinct types so the compiler rejects mixing them up
5//! at the call site.
6
7use std::fmt;
8
9use uuid::Uuid;
10
11macro_rules! define_id {
12 ($name:ident, $doc:literal) => {
13 #[doc = $doc]
14 #[derive(
15 Debug,
16 Clone,
17 Copy,
18 PartialEq,
19 Eq,
20 PartialOrd,
21 Ord,
22 Hash,
23 serde::Serialize,
24 serde::Deserialize,
25 )]
26 pub struct $name(Uuid);
27
28 impl $name {
29 /// Generate a fresh random identifier.
30 #[must_use]
31 pub fn new() -> Self {
32 Self(Uuid::new_v4())
33 }
34
35 /// Wrap an existing UUID.
36 #[must_use]
37 pub fn from_uuid(u: Uuid) -> Self {
38 Self(u)
39 }
40
41 /// Return the underlying UUID.
42 #[must_use]
43 pub fn as_uuid(self) -> Uuid {
44 self.0
45 }
46 }
47
48 impl Default for $name {
49 fn default() -> Self {
50 Self::new()
51 }
52 }
53
54 impl fmt::Display for $name {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 fmt::Display::fmt(&self.0, f)
57 }
58 }
59
60 impl From<Uuid> for $name {
61 fn from(u: Uuid) -> Self {
62 Self(u)
63 }
64 }
65 };
66}
67
68define_id!(
69 EventId,
70 "Globally unique identifier for a single persisted event."
71);
72define_id!(
73 CorrelationId,
74 "Groups all events and commands that originate from the same root operation."
75);
76
77impl CorrelationId {
78 /// Derive a deterministic `CorrelationId` from an EDIFACT interchange
79 /// reference string using UUID v5 (SHA-1 hash in a fixed namespace).
80 ///
81 /// EDIFACT interchanges carry a reference number in `UNB+…+…+<ref>`.
82 /// Messages retransmitted with the same reference must produce the same
83 /// `CorrelationId` so duplicate EDIFACT messages are correlated correctly
84 /// in traces and never generate spurious new process roots.
85 ///
86 /// The namespace UUID is fixed and private to this crate:
87 /// `a3c7e1f0-5b2d-4e80-9f6a-1b3c5d7e9a0b` (registered once).
88 ///
89 /// # Example
90 ///
91 /// ```rust
92 /// use mako_engine::ids::CorrelationId;
93 ///
94 /// let id = CorrelationId::from_interchange_ref("A000123");
95 /// // Same reference → same CorrelationId (idempotent dispatch).
96 /// assert_eq!(id, CorrelationId::from_interchange_ref("A000123"));
97 /// // Different reference → different CorrelationId.
98 /// assert_ne!(id, CorrelationId::from_interchange_ref("A000124"));
99 /// ```
100 #[must_use]
101 pub fn from_interchange_ref(interchange_ref: &str) -> Self {
102 const INTERCHANGE_NS: Uuid = Uuid::from_u128(0xa3c7_e1f0_5b2d_4e80_9f6a_1b3c_5d7e_9a0b);
103 Self(Uuid::new_v5(&INTERCHANGE_NS, interchange_ref.as_bytes()))
104 }
105}
106
107define_id!(
108 CausationId,
109 "Points to the event or command that directly caused this event."
110);
111define_id!(
112 ConversationId,
113 "Links events that belong to the same business conversation \
114 (e.g. a UTILMD exchange and its APERAK acknowledgement)."
115);
116define_id!(
117 ProcessId,
118 "Stable identifier for a single MaKo process instance."
119);
120define_id!(
121 TenantId,
122 "Scopes all streams and events to a single market participant or deployment tenant."
123);
124
125impl TenantId {
126 /// Derive a deterministic `TenantId` from a GLN or other opaque operator
127 /// identifier string using UUID v5 (SHA-1 hash in a fixed namespace).
128 ///
129 /// This allows the production binary (`makod`) to accept a GLN from the
130 /// Derive a deterministic `TenantId` from a market-participant identifier
131 /// (GLN, BDEW code, EIC, or any opaque operator string) using UUID v5
132 /// (SHA-1 hash in a fixed namespace).
133 ///
134 /// This allows the production binary (`makod`) to accept a BDEW code or
135 /// GLN from the CLI and produce a stable `TenantId` that is consistent
136 /// across process restarts, without requiring that the identifier already
137 /// be a UUID.
138 ///
139 /// The accepted identifier formats are:
140 /// - **BDEW code** (13-digit, agency `"293"`) — most common in German MaKo
141 /// - **GLN** (13-digit GS1, agency `"9"`) — global GS1 scheme, rare in MaKo
142 /// - **EIC** (16-char ENTSO-E, agency `"305"`) — used by TSOs / Regelzonen
143 /// - Any other opaque string used as `--tenant-id`
144 ///
145 /// The namespace UUID is fixed and private to this crate:
146 /// `7e4a6b1c-2d3e-5f60-8a9b-0c1d2e3f4a5b` (arbitrary, registered once).
147 ///
148 /// # Example
149 ///
150 /// ```rust
151 /// use mako_engine::ids::TenantId;
152 ///
153 /// // BDEW-issued market participant code (agency 293)
154 /// let id = TenantId::from_party_id("9900123456789");
155 /// assert_eq!(id, TenantId::from_party_id("9900123456789"));
156 /// assert_ne!(id, TenantId::from_party_id("9900357000004"));
157 ///
158 /// // EIC code (ENTSO-E, agency 305) — e.g. for a TSO
159 /// let tso = TenantId::from_party_id("10XDE-EON-NETZ--I");
160 /// assert_ne!(id, tso);
161 /// ```
162 #[must_use]
163 pub fn from_party_id(party_id: &str) -> Self {
164 // Fixed v5 namespace for MaKo tenant party identifiers.
165 const TENANT_NS: Uuid = Uuid::from_u128(0x7e4a_6b1c_2d3e_5f60_8a9b_0c1d_2e3f_4a5b);
166 Self(Uuid::new_v5(&TENANT_NS, party_id.as_bytes()))
167 }
168}
169
170define_id!(
171 OutboxMessageId,
172 "Unique identifier for a single outbox message entry."
173);
174
175define_id!(
176 DeadlineId,
177 "Unique identifier for a registered process deadline."
178);
179
180// ── Causation conversions ─────────────────────────────────────────────────────
181
182// `CausationId` tracks what *caused* an event. The cause is always an
183// `EventId` (a prior event) or a `CorrelationId` (a root command correlation).
184// These `From` impls enable ergonomic construction without a round-trip through
185// `as_uuid()`:
186//
187// ctx.with_causation(prior_event_id.into())
188// ctx.with_causation(correlation_id.into())
189
190impl From<EventId> for CausationId {
191 /// Treat a prior event as the direct cause of the next event.
192 fn from(id: EventId) -> Self {
193 Self(id.0)
194 }
195}
196
197impl From<CorrelationId> for CausationId {
198 /// Treat a correlation root as the direct cause (useful for first events).
199 fn from(id: CorrelationId) -> Self {
200 Self(id.0)
201 }
202}
203
204// ── StreamId ──────────────────────────────────────────────────────────────────
205
206/// An append-only event stream identifier.
207///
208/// Streams are named with a category prefix so routing and partitioning are
209/// explicit (e.g. `process/{tenant_id}/{process_id}`, `partner/{partner_id}`).
210#[derive(
211 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
212)]
213pub struct StreamId(Box<str>);
214
215impl StreamId {
216 /// Construct a stream identifier from any string-like value.
217 ///
218 /// # Panics
219 ///
220 /// Panics if `id` is empty or contains a NUL byte (`\0`).
221 /// Use this constructor only for **compile-time literals** where the value
222 /// is statically known to be valid. For runtime/externally-supplied strings
223 /// use [`StreamId::try_new`] or the typed constructors
224 /// ([`StreamId::for_process`], [`StreamId::for_partner`]).
225 #[must_use]
226 pub fn new(id: impl Into<Box<str>>) -> Self {
227 let id: Box<str> = id.into();
228 assert!(!id.is_empty(), "StreamId must not be empty");
229 assert!(
230 !id.contains('\0'),
231 "StreamId must not contain NUL bytes, got: {id:?}"
232 );
233 Self(id)
234 }
235
236 /// Fallible constructor — returns `Err` instead of panicking.
237 ///
238 /// Prefer this over [`StreamId::new`] whenever the input string originates
239 /// from user input, network data, or storage. The typed constructors
240 /// ([`StreamId::for_process`], [`StreamId::for_partner`]) call this
241 /// internally.
242 ///
243 /// # Errors
244 ///
245 /// Returns [`crate::error::EngineError::InvalidStreamId`] if `id` is empty or contains
246 /// a NUL byte.
247 pub fn try_new(id: impl Into<Box<str>>) -> Result<Self, crate::error::EngineError> {
248 let id: Box<str> = id.into();
249 if id.is_empty() {
250 return Err(crate::error::EngineError::InvalidStreamId {
251 input: id,
252 reason: "stream ID must not be empty",
253 });
254 }
255 if id.contains('\0') {
256 // Truncate the displayed input to avoid log injection via embedded
257 // NUL bytes or very long attacker-controlled strings.
258 let truncated: Box<str> = id.chars().take(200).collect::<String>().into();
259 return Err(crate::error::EngineError::InvalidStreamId {
260 input: truncated,
261 reason: "stream ID must not contain NUL bytes",
262 });
263 }
264 Ok(Self(id))
265 }
266
267 /// Canonical stream for a process instance: `process/{tenant_id}/{process_id}`.
268 ///
269 /// The tenant discriminator prevents cross-tenant event leakage when
270 /// `list_streams` is called with a tenant-scoped prefix
271 /// (`process/{tenant_id}/`).
272 #[must_use]
273 pub fn for_process(tenant_id: TenantId, process_id: &ProcessId) -> Self {
274 Self::new(format!("process/{tenant_id}/{process_id}"))
275 }
276
277 /// Canonical stream for a market partner: `partner/{partner_id}`.
278 ///
279 /// # Errors
280 ///
281 /// Returns an error if `partner_id` contains `/` or a NUL byte, which
282 /// would escape the `partner/` prefix used for range scans.
283 pub fn for_partner(partner_id: &str) -> Result<Self, crate::error::EngineError> {
284 if partner_id.contains('\0') || partner_id.contains('/') {
285 return Err(crate::error::EngineError::InvalidStreamId {
286 input: partner_id.chars().take(200).collect::<String>().into(),
287 reason: "partner_id must not contain '/' or NUL bytes",
288 });
289 }
290 Ok(Self::new(format!("partner/{partner_id}")))
291 }
292
293 /// The raw stream identifier string.
294 #[must_use]
295 pub fn as_str(&self) -> &str {
296 &self.0
297 }
298}
299
300impl fmt::Display for StreamId {
301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302 f.write_str(&self.0)
303 }
304}
305
306impl TryFrom<&str> for StreamId {
307 type Error = crate::error::EngineError;
308 fn try_from(s: &str) -> Result<Self, Self::Error> {
309 Self::try_new(s)
310 }
311}
312
313impl TryFrom<String> for StreamId {
314 type Error = crate::error::EngineError;
315 fn try_from(s: String) -> Result<Self, Self::Error> {
316 Self::try_new(s)
317 }
318}
319
320impl TryFrom<Box<str>> for StreamId {
321 type Error = crate::error::EngineError;
322 fn try_from(s: Box<str>) -> Result<Self, Self::Error> {
323 Self::try_new(s)
324 }
325}
326
327// ── ProcessIdentity ───────────────────────────────────────────────────────────
328
329/// A serializable value type that bundles all four process identifiers.
330///
331/// Use `ProcessIdentity` to persist process routing information without
332/// keeping a live [`Process`] handle. When a new inbound EDIFACT message
333/// arrives and must be routed to a running process, look up the identity in
334/// your routing table and call [`Process::from_identity`] to attach.
335///
336/// ## Example
337///
338/// ```rust,ignore
339/// // Persist after process creation:
340/// let identity = process.identity();
341/// routing_table.insert(utilmd_conversation_id, identity.clone());
342///
343/// // Restore on a subsequent message:
344/// let identity = routing_table.get(&aperak_conversation_id)?;
345/// let process = Process::<MyWorkflow, _>::from_identity(store, identity);
346/// process.execute(HandleAperak { .. }).await?;
347/// ```
348///
349/// [`Process`]: crate::process::Process
350/// [`Process::from_identity`]: crate::process::Process::from_identity
351#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
352pub struct ProcessIdentity {
353 /// The event stream identifier for this process.
354 stream_id: StreamId,
355 /// The stable process identifier.
356 pub process_id: ProcessId,
357 /// The tenant that owns this process.
358 pub tenant_id: TenantId,
359 /// The workflow version under which this process was created.
360 pub workflow_id: crate::version::WorkflowId,
361}
362
363impl ProcessIdentity {
364 /// Construct a `ProcessIdentity`, deriving `stream_id` automatically from
365 /// `tenant_id` and `process_id`.
366 ///
367 /// `stream_id` is always `StreamId::for_process(tenant_id, &process_id)` —
368 /// callers must not supply it independently to avoid accidental mismatches.
369 #[must_use]
370 pub fn new(
371 process_id: ProcessId,
372 tenant_id: TenantId,
373 workflow_id: crate::version::WorkflowId,
374 ) -> Self {
375 Self {
376 stream_id: StreamId::for_process(tenant_id, &process_id),
377 process_id,
378 tenant_id,
379 workflow_id,
380 }
381 }
382
383 /// The event stream identifier for this process.
384 #[must_use]
385 pub fn stream_id(&self) -> &StreamId {
386 &self.stream_id
387 }
388}