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(×tamp_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}