Skip to main content

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}