apalis_core/task/
task_id.rs1use std::{
7 fmt::{Debug, Display},
8 str::FromStr,
9};
10
11use crate::{
12 task::from_request::FromRequest,
13 task::{Task, data::MissingDataError},
14};
15
16pub use random_id::RandomId;
17
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
25#[cfg_attr(feature = "serde", serde(untagged))]
26#[non_exhaustive]
27pub enum TaskId {
28 Int(u64),
30 String(String),
32 #[cfg(feature = "uuid")]
34 Uuid(uuid::Uuid),
35 #[cfg(feature = "ulid")]
37 Ulid(ulid::Ulid),
38}
39
40impl TaskId {
41 #[must_use]
43 pub fn from_int(id: u64) -> Self {
44 Self::Int(id)
45 }
46
47 #[must_use]
49 pub fn from_string(id: impl Into<String>) -> Self {
50 Self::String(id.into())
51 }
52
53 #[cfg(feature = "uuid")]
55 #[must_use]
56 pub fn from_uuid(id: uuid::Uuid) -> Self {
57 Self::Uuid(id)
58 }
59
60 #[cfg(feature = "ulid")]
62 #[must_use]
63 pub fn from_ulid(id: ulid::Ulid) -> Self {
64 Self::Ulid(id)
65 }
66
67 #[must_use]
69 #[allow(clippy::match_wildcard_for_single_variants)]
70 pub fn as_int(&self) -> Option<u64> {
71 match self {
72 Self::Int(id) => Some(*id),
73 _ => None,
74 }
75 }
76
77 #[must_use]
79 #[allow(clippy::match_wildcard_for_single_variants)]
80 pub fn as_string(&self) -> Option<&str> {
81 match self {
82 Self::String(id) => Some(id.as_str()),
83 _ => None,
84 }
85 }
86
87 #[cfg(feature = "uuid")]
89 #[must_use]
90 pub fn as_uuid(&self) -> Option<uuid::Uuid> {
91 match self {
92 Self::Uuid(id) => Some(*id),
93 _ => None,
94 }
95 }
96
97 #[cfg(feature = "ulid")]
99 #[must_use]
100 pub fn as_ulid(&self) -> Option<ulid::Ulid> {
101 match self {
102 Self::Ulid(id) => Some(*id),
103 _ => None,
104 }
105 }
106}
107
108impl TaskId {
109 #[cfg(feature = "uuid")]
111 #[must_use]
112 pub fn to_uuid(&self) -> uuid::Uuid {
113 const NAMESPACE: uuid::Uuid = uuid::Uuid::from_u128(0x6ba7b8109dad11d180b400c04fd430c8);
114
115 match self {
116 Self::Int(id) => uuid::Uuid::new_v5(&NAMESPACE, &id.to_be_bytes()),
117 Self::String(id) => uuid::Uuid::new_v5(&NAMESPACE, id.as_bytes()),
118 #[cfg(feature = "uuid")]
119 Self::Uuid(id) => *id,
120 #[cfg(feature = "ulid")]
121 Self::Ulid(id) => (*id).into(),
122 }
123 }
124}
125
126#[derive(Debug, thiserror::Error)]
128#[non_exhaustive]
129pub enum TaskIdError {
130 #[error("could not decode task_id: `{0}`")]
132 Decode(String),
133}
134
135impl FromStr for TaskId {
136 type Err = TaskIdError;
137 fn from_str(s: &str) -> Result<Self, Self::Err> {
138 #[cfg(feature = "uuid")]
139 if let Ok(id) = uuid::Uuid::from_str(s) {
140 return Ok(Self::Uuid(id));
141 }
142
143 #[cfg(feature = "ulid")]
144 if let Ok(id) = ulid::Ulid::from_str(s) {
145 return Ok(Self::Ulid(id));
146 }
147
148 if let Ok(id) = u64::from_str(s) {
149 return Ok(Self::Int(id));
150 }
151
152 if !s.is_empty() {
153 return Ok(Self::String(s.to_owned()));
154 }
155
156 Err(TaskIdError::Decode(s.to_owned()))
157 }
158}
159
160impl TryFrom<&'_ str> for TaskId {
161 type Error = TaskIdError;
162
163 fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
164 Self::from_str(value)
165 }
166}
167
168impl Display for TaskId {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 Self::Int(id) => Display::fmt(id, f),
172 Self::String(id) => Display::fmt(id, f),
173 #[cfg(feature = "uuid")]
174 Self::Uuid(id) => Display::fmt(id, f),
175 #[cfg(feature = "ulid")]
176 Self::Ulid(id) => Display::fmt(id, f),
177 }
178 }
179}
180
181impl<Args: Sync> FromRequest<Task<Args>> for TaskId {
182 type Error = MissingDataError;
183 async fn from_request(req: &Task<Args>) -> Result<Self, Self::Error> {
184 req.task_id().cloned().ok_or(MissingDataError::NotFound(
185 std::any::type_name::<Self>().to_owned(),
186 ))
187 }
188}
189
190impl From<u64> for TaskId {
191 fn from(id: u64) -> Self {
192 Self::Int(id)
193 }
194}
195
196impl From<String> for TaskId {
197 fn from(id: String) -> Self {
198 Self::String(id)
199 }
200}
201
202#[cfg(feature = "uuid")]
203impl From<uuid::Uuid> for TaskId {
204 fn from(id: uuid::Uuid) -> Self {
205 Self::Uuid(id)
206 }
207}
208
209#[cfg(feature = "ulid")]
210impl From<ulid::Ulid> for TaskId {
211 fn from(id: ulid::Ulid) -> Self {
212 Self::Ulid(id)
213 }
214}
215
216pub fn coalesce_ids<T: Display>(task_ids: impl IntoIterator<Item = T> + Send) -> String {
220 use std::fmt::Write;
221 let mut ids = String::from("[");
222
223 for (i, id) in task_ids.into_iter().enumerate() {
224 if i != 0 {
225 ids.push(',');
226 }
227 write!(&mut ids, "\"{id}\"").unwrap();
228 }
229
230 ids.push(']');
231 ids
232}
233pub trait GenerateId {
235 fn generate() -> TaskId;
237}
238
239#[cfg(feature = "uuid")]
240impl GenerateId for uuid::Uuid {
241 fn generate() -> TaskId {
242 TaskId::Uuid(Self::new_v4())
243 }
244}
245
246#[cfg(feature = "ulid")]
247impl GenerateId for ulid::Ulid {
248 fn generate() -> TaskId {
249 TaskId::Ulid(Self::generate())
250 }
251}
252
253impl GenerateId for RandomId {
254 fn generate() -> TaskId {
255 TaskId::from_string(Self::default())
256 }
257}
258
259impl GenerateId for u64 {
260 fn generate() -> TaskId {
261 TaskId::Int(rand::random::<Self>())
262 }
263}
264
265mod random_id {
266 use super::*;
267 use std::convert::Infallible;
268 use std::sync::atomic::{AtomicU64, Ordering};
269 use std::time::{SystemTime, UNIX_EPOCH};
270
271 const ALPHABET: &[u8] = b"abcdefghijkmnopqrstuvwxyz23456789-";
272 const BASE: u64 = 34;
273 const TIME_LEN: usize = 6;
274 const RANDOM_LEN: usize = 5;
275
276 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
281 #[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
282 pub struct RandomId(String);
283
284 impl From<RandomId> for String {
285 fn from(value: RandomId) -> Self {
286 value.0
287 }
288 }
289
290 impl FromStr for RandomId {
291 type Err = Infallible;
292 fn from_str(s: &str) -> Result<Self, Self::Err> {
293 Ok(Self(s.to_owned()))
294 }
295 }
296
297 #[allow(clippy::infallible_try_from)]
298 impl TryFrom<&'_ str> for RandomId {
299 type Error = Infallible;
300
301 fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
302 Self::from_str(value)
303 }
304 }
305
306 impl Display for RandomId {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 Display::fmt(&self.0, f)
309 }
310 }
311
312 impl Default for RandomId {
313 fn default() -> Self {
314 Self(unique_id())
315 }
316 }
317
318 static COUNTER: AtomicU64 = AtomicU64::new(0);
320
321 fn encode_base64(mut value: u64, length: usize) -> String {
323 let mut buf = vec![b'A'; length];
324 for i in (0..length).rev() {
325 buf[i] = ALPHABET[(value % BASE) as usize];
326 value /= BASE;
327 }
328 String::from_utf8(buf).unwrap()
329 }
330
331 pub(super) fn unique_id() -> String {
333 let timestamp = current_time_millis();
334 let time_str = encode_base64(timestamp, TIME_LEN);
335
336 let count = COUNTER.fetch_add(1, Ordering::Relaxed);
338 let rand_part = encode_base64(rand::random::<u64>(), RANDOM_LEN);
339
340 format!("{time_str}{rand_part}{count}")
341 }
342
343 fn current_time_millis() -> u64 {
345 SystemTime::now()
346 .duration_since(UNIX_EPOCH)
347 .unwrap_or_default()
348 .as_millis() as u64
349 }
350}