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)]
45pub struct PackageVersion(String);
46
47impl PackageVersion {
48 #[must_use]
50 pub fn new(version: impl Into<String>) -> Self {
51 Self(version.into())
52 }
53
54 #[must_use]
56 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59}
60
61impl fmt::Display for PackageVersion {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 self.0.fmt(formatter)
64 }
65}
66
67#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
69pub struct ActivityId(u64);
70
71impl ActivityId {
72 #[must_use]
74 pub const fn from_sequence_position(sequence_position: u64) -> Self {
75 Self(sequence_position)
76 }
77
78 #[must_use]
80 pub const fn sequence_position(&self) -> u64 {
81 self.0
82 }
83}
84
85impl fmt::Display for ActivityId {
86 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(formatter, "activity:{}", self.0)
88 }
89}
90
91#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
93pub enum IdError {
94 #[error("timer name must not be empty")]
96 EmptyTimerName,
97}
98
99#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
101pub struct TimerId(TimerIdKind);
102
103impl TimerId {
104 pub fn named(name: impl Into<String>) -> Result<Self, IdError> {
110 let name = name.into();
111 if name.is_empty() {
112 return Err(IdError::EmptyTimerName);
113 }
114 Ok(Self(TimerIdKind::Named(name)))
115 }
116
117 #[must_use]
119 pub const fn anonymous(sequence_position: u64) -> Self {
120 Self(TimerIdKind::Anonymous(sequence_position))
121 }
122
123 #[must_use]
125 pub fn name(&self) -> Option<&str> {
126 match &self.0 {
127 TimerIdKind::Named(name) => Some(name.as_str()),
128 TimerIdKind::Anonymous(_) => None,
129 }
130 }
131
132 #[must_use]
134 pub fn sequence_position(&self) -> Option<u64> {
135 match &self.0 {
136 TimerIdKind::Named(_) => None,
137 TimerIdKind::Anonymous(sequence_position) => Some(*sequence_position),
138 }
139 }
140}
141
142impl fmt::Display for TimerId {
143 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match &self.0 {
145 TimerIdKind::Named(name) => write!(formatter, "timer:named:{name}"),
146 TimerIdKind::Anonymous(sequence_position) => {
147 write!(formatter, "timer:anonymous:{sequence_position}")
148 }
149 }
150 }
151}
152
153#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
155enum TimerIdKind {
156 Named(String),
158 Anonymous(u64),
160}
161
162#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
164pub struct RunId(Uuid);
165
166impl RunId {
167 #[must_use]
169 pub const fn new(id: Uuid) -> Self {
170 Self(id)
171 }
172
173 #[must_use]
175 pub fn new_v4() -> Self {
176 Self(Uuid::new_v4())
177 }
178
179 #[must_use]
181 pub const fn as_uuid(&self) -> Uuid {
182 self.0
183 }
184}
185
186impl fmt::Display for RunId {
187 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
188 self.0.fmt(formatter)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use std::collections::HashMap;
195
196 use serde::de::DeserializeOwned;
197
198 use super::{ActivityId, IdError, RunId, TimerId, WorkflowId};
199
200 fn round_trip<T>(identifier: &T) -> Result<(), serde_json::Error>
201 where
202 T: DeserializeOwned + PartialEq + serde::Serialize + std::fmt::Debug,
203 {
204 let json = serde_json::to_string(identifier)?;
205 let decoded = serde_json::from_str::<T>(&json)?;
206 assert_eq!(*identifier, decoded);
207 Ok(())
208 }
209
210 #[test]
211 fn identifiers_round_trip_through_json() -> Result<(), Box<dyn std::error::Error>> {
212 round_trip(&WorkflowId::new_v4())?;
213 round_trip(&ActivityId::from_sequence_position(17))?;
214 round_trip(&TimerId::named("reminder")?)?;
215 round_trip(&TimerId::anonymous(29))?;
216 round_trip(&RunId::new_v4())?;
217 Ok(())
218 }
219
220 #[test]
221 fn uuid_identifiers_are_hash_map_keys() {
222 let workflow_id = WorkflowId::new_v4();
223 let run_id = RunId::new_v4();
224
225 let mut workflows = HashMap::new();
226 workflows.insert(workflow_id.clone(), "workflow");
227 assert_eq!(workflows.get(&workflow_id), Some(&"workflow"));
228
229 let mut runs = HashMap::new();
230 runs.insert(run_id.clone(), "run");
231 assert_eq!(runs.get(&run_id), Some(&"run"));
232 }
233
234 #[test]
235 fn sequence_identifiers_expose_positions() -> Result<(), IdError> {
236 let activity_id = ActivityId::from_sequence_position(42);
237 let timer_id = TimerId::anonymous(43);
238
239 assert_eq!(activity_id.sequence_position(), 42);
240 assert_eq!(timer_id.sequence_position(), Some(43));
241 assert_eq!(TimerId::named("deadline")?.name(), Some("deadline"));
242 Ok(())
243 }
244
245 #[test]
246 fn display_formats_are_stable() -> Result<(), IdError> {
247 let workflow_id = WorkflowId::new(uuid::Uuid::nil());
248 let run_id = RunId::new(uuid::Uuid::nil());
249
250 assert_eq!(
251 workflow_id.to_string(),
252 "00000000-0000-0000-0000-000000000000"
253 );
254 assert_eq!(run_id.to_string(), "00000000-0000-0000-0000-000000000000");
255 assert_eq!(
256 ActivityId::from_sequence_position(7).to_string(),
257 "activity:7"
258 );
259 assert_eq!(
260 TimerId::named("reminder")?.to_string(),
261 "timer:named:reminder"
262 );
263 assert_eq!(TimerId::anonymous(3).to_string(), "timer:anonymous:3");
264 Ok(())
265 }
266
267 #[test]
268 fn named_timer_rejects_empty_name() {
269 assert_eq!(TimerId::named(""), Err(IdError::EmptyTimerName));
270 }
271}