Skip to main content

aion_core/
ids.rs

1//! Strongly typed identifiers for workflows, activities, timers, and runs.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8/// Identifier for a logical workflow.
9#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
10pub struct WorkflowId(Uuid);
11
12impl WorkflowId {
13    /// Creates a workflow identifier from an existing UUID.
14    #[must_use]
15    pub const fn new(id: Uuid) -> Self {
16        Self(id)
17    }
18
19    /// Creates a workflow identifier with a random version 4 UUID.
20    #[must_use]
21    pub fn new_v4() -> Self {
22        Self(Uuid::new_v4())
23    }
24
25    /// Returns the UUID backing this identifier.
26    #[must_use]
27    pub const fn as_uuid(&self) -> Uuid {
28        self.0
29    }
30}
31
32impl fmt::Display for WorkflowId {
33    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34        self.0.fmt(formatter)
35    }
36}
37
38/// Identifier for an activity scheduled within a workflow history.
39#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
40pub struct ActivityId(u64);
41
42impl ActivityId {
43    /// Derives an activity identifier from its scheduling sequence position.
44    #[must_use]
45    pub const fn from_sequence_position(sequence_position: u64) -> Self {
46        Self(sequence_position)
47    }
48
49    /// Returns the scheduling sequence position used to derive this identifier.
50    #[must_use]
51    pub const fn sequence_position(&self) -> u64 {
52        self.0
53    }
54}
55
56impl fmt::Display for ActivityId {
57    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(formatter, "activity:{}", self.0)
59    }
60}
61
62/// Errors from identifier construction.
63#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
64pub enum IdError {
65    /// A timer name must be non-empty.
66    #[error("timer name must not be empty")]
67    EmptyTimerName,
68}
69
70/// Identifier for a timer scheduled by workflow code or by the engine.
71#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
72pub struct TimerId(TimerIdKind);
73
74impl TimerId {
75    /// Creates an author-assigned timer identifier.
76    ///
77    /// # Errors
78    ///
79    /// Returns [`IdError::EmptyTimerName`] if the name is empty.
80    pub fn named(name: impl Into<String>) -> Result<Self, IdError> {
81        let name = name.into();
82        if name.is_empty() {
83            return Err(IdError::EmptyTimerName);
84        }
85        Ok(Self(TimerIdKind::Named(name)))
86    }
87
88    /// Creates an engine-assigned timer identifier derived from sequence position.
89    #[must_use]
90    pub const fn anonymous(sequence_position: u64) -> Self {
91        Self(TimerIdKind::Anonymous(sequence_position))
92    }
93
94    /// Returns the author-assigned timer name, if this is a named timer.
95    #[must_use]
96    pub fn name(&self) -> Option<&str> {
97        match &self.0 {
98            TimerIdKind::Named(name) => Some(name.as_str()),
99            TimerIdKind::Anonymous(_) => None,
100        }
101    }
102
103    /// Returns the scheduling sequence position, if this is an anonymous timer.
104    #[must_use]
105    pub fn sequence_position(&self) -> Option<u64> {
106        match &self.0 {
107            TimerIdKind::Named(_) => None,
108            TimerIdKind::Anonymous(sequence_position) => Some(*sequence_position),
109        }
110    }
111}
112
113impl fmt::Display for TimerId {
114    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115        match &self.0 {
116            TimerIdKind::Named(name) => write!(formatter, "timer:named:{name}"),
117            TimerIdKind::Anonymous(sequence_position) => {
118                write!(formatter, "timer:anonymous:{sequence_position}")
119            }
120        }
121    }
122}
123
124/// Backing representation for timer identifiers.
125#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
126enum TimerIdKind {
127    /// Author-assigned timer name.
128    Named(String),
129    /// Engine-assigned timer sequence position.
130    Anonymous(u64),
131}
132
133/// Identifier for a concrete run of a logical workflow.
134#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
135pub struct RunId(Uuid);
136
137impl RunId {
138    /// Creates a run identifier from an existing UUID.
139    #[must_use]
140    pub const fn new(id: Uuid) -> Self {
141        Self(id)
142    }
143
144    /// Creates a run identifier with a random version 4 UUID.
145    #[must_use]
146    pub fn new_v4() -> Self {
147        Self(Uuid::new_v4())
148    }
149
150    /// Returns the UUID backing this identifier.
151    #[must_use]
152    pub const fn as_uuid(&self) -> Uuid {
153        self.0
154    }
155}
156
157impl fmt::Display for RunId {
158    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
159        self.0.fmt(formatter)
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use std::collections::HashMap;
166
167    use serde::de::DeserializeOwned;
168
169    use super::{ActivityId, IdError, RunId, TimerId, WorkflowId};
170
171    fn round_trip<T>(identifier: &T) -> Result<(), serde_json::Error>
172    where
173        T: DeserializeOwned + PartialEq + serde::Serialize + std::fmt::Debug,
174    {
175        let json = serde_json::to_string(identifier)?;
176        let decoded = serde_json::from_str::<T>(&json)?;
177        assert_eq!(*identifier, decoded);
178        Ok(())
179    }
180
181    #[test]
182    fn identifiers_round_trip_through_json() -> Result<(), Box<dyn std::error::Error>> {
183        round_trip(&WorkflowId::new_v4())?;
184        round_trip(&ActivityId::from_sequence_position(17))?;
185        round_trip(&TimerId::named("reminder")?)?;
186        round_trip(&TimerId::anonymous(29))?;
187        round_trip(&RunId::new_v4())?;
188        Ok(())
189    }
190
191    #[test]
192    fn uuid_identifiers_are_hash_map_keys() {
193        let workflow_id = WorkflowId::new_v4();
194        let run_id = RunId::new_v4();
195
196        let mut workflows = HashMap::new();
197        workflows.insert(workflow_id.clone(), "workflow");
198        assert_eq!(workflows.get(&workflow_id), Some(&"workflow"));
199
200        let mut runs = HashMap::new();
201        runs.insert(run_id.clone(), "run");
202        assert_eq!(runs.get(&run_id), Some(&"run"));
203    }
204
205    #[test]
206    fn sequence_identifiers_expose_positions() -> Result<(), IdError> {
207        let activity_id = ActivityId::from_sequence_position(42);
208        let timer_id = TimerId::anonymous(43);
209
210        assert_eq!(activity_id.sequence_position(), 42);
211        assert_eq!(timer_id.sequence_position(), Some(43));
212        assert_eq!(TimerId::named("deadline")?.name(), Some("deadline"));
213        Ok(())
214    }
215
216    #[test]
217    fn display_formats_are_stable() -> Result<(), IdError> {
218        let workflow_id = WorkflowId::new(uuid::Uuid::nil());
219        let run_id = RunId::new(uuid::Uuid::nil());
220
221        assert_eq!(
222            workflow_id.to_string(),
223            "00000000-0000-0000-0000-000000000000"
224        );
225        assert_eq!(run_id.to_string(), "00000000-0000-0000-0000-000000000000");
226        assert_eq!(
227            ActivityId::from_sequence_position(7).to_string(),
228            "activity:7"
229        );
230        assert_eq!(
231            TimerId::named("reminder")?.to_string(),
232            "timer:named:reminder"
233        );
234        assert_eq!(TimerId::anonymous(3).to_string(), "timer:anonymous:3");
235        Ok(())
236    }
237
238    #[test]
239    fn named_timer_rejects_empty_name() {
240        assert_eq!(TimerId::named(""), Err(IdError::EmptyTimerName));
241    }
242}