1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
10pub struct WorkflowId(Uuid);
11
12impl WorkflowId {
13 #[must_use]
15 pub const fn new(id: Uuid) -> Self {
16 Self(id)
17 }
18
19 #[must_use]
21 pub fn new_v4() -> Self {
22 Self(Uuid::new_v4())
23 }
24
25 #[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#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
40pub struct ActivityId(u64);
41
42impl ActivityId {
43 #[must_use]
45 pub const fn from_sequence_position(sequence_position: u64) -> Self {
46 Self(sequence_position)
47 }
48
49 #[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#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
64pub enum IdError {
65 #[error("timer name must not be empty")]
67 EmptyTimerName,
68}
69
70#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
72pub struct TimerId(TimerIdKind);
73
74impl TimerId {
75 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 #[must_use]
90 pub const fn anonymous(sequence_position: u64) -> Self {
91 Self(TimerIdKind::Anonymous(sequence_position))
92 }
93
94 #[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 #[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#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
126enum TimerIdKind {
127 Named(String),
129 Anonymous(u64),
131}
132
133#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
135pub struct RunId(Uuid);
136
137impl RunId {
138 #[must_use]
140 pub const fn new(id: Uuid) -> Self {
141 Self(id)
142 }
143
144 #[must_use]
146 pub fn new_v4() -> Self {
147 Self(Uuid::new_v4())
148 }
149
150 #[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}