short_id/
lib.rs

1//! A tiny crate for generating short, URL-safe, unique identifiers.
2//!
3//! Unlike full UUIDs (which are 36 characters and include hyphens), `short-id` gives you
4//! compact 14-character strings that are easy to copy, paste, and use in URLs.
5//!
6//! # Goals
7//!
8//! 1. **Make it very easy to generate short random IDs** for things like request IDs,
9//!    user-facing tokens, test data, and log correlation.
10//!
11//! 2. **Provide an optional "ordered" variant** where IDs include a timestamp prefix,
12//!    so when you sort them as strings they roughly follow creation time.
13//!
14//! This crate is intentionally minimal - no configuration, no custom alphabets, no complex API.
15//!
16//! # Quick Start
17//!
18//! ```
19//! use short_id::short_id;
20//!
21//! // Generate a random ID
22//! let id = short_id();
23//! println!("Request ID: {}", id);
24//! // Example output: "X7K9mP2nQwE-Tg"
25//! ```
26//!
27//! For time-ordered IDs:
28//!
29//! ```
30//! use short_id::short_id_ordered;
31//!
32//! let id1 = short_id_ordered();
33//! std::thread::sleep(std::time::Duration::from_millis(100));
34//! let id2 = short_id_ordered();
35//!
36//! // IDs from different times are different
37//! assert_ne!(id1, id2);
38//! ```
39//!
40//! # Use Cases
41//!
42//! - Request IDs for logging and tracing
43//! - User-facing tokens and session IDs
44//! - Test data generation
45//! - Short URLs and resource identifiers
46//! - Any place you want something shorter and simpler than UUIDs
47//!
48//! # Characteristics
49//!
50//! - **Length**: Always exactly 14 characters
51//! - **URL-safe**: Only `A-Z`, `a-z`, `0-9`, `-`, `_` (no special characters)
52//! - **Cryptographically secure**: Uses `OsRng` for random bytes
53//! - **No configuration needed**: Just call the function
54//!
55//! # Features
56//!
57//! - **`std`** (enabled by default): Enables [`short_id_ordered()`] which needs `std::time::SystemTime`
58//!
59//! For `no_std` environments with `alloc`:
60//!
61//! ```toml
62//! [dependencies]
63//! short-id = { version = "0.1", default-features = false }
64//! ```
65//!
66//! In `no_std` mode, only [`short_id()`] is available.
67
68#![cfg_attr(not(feature = "std"), no_std)]
69
70#[cfg(not(feature = "std"))]
71extern crate alloc;
72
73#[cfg(not(feature = "std"))]
74use alloc::string::String;
75
76use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
77use rand::{rngs::OsRng, RngCore};
78
79/// Generates a random, URL-safe short ID.
80///
81/// Creates a 14-character ID from 10 cryptographically secure random bytes,
82/// encoded with base64url (no padding).
83///
84/// # Examples
85///
86/// Basic usage:
87///
88/// ```
89/// use short_id::short_id;
90///
91/// let id = short_id();
92/// assert_eq!(id.len(), 14);
93/// ```
94///
95/// Use for request IDs:
96///
97/// ```
98/// use short_id::short_id;
99///
100/// fn handle_request() -> String {
101///     let request_id = short_id();
102///     println!("Processing request {}", request_id);
103///     request_id
104/// }
105///
106/// let id = handle_request();
107/// assert_eq!(id.len(), 14);
108/// ```
109///
110/// Generate multiple unique IDs:
111///
112/// ```
113/// use short_id::short_id;
114///
115/// let ids: Vec<String> = (0..10).map(|_| short_id()).collect();
116///
117/// // All IDs are unique
118/// for i in 0..ids.len() {
119///     for j in i+1..ids.len() {
120///         assert_ne!(ids[i], ids[j]);
121///     }
122/// }
123/// ```
124///
125/// IDs are URL-safe:
126///
127/// ```
128/// use short_id::short_id;
129///
130/// let id = short_id();
131/// let url = format!("https://example.com/resource/{}", id);
132/// // No encoding needed - safe to use directly
133/// ```
134pub fn short_id() -> String {
135    let mut bytes = [0u8; 10];
136    OsRng.fill_bytes(&mut bytes);
137    URL_SAFE_NO_PAD.encode(bytes)
138}
139
140/// Generates a time-ordered, URL-safe short ID.
141///
142/// Creates a 14-character ID with microsecond-precision timestamp for excellent time
143/// resolution when generating IDs in rapid succession. The ID consists of:
144/// - First 8 bytes: Unix timestamp (microseconds since epoch) as big-endian u64
145/// - Next 2 bytes: Cryptographically secure random bytes
146///
147/// With microsecond precision, IDs created within the same microsecond will differ
148/// by their random component (65,536 possible values per microsecond).
149///
150/// **This function requires the `std` feature** (enabled by default).
151///
152/// # Examples
153///
154/// Basic usage:
155///
156/// ```
157/// use short_id::short_id_ordered;
158///
159/// let id = short_id_ordered();
160/// assert_eq!(id.len(), 14);
161/// ```
162///
163/// IDs from different times differ:
164///
165/// ```
166/// use short_id::short_id_ordered;
167///
168/// let id1 = short_id_ordered();
169/// std::thread::sleep(std::time::Duration::from_millis(100));
170/// let id2 = short_id_ordered();
171///
172/// // IDs generated at different times are different
173/// assert_ne!(id1, id2);
174/// ```
175///
176/// Even within the same second, IDs are unique:
177///
178/// ```
179/// use short_id::short_id_ordered;
180///
181/// let ids: Vec<String> = (0..10).map(|_| short_id_ordered()).collect();
182///
183/// // All unique due to random component
184/// for i in 0..ids.len() {
185///     for j in i+1..ids.len() {
186///         assert_ne!(ids[i], ids[j]);
187///     }
188/// }
189/// ```
190///
191/// Use for log entries:
192///
193/// ```
194/// use short_id::short_id_ordered;
195///
196/// struct LogEntry {
197///     id: String,
198///     message: String,
199/// }
200///
201/// impl LogEntry {
202///     fn new(message: String) -> Self {
203///         LogEntry {
204///             id: short_id_ordered(),
205///             message,
206///         }
207///     }
208/// }
209///
210/// let log = LogEntry::new("Started processing".to_string());
211/// assert_eq!(log.id.len(), 14);
212/// ```
213#[cfg(feature = "std")]
214pub fn short_id_ordered() -> String {
215    let timestamp_us = std::time::SystemTime::now()
216        .duration_since(std::time::UNIX_EPOCH)
217        .expect("system time before Unix epoch")
218        .as_micros() as u64;
219
220    let mut bytes = [0u8; 10];
221    bytes[0..8].copy_from_slice(&timestamp_us.to_be_bytes());
222    OsRng.fill_bytes(&mut bytes[8..10]);
223
224    URL_SAFE_NO_PAD.encode(bytes)
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_short_id_length() {
233        let id = short_id();
234        assert_eq!(id.len(), 14);
235    }
236
237    #[test]
238    fn test_short_id_unique() {
239        let id1 = short_id();
240        let id2 = short_id();
241        assert_ne!(id1, id2);
242    }
243
244    #[test]
245    fn test_short_id_url_safe() {
246        for _ in 0..100 {
247            let id = short_id();
248            assert!(!id.contains('+'));
249            assert!(!id.contains('/'));
250            assert!(!id.contains('='));
251        }
252    }
253
254    #[test]
255    fn test_many_unique_ids() {
256        // Generate many IDs and ensure all are unique
257        #[cfg(feature = "std")]
258        {
259            let ids: Vec<String> = (0..1000).map(|_| short_id()).collect();
260            let unique_count = ids.iter().collect::<std::collections::HashSet<_>>().len();
261            assert_eq!(unique_count, 1000);
262        }
263
264        #[cfg(not(feature = "std"))]
265        {
266            // In no_std, just verify a few IDs are unique
267            let id1 = short_id();
268            let id2 = short_id();
269            let id3 = short_id();
270            assert_ne!(id1, id2);
271            assert_ne!(id2, id3);
272            assert_ne!(id1, id3);
273        }
274    }
275
276    #[cfg(feature = "std")]
277    #[test]
278    fn test_short_id_ordered_length() {
279        let id = short_id_ordered();
280        assert_eq!(id.len(), 14);
281    }
282
283    #[cfg(feature = "std")]
284    #[test]
285    fn test_short_id_ordered_unique() {
286        let id1 = short_id_ordered();
287        let id2 = short_id_ordered();
288        assert_ne!(id1, id2);
289    }
290
291    #[cfg(feature = "std")]
292    #[test]
293    fn test_short_id_ordered_includes_timestamp() {
294        // Generate IDs and verify they contain timestamp information
295        // by checking they change over time
296        let id1 = short_id_ordered();
297        std::thread::sleep(std::time::Duration::from_secs(1));
298        let id2 = short_id_ordered();
299
300        // IDs from different times should differ
301        assert_ne!(id1, id2);
302    }
303
304    #[cfg(feature = "std")]
305    #[test]
306    fn test_short_id_ordered_url_safe() {
307        for _ in 0..100 {
308            let id = short_id_ordered();
309            assert!(!id.contains('+'));
310            assert!(!id.contains('/'));
311            assert!(!id.contains('='));
312        }
313    }
314}