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