Skip to main content

commons/
id.rs

1//! ID generation utilities.
2//!
3//! Provides various ID generation strategies including timestamp-based,
4//! random, and sortable IDs.
5//!
6//! # Example
7//!
8//! ```rust
9//! use commons::id::{generate_id, IdFormat};
10//!
11//! let id = generate_id(IdFormat::Timestamp);
12//! println!("Generated ID: {}", id);
13//! ```
14
15use std::fmt::Write;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::time::{SystemTime, UNIX_EPOCH};
18
19/// Counter for timestamp-based ID uniqueness within the same millisecond.
20static TIMESTAMP_COUNTER: AtomicU64 = AtomicU64::new(0);
21
22/// Counter for entropy seeding in random byte generation.
23static ENTROPY_COUNTER: AtomicU64 = AtomicU64::new(0);
24
25/// ID format options.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum IdFormat {
28    /// Timestamp-based ID (sortable, 20 chars).
29    Timestamp,
30    /// Random hex ID (32 chars).
31    RandomHex,
32    /// Short random ID (12 chars, base62).
33    Short,
34    /// Prefixed ID with custom prefix.
35    Prefixed,
36}
37
38/// Generate a unique ID.
39///
40/// # Arguments
41///
42/// * `format` - The ID format to use
43///
44/// # Returns
45///
46/// A unique string ID.
47#[must_use]
48pub fn generate_id(format: IdFormat) -> String {
49    match format {
50        IdFormat::RandomHex => generate_random_hex(),
51        IdFormat::Short => generate_short_id(),
52        IdFormat::Timestamp | IdFormat::Prefixed => generate_timestamp_id(),
53    }
54}
55
56/// Generate a prefixed ID.
57///
58/// # Arguments
59///
60/// * `prefix` - Prefix string (e.g., "usr", "ord")
61///
62/// # Returns
63///
64/// A prefixed unique ID like `usr_abc123`.
65#[must_use]
66pub fn generate_prefixed_id(prefix: &str) -> String {
67    format!("{prefix}_{}", generate_short_id())
68}
69
70/// Generate a timestamp-based sortable ID.
71///
72/// Format: 13 digits timestamp + 7 digits counter = 20 chars
73/// IDs generated in the same millisecond are still unique and sortable.
74#[must_use]
75pub fn generate_timestamp_id() -> String {
76    let timestamp = current_timestamp_millis();
77    let counter = TIMESTAMP_COUNTER.fetch_add(1, Ordering::SeqCst) % 10_000_000;
78    format!("{timestamp:013}{counter:07}")
79}
80
81/// Generate a random hexadecimal ID (32 characters).
82#[must_use]
83pub fn generate_random_hex() -> String {
84    let mut bytes = [0u8; 16];
85    fill_random_bytes(&mut bytes);
86    bytes.iter().fold(String::with_capacity(32), |mut s, b| {
87        let _ = write!(s, "{b:02x}");
88        s
89    })
90}
91
92/// Generate a short random ID (12 characters, base62).
93#[must_use]
94pub fn generate_short_id() -> String {
95    const CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
96    let mut bytes = [0u8; 12];
97    fill_random_bytes(&mut bytes);
98
99    bytes
100        .iter()
101        .map(|b| CHARS[(*b as usize) % CHARS.len()] as char)
102        .collect()
103}
104
105/// Generate a UUID v4-like string.
106///
107/// Note: This is not a cryptographically secure UUID.
108/// For production use, consider the `uuid` crate.
109#[must_use]
110pub fn generate_uuid_like() -> String {
111    let mut bytes = [0u8; 16];
112    fill_random_bytes(&mut bytes);
113
114    // Set version (4) and variant bits
115    bytes[6] = (bytes[6] & 0x0f) | 0x40;
116    bytes[8] = (bytes[8] & 0x3f) | 0x80;
117
118    format!(
119        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
120        bytes[0],
121        bytes[1],
122        bytes[2],
123        bytes[3],
124        bytes[4],
125        bytes[5],
126        bytes[6],
127        bytes[7],
128        bytes[8],
129        bytes[9],
130        bytes[10],
131        bytes[11],
132        bytes[12],
133        bytes[13],
134        bytes[14],
135        bytes[15]
136    )
137}
138
139/// Get current timestamp in milliseconds.
140#[must_use]
141#[allow(clippy::cast_possible_truncation)]
142pub fn current_timestamp_millis() -> u64 {
143    SystemTime::now()
144        .duration_since(UNIX_EPOCH)
145        .unwrap_or_default()
146        .as_millis() as u64
147}
148
149/// Fill a byte slice with pseudo-random values.
150fn fill_random_bytes(bytes: &mut [u8]) {
151    use std::collections::hash_map::DefaultHasher;
152    use std::hash::{Hash, Hasher};
153
154    let counter = ENTROPY_COUNTER.fetch_add(1, Ordering::SeqCst);
155    let timestamp = current_timestamp_millis();
156
157    // Use multiple sources of entropy
158    let mut hasher = DefaultHasher::new();
159    timestamp.hash(&mut hasher);
160    counter.hash(&mut hasher);
161    std::process::id().hash(&mut hasher);
162    std::thread::current().id().hash(&mut hasher);
163
164    let mut seed = hasher.finish();
165
166    // Simple xorshift for pseudo-randomness
167    for byte in bytes.iter_mut() {
168        seed ^= seed << 13;
169        seed ^= seed >> 7;
170        seed ^= seed << 17;
171        *byte = (seed & 0xff) as u8;
172        // Mix in counter for each byte
173        seed = seed.wrapping_add(counter);
174    }
175}
176
177/// ID generator with configuration.
178#[derive(Debug, Clone)]
179pub struct IdGenerator {
180    prefix: Option<String>,
181    format: IdFormat,
182}
183
184impl IdGenerator {
185    /// Create a new ID generator.
186    #[must_use]
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    /// Set a prefix for generated IDs.
192    #[must_use]
193    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
194        self.prefix = Some(prefix.into());
195        self
196    }
197
198    /// Set the ID format.
199    #[must_use]
200    pub const fn with_format(mut self, format: IdFormat) -> Self {
201        self.format = format;
202        self
203    }
204
205    /// Generate an ID.
206    #[must_use]
207    pub fn generate(&self) -> String {
208        let id = generate_id(self.format);
209        match &self.prefix {
210            Some(p) => format!("{p}_{id}"),
211            None => id,
212        }
213    }
214}
215
216impl Default for IdGenerator {
217    fn default() -> Self {
218        Self {
219            prefix: None,
220            format: IdFormat::Short,
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use std::collections::HashSet;
229
230    #[test]
231    fn test_timestamp_id_format() {
232        let id = generate_timestamp_id();
233        assert_eq!(id.len(), 20);
234        assert!(id.chars().all(|c| c.is_ascii_digit()));
235    }
236
237    #[test]
238    fn test_timestamp_ids_are_sortable() {
239        let id1 = generate_timestamp_id();
240        std::thread::sleep(std::time::Duration::from_millis(1));
241        let id2 = generate_timestamp_id();
242        assert!(id1 < id2);
243    }
244
245    #[test]
246    fn test_timestamp_ids_unique_in_same_ms() {
247        let ids: Vec<String> = (0..100).map(|_| generate_timestamp_id()).collect();
248        let unique: HashSet<_> = ids.iter().collect();
249        assert_eq!(ids.len(), unique.len());
250    }
251
252    #[test]
253    fn test_random_hex_format() {
254        let id = generate_random_hex();
255        assert_eq!(id.len(), 32);
256        assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
257    }
258
259    #[test]
260    fn test_short_id_format() {
261        let id = generate_short_id();
262        assert_eq!(id.len(), 12);
263        assert!(id.chars().all(|c| c.is_ascii_alphanumeric()));
264    }
265
266    #[test]
267    fn test_uuid_like_format() {
268        let id = generate_uuid_like();
269        assert_eq!(id.len(), 36);
270        assert_eq!(id.chars().filter(|&c| c == '-').count(), 4);
271    }
272
273    #[test]
274    fn test_prefixed_id() {
275        let id = generate_prefixed_id("usr");
276        assert!(id.starts_with("usr_"));
277        assert_eq!(id.len(), 4 + 12); // "usr_" + 12 char short id
278    }
279
280    #[test]
281    fn test_id_generator() {
282        let generator = IdGenerator::new()
283            .with_prefix("order")
284            .with_format(IdFormat::Short);
285
286        let id = generator.generate();
287        assert!(id.starts_with("order_"));
288    }
289
290    #[test]
291    fn test_uniqueness() {
292        let ids: HashSet<String> = (0..1000).map(|_| generate_short_id()).collect();
293        assert_eq!(ids.len(), 1000);
294    }
295
296    #[test]
297    fn test_counter_isolation() {
298        // Heavy random ID generation should not exhaust the timestamp counter domain.
299        let _random_ids: Vec<String> = (0..1000).map(|_| generate_random_hex()).collect();
300
301        // Timestamp IDs should still be unique after heavy random generation.
302        let ts_ids: HashSet<String> = (0..100).map(|_| generate_timestamp_id()).collect();
303        assert_eq!(ts_ids.len(), 100);
304    }
305}