use std::{
fmt::{Debug, Display},
str::FromStr,
};
use crate::{
task::from_request::FromRequest,
task::{Task, data::MissingDataError},
};
pub use random_id::RandomId;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", serde(untagged))]
#[non_exhaustive]
pub enum TaskId {
Int(u64),
String(String),
#[cfg(feature = "uuid")]
Uuid(uuid::Uuid),
#[cfg(feature = "ulid")]
Ulid(ulid::Ulid),
}
impl TaskId {
#[must_use]
pub fn from_int(id: u64) -> Self {
Self::Int(id)
}
#[must_use]
pub fn from_string(id: impl Into<String>) -> Self {
Self::String(id.into())
}
#[cfg(feature = "uuid")]
#[must_use]
pub fn from_uuid(id: uuid::Uuid) -> Self {
Self::Uuid(id)
}
#[cfg(feature = "ulid")]
#[must_use]
pub fn from_ulid(id: ulid::Ulid) -> Self {
Self::Ulid(id)
}
#[must_use]
#[allow(clippy::match_wildcard_for_single_variants)]
pub fn as_int(&self) -> Option<u64> {
match self {
Self::Int(id) => Some(*id),
_ => None,
}
}
#[must_use]
#[allow(clippy::match_wildcard_for_single_variants)]
pub fn as_string(&self) -> Option<&str> {
match self {
Self::String(id) => Some(id.as_str()),
_ => None,
}
}
#[cfg(feature = "uuid")]
#[must_use]
pub fn as_uuid(&self) -> Option<uuid::Uuid> {
match self {
Self::Uuid(id) => Some(*id),
_ => None,
}
}
#[cfg(feature = "ulid")]
#[must_use]
pub fn as_ulid(&self) -> Option<ulid::Ulid> {
match self {
Self::Ulid(id) => Some(*id),
_ => None,
}
}
}
impl TaskId {
#[cfg(feature = "uuid")]
#[must_use]
pub fn to_uuid(&self) -> uuid::Uuid {
const NAMESPACE: uuid::Uuid = uuid::Uuid::from_u128(0x6ba7b8109dad11d180b400c04fd430c8);
match self {
Self::Int(id) => uuid::Uuid::new_v5(&NAMESPACE, &id.to_be_bytes()),
Self::String(id) => uuid::Uuid::new_v5(&NAMESPACE, id.as_bytes()),
#[cfg(feature = "uuid")]
Self::Uuid(id) => *id,
#[cfg(feature = "ulid")]
Self::Ulid(id) => (*id).into(),
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TaskIdError {
#[error("could not decode task_id: `{0}`")]
Decode(String),
}
impl FromStr for TaskId {
type Err = TaskIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
#[cfg(feature = "uuid")]
if let Ok(id) = uuid::Uuid::from_str(s) {
return Ok(Self::Uuid(id));
}
#[cfg(feature = "ulid")]
if let Ok(id) = ulid::Ulid::from_str(s) {
return Ok(Self::Ulid(id));
}
if let Ok(id) = u64::from_str(s) {
return Ok(Self::Int(id));
}
if !s.is_empty() {
return Ok(Self::String(s.to_owned()));
}
Err(TaskIdError::Decode(s.to_owned()))
}
}
impl TryFrom<&'_ str> for TaskId {
type Error = TaskIdError;
fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
Self::from_str(value)
}
}
impl Display for TaskId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Int(id) => Display::fmt(id, f),
Self::String(id) => Display::fmt(id, f),
#[cfg(feature = "uuid")]
Self::Uuid(id) => Display::fmt(id, f),
#[cfg(feature = "ulid")]
Self::Ulid(id) => Display::fmt(id, f),
}
}
}
impl<Args: Sync> FromRequest<Task<Args>> for TaskId {
type Error = MissingDataError;
async fn from_request(req: &Task<Args>) -> Result<Self, Self::Error> {
req.task_id().cloned().ok_or(MissingDataError::NotFound(
std::any::type_name::<Self>().to_owned(),
))
}
}
impl From<u64> for TaskId {
fn from(id: u64) -> Self {
Self::Int(id)
}
}
impl From<String> for TaskId {
fn from(id: String) -> Self {
Self::String(id)
}
}
#[cfg(feature = "uuid")]
impl From<uuid::Uuid> for TaskId {
fn from(id: uuid::Uuid) -> Self {
Self::Uuid(id)
}
}
#[cfg(feature = "ulid")]
impl From<ulid::Ulid> for TaskId {
fn from(id: ulid::Ulid) -> Self {
Self::Ulid(id)
}
}
pub fn coalesce_ids<T: Display>(task_ids: impl IntoIterator<Item = T> + Send) -> String {
use std::fmt::Write;
let mut ids = String::from("[");
for (i, id) in task_ids.into_iter().enumerate() {
if i != 0 {
ids.push(',');
}
write!(&mut ids, "\"{id}\"").unwrap();
}
ids.push(']');
ids
}
pub trait GenerateId {
fn generate() -> TaskId;
}
#[cfg(feature = "uuid")]
impl GenerateId for uuid::Uuid {
fn generate() -> TaskId {
TaskId::Uuid(Self::new_v4())
}
}
#[cfg(feature = "ulid")]
impl GenerateId for ulid::Ulid {
fn generate() -> TaskId {
TaskId::Ulid(Self::generate())
}
}
impl GenerateId for RandomId {
fn generate() -> TaskId {
TaskId::from_string(Self::default())
}
}
impl GenerateId for u64 {
fn generate() -> TaskId {
TaskId::Int(rand::random::<Self>())
}
}
mod random_id {
use super::*;
use std::convert::Infallible;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
const ALPHABET: &[u8] = b"abcdefghijkmnopqrstuvwxyz23456789-";
const BASE: u64 = 34;
const TIME_LEN: usize = 6;
const RANDOM_LEN: usize = 5;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct RandomId(String);
impl From<RandomId> for String {
fn from(value: RandomId) -> Self {
value.0
}
}
impl FromStr for RandomId {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.to_owned()))
}
}
#[allow(clippy::infallible_try_from)]
impl TryFrom<&'_ str> for RandomId {
type Error = Infallible;
fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
Self::from_str(value)
}
}
impl Display for RandomId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl Default for RandomId {
fn default() -> Self {
Self(unique_id())
}
}
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn encode_base64(mut value: u64, length: usize) -> String {
let mut buf = vec![b'A'; length];
for i in (0..length).rev() {
buf[i] = ALPHABET[(value % BASE) as usize];
value /= BASE;
}
String::from_utf8(buf).unwrap()
}
pub(super) fn unique_id() -> String {
let timestamp = current_time_millis();
let time_str = encode_base64(timestamp, TIME_LEN);
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
let rand_part = encode_base64(rand::random::<u64>(), RANDOM_LEN);
format!("{time_str}{rand_part}{count}")
}
fn current_time_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
}