entid/
identifier.rs

1/* Copyright © 2025, CosmicMind, Inc. */
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::fmt::{self, Display};
6use std::hash::Hash;
7use std::mem::transmute;
8use std::str::FromStr;
9use std::time::{Duration, UNIX_EPOCH};
10
11use serde::{Deserialize, Serialize};
12use ulid::Ulid;
13use uuid::{Uuid, Version};
14
15use crate::error::IdentifierError;
16
17/// **Trait for identifier types**
18///
19/// This trait defines the common interface for different identifier types (UUID, ULID).
20/// It allows the `EntityId` type to be generic over the identifier implementation.
21pub trait Identifier:
22    Sized + Clone + PartialEq + Eq + Hash + Display + Serialize + for<'de> Deserialize<'de>
23{
24    /// Parse a string into an identifier
25    fn parse<S: AsRef<str>>(s: S) -> Result<Self, IdentifierError>;
26
27    /// Generate a new random identifier
28    fn generate() -> Self;
29
30    /// Convert the identifier to a string representation
31    fn as_str(&self) -> &str;
32
33    /// Get the timestamp in milliseconds (if applicable)
34    fn timestamp_ms(&self) -> Option<u64>;
35}
36
37/// **UUID-based identifier implementation**
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub struct UuidIdentifier(Uuid);
40
41// Thread-local cache for string representations
42thread_local! {
43    static UUID_CACHE: RefCell<HashMap<Uuid, String>> =
44        RefCell::new(HashMap::new());
45}
46
47impl UuidIdentifier {
48    /// Create a new UUID v4 (random)
49    pub fn new_v4() -> Self {
50        Self(Uuid::new_v4())
51    }
52
53    /// Create a new UUID v5 (name-based with SHA-1 hash)
54    pub fn new_v5(namespace: &Uuid, name: &str) -> Self {
55        Self(Uuid::new_v5(namespace, name.as_bytes()))
56    }
57
58    /// Get the underlying UUID
59    pub fn uuid(&self) -> Uuid {
60        self.0
61    }
62
63    /// Get the UUID version
64    pub fn version(&self) -> Option<Version> {
65        self.0.get_version()
66    }
67}
68
69impl Identifier for UuidIdentifier {
70    fn parse<S: AsRef<str>>(s: S) -> Result<Self, IdentifierError> {
71        Ok(Self(
72            Uuid::parse_str(s.as_ref()).map_err(IdentifierError::from)?,
73        ))
74    }
75
76    fn generate() -> Self {
77        Self::new_v4()
78    }
79
80    fn as_str(&self) -> &str {
81        UUID_CACHE.with(|cache| {
82            let mut cache = cache.borrow_mut();
83            cache.entry(self.0).or_insert_with(|| self.0.to_string());
84            // This is safe because we know the string exists in the cache
85            // and the cache lives for the duration of the thread
86            unsafe { transmute(cache.get(&self.0).unwrap().as_str()) }
87        })
88    }
89
90    fn timestamp_ms(&self) -> Option<u64> {
91        // UUIDs don't have a timestamp component by default
92        // UUID v1 has a timestamp, but we'd need to extract it
93        None
94    }
95}
96
97impl Display for UuidIdentifier {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(f, "{}", self.as_str())
100    }
101}
102
103impl From<Uuid> for UuidIdentifier {
104    fn from(uuid: Uuid) -> Self {
105        Self(uuid)
106    }
107}
108
109impl FromStr for UuidIdentifier {
110    type Err = IdentifierError;
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        Self::parse(s)
113    }
114}
115
116/// **ULID-based identifier implementation**
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
118pub struct UlidIdentifier(Ulid);
119
120// Thread-local cache for string representations
121thread_local! {
122    static ULID_CACHE: RefCell<HashMap<Ulid, String>> =
123        RefCell::new(HashMap::new());
124}
125
126impl Default for UlidIdentifier {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132impl UlidIdentifier {
133    /// Create a new ULID
134    pub fn new() -> Self {
135        Self(Ulid::new())
136    }
137
138    /// Create a ULID with a specific timestamp
139    pub fn with_timestamp(timestamp_ms: u64) -> Self {
140        // Use 0 for the random component
141        Self(Ulid::from_datetime(
142            UNIX_EPOCH + Duration::from_millis(timestamp_ms),
143        ))
144    }
145
146    /// Get the underlying ULID
147    pub fn ulid(&self) -> Ulid {
148        self.0
149    }
150
151    /// Get the timestamp in milliseconds
152    pub fn get_timestamp_ms(&self) -> u64 {
153        // Convert the datetime to milliseconds since UNIX epoch
154        let datetime = self.0.datetime();
155        let since_epoch = datetime
156            .duration_since(UNIX_EPOCH)
157            .unwrap_or(Duration::from_secs(0));
158        since_epoch.as_millis() as u64
159    }
160
161    /// Create a monotonic ULID based on a previous one
162    pub fn monotonic_from(previous: Option<&Self>) -> Self {
163        match previous {
164            Some(prev) => {
165                let new_ulid = Ulid::new();
166                let prev_ms = prev.get_timestamp_ms();
167                let new_ms = {
168                    let datetime = new_ulid.datetime();
169                    let since_epoch = datetime
170                        .duration_since(UNIX_EPOCH)
171                        .unwrap_or(Duration::from_secs(0));
172                    since_epoch.as_millis() as u64
173                };
174
175                if new_ms <= prev_ms {
176                    // Create a new ULID with the previous timestamp + 1ms and random bits
177                    Self(Ulid::from_datetime(
178                        UNIX_EPOCH + Duration::from_millis(prev_ms + 1),
179                    ))
180                } else {
181                    Self(new_ulid)
182                }
183            }
184            None => Self::new(),
185        }
186    }
187}
188
189impl Identifier for UlidIdentifier {
190    fn parse<S: AsRef<str>>(s: S) -> Result<Self, IdentifierError> {
191        Ok(Self(
192            Ulid::from_string(s.as_ref()).map_err(IdentifierError::from)?,
193        ))
194    }
195
196    fn generate() -> Self {
197        Self::new()
198    }
199
200    fn as_str(&self) -> &str {
201        ULID_CACHE.with(|cache| {
202            let mut cache = cache.borrow_mut();
203            cache.entry(self.0).or_insert_with(|| self.0.to_string());
204            // This is safe because we know the string exists in the cache
205            // and the cache lives for the duration of the thread
206            unsafe { transmute(cache.get(&self.0).unwrap().as_str()) }
207        })
208    }
209
210    fn timestamp_ms(&self) -> Option<u64> {
211        Some(self.get_timestamp_ms())
212    }
213}
214
215impl Display for UlidIdentifier {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        f.write_str(self.as_str())
218    }
219}
220
221impl From<Ulid> for UlidIdentifier {
222    fn from(ulid: Ulid) -> Self {
223        Self(ulid)
224    }
225}
226
227impl FromStr for UlidIdentifier {
228    type Err = IdentifierError;
229    fn from_str(s: &str) -> Result<Self, Self::Err> {
230        Self::parse(s)
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_uuid_identifier() {
240        let id = UuidIdentifier::generate();
241        let id_str = id.as_str();
242        let parsed = UuidIdentifier::parse(id_str).unwrap();
243        assert_eq!(id, parsed);
244    }
245
246    #[test]
247    fn test_ulid_identifier() {
248        let id = UlidIdentifier::generate();
249        let id_str = id.as_str();
250        let parsed = UlidIdentifier::parse(id_str).unwrap();
251        assert_eq!(id, parsed);
252
253        // Test timestamp
254        assert!(id.timestamp_ms().is_some());
255    }
256}