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 (default)
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//! # Advanced: Custom Length IDs
56//!
57//! For advanced use cases, you can control the ID length by specifying the number of random bytes:
58//!
59//! ```
60//! use short_id::{short_id_with_bytes, short_id_ordered_with_bytes};
61//!
62//! // Generate a shorter 8-character ID (6 bytes)
63//! let short = short_id_with_bytes(6).unwrap();
64//! assert_eq!(short.len(), 8);
65//!
66//! // Generate a longer 22-character ID (16 bytes)
67//! let long = short_id_with_bytes(16).unwrap();
68//! assert_eq!(long.len(), 22);
69//!
70//! // Time-ordered IDs also support custom lengths
71//! let ordered = short_id_ordered_with_bytes(12).unwrap();
72//! ```
73//!
74//! **When to use custom lengths:**
75//!
76//! - **Fewer bytes (e.g., 4-6)**: Use for low-volume applications where you need very short IDs
77//! and collision risk is acceptable. Keep in mind that 6 bytes provides only ~48 bits of entropy.
78//!
79//! - **Default (10 bytes)**: Recommended for most applications. Provides ~80 bits of entropy
80//! with 14-character IDs. The [`short_id()`] and [`short_id_ordered()`] functions use this.
81//!
82//! - **More bytes (e.g., 16-32)**: Use for high-volume applications or when you need extra
83//! safety margin. 16 bytes provides ~128 bits of entropy.
84//!
85//! **Important:** Using fewer bytes significantly increases collision probability. For most users,
86//! the default [`short_id()`] and [`short_id_ordered()`] functions are recommended.
87//!
88//! # Features
89//!
90//! - **`std`** (enabled by default): Enables [`short_id_ordered()`] and [`short_id_ordered_with_bytes()`]
91//! which need `std::time::SystemTime`
92//!
93//! For `no_std` environments with `alloc`:
94//!
95//! ```toml
96//! [dependencies]
97//! short-id = { version = "0.4", default-features = false }
98//! ```
99//!
100//! In `no_std` mode, only [`short_id()`] and [`short_id_with_bytes()`] are available.
101
102#![cfg_attr(not(feature = "std"), no_std)]
103
104#[cfg(not(feature = "std"))]
105extern crate alloc;
106
107#[cfg(not(feature = "std"))]
108use alloc::string::String;
109
110#[cfg(not(feature = "std"))]
111use alloc::vec;
112
113#[cfg(feature = "std")]
114use std::vec;
115
116use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
117use rand::{rngs::OsRng, RngCore};
118use thiserror::Error;
119
120/// Maximum number of random bytes allowed for custom-length ID generation.
121///
122/// This limit prevents excessive memory allocation and ensures reasonable ID sizes.
123const MAX_BYTES: usize = 32;
124
125/// Errors returned by the custom-length ID generation functions.
126#[derive(Error, Debug, Clone, PartialEq, Eq)]
127pub enum ShortIdError {
128 /// `num_bytes` was 0.
129 #[error("num_bytes must be greater than 0")]
130 ZeroBytes,
131 /// `num_bytes` exceeded [`MAX_BYTES`].
132 #[error("num_bytes must not exceed {max} (got {requested})")]
133 TooManyBytes { requested: usize, max: usize },
134 /// `num_bytes` was less than 8, which is required for the timestamp prefix in ordered IDs.
135 #[error("num_bytes must be at least 8 for ordered IDs (got {requested})")]
136 TooFewBytesForOrdered { requested: usize },
137 /// The string is not a valid `ShortId` (wrong length or characters outside the URL-safe
138 /// base64 alphabet).
139 #[error("string is not a valid ShortId (wrong length or invalid characters)")]
140 InvalidString,
141}
142
143/// Convenience macro for generating a random short ID.
144///
145/// This macro simply calls [`short_id()`] and is provided for ergonomics.
146///
147/// # Examples
148///
149/// ```
150/// use short_id::id;
151///
152/// let request_id = id!();
153/// assert_eq!(request_id.len(), 14);
154/// ```
155#[macro_export]
156macro_rules! id {
157 () => {
158 $crate::short_id()
159 };
160}
161
162/// Convenience macro for generating a time-ordered short ID.
163///
164/// This macro simply calls [`short_id_ordered()`] and is provided for ergonomics.
165/// Requires the `std` feature (enabled by default).
166///
167/// # Examples
168///
169/// ```
170/// use short_id::ordered_id;
171///
172/// let log_id = ordered_id!();
173/// assert_eq!(log_id.len(), 14);
174/// ```
175#[cfg(feature = "std")]
176#[macro_export]
177macro_rules! ordered_id {
178 () => {
179 $crate::short_id_ordered()
180 };
181}
182
183/// Internal helper: generates a random ID with the specified number of bytes.
184///
185/// Callers must validate `num_bytes` before calling this function.
186fn generate_random_id(num_bytes: usize) -> String {
187 let mut bytes = vec![0u8; num_bytes];
188 OsRng.fill_bytes(&mut bytes);
189 URL_SAFE_NO_PAD.encode(&bytes)
190}
191
192/// Generates a random, URL-safe short ID.
193///
194/// Creates a 14-character ID from 10 cryptographically secure random bytes,
195/// encoded with base64url (no padding).
196///
197/// # Examples
198///
199/// Basic usage:
200///
201/// ```
202/// use short_id::short_id;
203///
204/// let id = short_id();
205/// assert_eq!(id.len(), 14);
206/// ```
207///
208/// Use for request IDs:
209///
210/// ```
211/// use short_id::short_id;
212///
213/// fn handle_request() -> String {
214/// let request_id = short_id();
215/// println!("Processing request {}", request_id);
216/// request_id
217/// }
218///
219/// let id = handle_request();
220/// assert_eq!(id.len(), 14);
221/// ```
222///
223/// Generate multiple unique IDs:
224///
225/// ```
226/// use short_id::short_id;
227///
228/// let ids: Vec<String> = (0..10).map(|_| short_id()).collect();
229///
230/// // All IDs are unique
231/// for i in 0..ids.len() {
232/// for j in i+1..ids.len() {
233/// assert_ne!(ids[i], ids[j]);
234/// }
235/// }
236/// ```
237///
238/// IDs are URL-safe:
239///
240/// ```
241/// use short_id::short_id;
242///
243/// let id = short_id();
244/// let url = format!("https://example.com/resource/{}", id);
245/// // No encoding needed - safe to use directly
246/// ```
247pub fn short_id() -> String {
248 generate_random_id(10)
249}
250
251/// Internal helper: generates a time-ordered ID with the specified number of bytes.
252///
253/// Uses 8 bytes for timestamp and fills the remaining bytes with random data.
254/// Callers must validate `num_bytes` before calling this function.
255#[cfg(feature = "std")]
256fn generate_ordered_id(num_bytes: usize) -> String {
257 // as_micros() returns u128; cast to u64 is safe — u64 microseconds
258 // overflow in ~584,000 years (year ~586,912 AD).
259 let timestamp_us = std::time::SystemTime::now()
260 .duration_since(std::time::UNIX_EPOCH)
261 .expect("system time before Unix epoch")
262 .as_micros() as u64;
263
264 let mut bytes = vec![0u8; num_bytes];
265 bytes[0..8].copy_from_slice(×tamp_us.to_be_bytes());
266 OsRng.fill_bytes(&mut bytes[8..]);
267
268 URL_SAFE_NO_PAD.encode(&bytes)
269}
270
271/// Generates a time-ordered, URL-safe short ID.
272///
273/// Creates a 14-character ID with microsecond-precision timestamp for excellent time
274/// resolution when generating IDs in rapid succession. The ID consists of:
275/// - First 8 bytes: Unix timestamp (microseconds since epoch) as big-endian u64
276/// - Next 2 bytes: Cryptographically secure random bytes
277///
278/// With microsecond precision, IDs created within the same microsecond will differ
279/// by their random component (65,536 possible values per microsecond).
280///
281/// **This function requires the `std` feature** (enabled by default).
282///
283/// # Examples
284///
285/// Basic usage:
286///
287/// ```
288/// use short_id::short_id_ordered;
289///
290/// let id = short_id_ordered();
291/// assert_eq!(id.len(), 14);
292/// ```
293///
294/// IDs from different times differ:
295///
296/// ```
297/// use short_id::short_id_ordered;
298///
299/// let id1 = short_id_ordered();
300/// std::thread::sleep(std::time::Duration::from_millis(100));
301/// let id2 = short_id_ordered();
302///
303/// // IDs generated at different times are different
304/// assert_ne!(id1, id2);
305/// ```
306///
307/// Even within the same second, IDs are unique:
308///
309/// ```
310/// use short_id::short_id_ordered;
311///
312/// let ids: Vec<String> = (0..10).map(|_| short_id_ordered()).collect();
313///
314/// // All unique due to random component
315/// for i in 0..ids.len() {
316/// for j in i+1..ids.len() {
317/// assert_ne!(ids[i], ids[j]);
318/// }
319/// }
320/// ```
321///
322/// Use for log entries:
323///
324/// ```
325/// use short_id::short_id_ordered;
326///
327/// struct LogEntry {
328/// id: String,
329/// message: String,
330/// }
331///
332/// impl LogEntry {
333/// fn new(message: String) -> Self {
334/// LogEntry {
335/// id: short_id_ordered(),
336/// message,
337/// }
338/// }
339/// }
340///
341/// let log = LogEntry::new("Started processing".to_string());
342/// assert_eq!(log.id.len(), 14);
343/// ```
344#[cfg(feature = "std")]
345pub fn short_id_ordered() -> String {
346 generate_ordered_id(10)
347}
348
349/// **Advanced:** Generates a random, URL-safe short ID with a custom number of bytes.
350///
351/// This is an advanced API that allows you to control the ID length by specifying
352/// the number of random bytes to use. The ID is encoded using URL-safe base64 without
353/// padding, so the resulting string length will be approximately `(num_bytes * 4) / 3`.
354///
355/// **For most users, [`short_id()`] is the recommended API.**
356///
357/// # Parameters
358///
359/// - `num_bytes`: Number of random bytes to generate (1 to 32 inclusive)
360///
361/// # Errors
362///
363/// Returns [`ShortIdError::ZeroBytes`] if `num_bytes` is 0, or
364/// [`ShortIdError::TooManyBytes`] if `num_bytes` exceeds 32.
365///
366/// # Security Note
367///
368/// **Using fewer bytes reduces entropy and increases collision probability.**
369/// - 10 bytes (default): ~80 bits of entropy, collision probability ~1 in 10^24
370/// - 6 bytes: ~48 bits of entropy, collision probability ~1 in 10^14
371/// - 4 bytes: ~32 bits of entropy, collision probability ~1 in 4 billion
372///
373/// Choose an appropriate size based on your uniqueness requirements and expected scale.
374///
375/// # Examples
376///
377/// Generate a standard 14-character ID (equivalent to `short_id()`):
378///
379/// ```
380/// use short_id::short_id_with_bytes;
381///
382/// let id = short_id_with_bytes(10).unwrap();
383/// assert_eq!(id.len(), 14);
384/// ```
385///
386/// Generate a shorter 8-character ID with less entropy:
387///
388/// ```
389/// use short_id::short_id_with_bytes;
390///
391/// let short_id = short_id_with_bytes(6).unwrap();
392/// assert_eq!(short_id.len(), 8);
393/// // Suitable for small-scale applications with fewer expected IDs
394/// ```
395///
396/// Generate a longer ID with more entropy:
397///
398/// ```
399/// use short_id::short_id_with_bytes;
400///
401/// let long_id = short_id_with_bytes(16).unwrap();
402/// assert_eq!(long_id.len(), 22);
403/// // Extra safety margin for high-volume applications
404/// ```
405///
406/// All IDs are URL-safe regardless of size:
407///
408/// ```
409/// use short_id::short_id_with_bytes;
410///
411/// let id = short_id_with_bytes(6).unwrap();
412/// let url = format!("https://example.com/resource/{}", id);
413/// // No encoding needed - safe to use directly
414/// ```
415pub fn short_id_with_bytes(num_bytes: usize) -> Result<String, ShortIdError> {
416 if num_bytes == 0 {
417 return Err(ShortIdError::ZeroBytes);
418 }
419 if num_bytes > MAX_BYTES {
420 return Err(ShortIdError::TooManyBytes {
421 requested: num_bytes,
422 max: MAX_BYTES,
423 });
424 }
425 Ok(generate_random_id(num_bytes))
426}
427
428/// **Advanced:** Generates a time-ordered, URL-safe short ID with a custom number of bytes.
429///
430/// This is an advanced API that allows you to control the ID length by specifying
431/// the number of bytes to use. The first 8 bytes always contain a microsecond-precision
432/// timestamp, and the remaining bytes are filled with cryptographically secure random data.
433///
434/// **For most users, [`short_id_ordered()`] is the recommended API.**
435///
436/// **This function requires the `std` feature** (enabled by default).
437///
438/// # Parameters
439///
440/// - `num_bytes`: Total number of bytes for the ID (8 to 32 inclusive, must be at least 8 for the timestamp)
441///
442/// # Errors
443///
444/// Returns [`ShortIdError::TooFewBytesForOrdered`] if `num_bytes` is less than 8, or
445/// [`ShortIdError::TooManyBytes`] if `num_bytes` exceeds 32.
446///
447/// # Security Note
448///
449/// **Using fewer random bytes (beyond the 8-byte timestamp) reduces uniqueness within the same microsecond.**
450/// - 10 bytes (default): 8 bytes timestamp + 2 bytes random (~16 bits randomness per microsecond)
451/// - 8 bytes: timestamp only, no randomness (IDs in the same microsecond will collide!)
452/// - 16 bytes: 8 bytes timestamp + 8 bytes random (~64 bits randomness per microsecond)
453///
454/// # Examples
455///
456/// Generate a standard time-ordered ID (equivalent to `short_id_ordered()`):
457///
458/// ```
459/// use short_id::short_id_ordered_with_bytes;
460///
461/// let id = short_id_ordered_with_bytes(10).unwrap();
462/// assert_eq!(id.len(), 14);
463/// ```
464///
465/// IDs from different times contain different timestamps:
466///
467/// ```
468/// use short_id::short_id_ordered_with_bytes;
469///
470/// let id1 = short_id_ordered_with_bytes(10).unwrap();
471/// std::thread::sleep(std::time::Duration::from_millis(10));
472/// let id2 = short_id_ordered_with_bytes(10).unwrap();
473///
474/// // IDs from different times are different
475/// assert_ne!(id1, id2);
476/// ```
477///
478/// Shorter time-ordered IDs with minimal randomness:
479///
480/// ```
481/// use short_id::short_id_ordered_with_bytes;
482///
483/// let id = short_id_ordered_with_bytes(8).unwrap();
484/// assert_eq!(id.len(), 11);
485/// // Warning: No random component! Only suitable if you never generate
486/// // multiple IDs within the same microsecond.
487/// ```
488///
489/// Longer time-ordered IDs with extra randomness:
490///
491/// ```
492/// use short_id::short_id_ordered_with_bytes;
493///
494/// let id = short_id_ordered_with_bytes(16).unwrap();
495/// assert_eq!(id.len(), 22);
496/// // 8 bytes random component provides excellent uniqueness
497/// // even when generating millions of IDs per second
498/// ```
499#[cfg(feature = "std")]
500pub fn short_id_ordered_with_bytes(num_bytes: usize) -> Result<String, ShortIdError> {
501 if num_bytes < 8 {
502 return Err(ShortIdError::TooFewBytesForOrdered {
503 requested: num_bytes,
504 });
505 }
506 if num_bytes > MAX_BYTES {
507 return Err(ShortIdError::TooManyBytes {
508 requested: num_bytes,
509 max: MAX_BYTES,
510 });
511 }
512 Ok(generate_ordered_id(num_bytes))
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518
519 #[test]
520 fn test_short_id_length() {
521 let id = short_id();
522 assert_eq!(id.len(), 14);
523 }
524
525 #[test]
526 fn test_short_id_unique() {
527 let id1 = short_id();
528 let id2 = short_id();
529 assert_ne!(id1, id2);
530 }
531
532 #[test]
533 fn test_short_id_url_safe() {
534 for _ in 0..100 {
535 let id = short_id();
536 assert!(!id.contains('+'));
537 assert!(!id.contains('/'));
538 assert!(!id.contains('='));
539 }
540 }
541
542 #[test]
543 fn test_many_unique_ids() {
544 // Generate many IDs and ensure all are unique
545 #[cfg(feature = "std")]
546 {
547 let ids: Vec<String> = (0..1000).map(|_| short_id()).collect();
548 let unique_count = ids.iter().collect::<std::collections::HashSet<_>>().len();
549 assert_eq!(unique_count, 1000);
550 }
551
552 #[cfg(not(feature = "std"))]
553 {
554 // In no_std, just verify a few IDs are unique
555 let id1 = short_id();
556 let id2 = short_id();
557 let id3 = short_id();
558 assert_ne!(id1, id2);
559 assert_ne!(id2, id3);
560 assert_ne!(id1, id3);
561 }
562 }
563
564 #[cfg(feature = "std")]
565 #[test]
566 fn test_short_id_ordered_length() {
567 let id = short_id_ordered();
568 assert_eq!(id.len(), 14);
569 }
570
571 #[cfg(feature = "std")]
572 #[test]
573 fn test_short_id_ordered_unique() {
574 let id1 = short_id_ordered();
575 let id2 = short_id_ordered();
576 assert_ne!(id1, id2);
577 }
578
579 #[cfg(feature = "std")]
580 #[test]
581 fn test_short_id_ordered_url_safe() {
582 for _ in 0..100 {
583 let id = short_id_ordered();
584 assert!(!id.contains('+'));
585 assert!(!id.contains('/'));
586 assert!(!id.contains('='));
587 }
588 }
589
590 // Tests for short_id_with_bytes
591
592 #[test]
593 fn test_short_id_with_bytes_standard() {
594 let id = short_id_with_bytes(10).unwrap();
595 assert_eq!(id.len(), 14);
596 }
597
598 #[test]
599 fn test_short_id_with_bytes_shorter() {
600 let id = short_id_with_bytes(6).unwrap();
601 assert_eq!(id.len(), 8);
602 }
603
604 #[test]
605 fn test_short_id_with_bytes_longer() {
606 let id = short_id_with_bytes(16).unwrap();
607 assert_eq!(id.len(), 22);
608 }
609
610 #[test]
611 fn test_short_id_with_bytes_url_safe() {
612 for num_bytes in [6, 10, 16, 32] {
613 let id = short_id_with_bytes(num_bytes).unwrap();
614 assert!(!id.contains('+'));
615 assert!(!id.contains('/'));
616 assert!(!id.contains('='));
617 }
618 }
619
620 #[test]
621 fn test_short_id_with_bytes_unique() {
622 for num_bytes in [6, 10, 16] {
623 let id1 = short_id_with_bytes(num_bytes).unwrap();
624 let id2 = short_id_with_bytes(num_bytes).unwrap();
625 assert_ne!(id1, id2);
626 }
627 }
628
629 #[test]
630 fn test_short_id_with_bytes_zero_errors() {
631 assert_eq!(short_id_with_bytes(0), Err(ShortIdError::ZeroBytes));
632 }
633
634 #[test]
635 fn test_short_id_with_bytes_too_large_errors() {
636 assert_eq!(
637 short_id_with_bytes(33),
638 Err(ShortIdError::TooManyBytes {
639 requested: 33,
640 max: MAX_BYTES
641 })
642 );
643 }
644
645 // Tests for short_id_ordered_with_bytes
646
647 #[cfg(feature = "std")]
648 #[test]
649 fn test_short_id_ordered_with_bytes_standard() {
650 let id = short_id_ordered_with_bytes(10).unwrap();
651 assert_eq!(id.len(), 14);
652 }
653
654 #[cfg(feature = "std")]
655 #[test]
656 fn test_short_id_ordered_with_bytes_minimal() {
657 let id = short_id_ordered_with_bytes(8).unwrap();
658 assert_eq!(id.len(), 11);
659 }
660
661 #[cfg(feature = "std")]
662 #[test]
663 fn test_short_id_ordered_with_bytes_longer() {
664 let id = short_id_ordered_with_bytes(16).unwrap();
665 assert_eq!(id.len(), 22);
666 }
667
668 #[cfg(feature = "std")]
669 #[test]
670 fn test_short_id_ordered_with_bytes_url_safe() {
671 for num_bytes in [8, 10, 16, 32] {
672 let id = short_id_ordered_with_bytes(num_bytes).unwrap();
673 assert!(!id.contains('+'));
674 assert!(!id.contains('/'));
675 assert!(!id.contains('='));
676 }
677 }
678
679 #[cfg(feature = "std")]
680 #[test]
681 fn test_short_id_ordered_with_bytes_unique() {
682 for num_bytes in [10, 16] {
683 let id1 = short_id_ordered_with_bytes(num_bytes).unwrap();
684 let id2 = short_id_ordered_with_bytes(num_bytes).unwrap();
685 assert_ne!(id1, id2);
686 }
687 }
688
689 #[cfg(feature = "std")]
690 #[test]
691 fn test_short_id_ordered_with_bytes_too_small_errors() {
692 assert_eq!(
693 short_id_ordered_with_bytes(7),
694 Err(ShortIdError::TooFewBytesForOrdered { requested: 7 })
695 );
696 }
697
698 #[cfg(feature = "std")]
699 #[test]
700 fn test_short_id_ordered_with_bytes_too_large_errors() {
701 assert_eq!(
702 short_id_ordered_with_bytes(33),
703 Err(ShortIdError::TooManyBytes {
704 requested: 33,
705 max: MAX_BYTES
706 })
707 );
708 }
709}
710
711/// A newtype wrapper around a short ID string.
712///
713/// Provides a typed interface for working with short IDs, with methods for
714/// generation and conversion. The inner string is always a valid 14-character
715/// URL-safe base64 identifier (characters `A-Z`, `a-z`, `0-9`, `-`, `_`).
716///
717/// # Ordering
718///
719/// `ShortId` implements [`Ord`] and [`PartialOrd`] via lexicographic string comparison.
720/// This ordering is meaningful only for IDs created with [`ShortId::ordered()`], where
721/// the timestamp prefix ensures creation-time order. For random IDs from
722/// [`ShortId::random()`], the ordering is arbitrary.
723///
724/// # Examples
725///
726/// ```
727/// use short_id::ShortId;
728///
729/// // Generate a random ID
730/// let id = ShortId::random();
731/// assert_eq!(id.as_str().len(), 14);
732///
733/// // Convert to string
734/// let s: String = id.into();
735/// ```
736#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
737pub struct ShortId(String);
738
739impl ShortId {
740 /// Creates a new random short ID.
741 ///
742 /// This is equivalent to calling [`short_id()`] but returns a typed [`ShortId`].
743 ///
744 /// # Examples
745 ///
746 /// ```
747 /// use short_id::ShortId;
748 ///
749 /// let id = ShortId::random();
750 /// assert_eq!(id.as_str().len(), 14);
751 /// ```
752 pub fn random() -> Self {
753 ShortId(short_id())
754 }
755
756 /// Creates a new time-ordered short ID.
757 ///
758 /// This is equivalent to calling [`short_id_ordered()`] but returns a typed [`ShortId`].
759 /// Requires the `std` feature (enabled by default).
760 ///
761 /// IDs generated with this method sort in creation-time order when compared with [`Ord`].
762 ///
763 /// # Examples
764 ///
765 /// ```
766 /// use short_id::ShortId;
767 ///
768 /// let id = ShortId::ordered();
769 /// assert_eq!(id.as_str().len(), 14);
770 /// ```
771 #[cfg(feature = "std")]
772 pub fn ordered() -> Self {
773 ShortId(short_id_ordered())
774 }
775
776 /// Returns the ID as a string slice.
777 ///
778 /// # Examples
779 ///
780 /// ```
781 /// use short_id::ShortId;
782 ///
783 /// let id = ShortId::random();
784 /// let s: &str = id.as_str();
785 /// assert_eq!(s.len(), 14);
786 /// ```
787 pub fn as_str(&self) -> &str {
788 &self.0
789 }
790}
791
792impl core::fmt::Display for ShortId {
793 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
794 write!(f, "{}", self.0)
795 }
796}
797
798impl AsRef<str> for ShortId {
799 fn as_ref(&self) -> &str {
800 &self.0
801 }
802}
803
804impl From<ShortId> for String {
805 fn from(id: ShortId) -> Self {
806 id.0
807 }
808}
809
810/// Attempts to wrap a `String` as a [`ShortId`].
811///
812/// Validates that the string is exactly 14 characters long and contains only
813/// URL-safe base64 characters (`A-Z`, `a-z`, `0-9`, `-`, `_`).
814///
815/// # Errors
816///
817/// Returns [`ShortIdError::InvalidString`] if validation fails.
818///
819/// # Examples
820///
821/// ```
822/// use short_id::{ShortId, ShortIdError};
823///
824/// let id = ShortId::random();
825/// let s: String = id.clone().into();
826///
827/// // A valid 14-char URL-safe string round-trips correctly
828/// let recovered: ShortId = s.try_into().unwrap();
829/// assert_eq!(id, recovered);
830///
831/// // An invalid string is rejected
832/// let result = ShortId::try_from(String::from("not-valid!!"));
833/// assert_eq!(result, Err(ShortIdError::InvalidString));
834/// ```
835impl TryFrom<String> for ShortId {
836 type Error = ShortIdError;
837
838 fn try_from(s: String) -> Result<Self, Self::Error> {
839 ShortId::try_from(s.as_str()).map(|_| ShortId(s))
840 }
841}
842
843/// Attempts to wrap a `&str` as a [`ShortId`], allocating only on success.
844///
845/// Validates that the string is exactly 14 characters long and contains only
846/// URL-safe base64 characters (`A-Z`, `a-z`, `0-9`, `-`, `_`).
847///
848/// # Errors
849///
850/// Returns [`ShortIdError::InvalidString`] if validation fails.
851///
852/// # Examples
853///
854/// ```
855/// use short_id::{ShortId, ShortIdError};
856///
857/// let id = ShortId::random();
858/// let recovered = ShortId::try_from(id.as_str()).unwrap();
859/// assert_eq!(id, recovered);
860///
861/// assert_eq!(ShortId::try_from("bad"), Err(ShortIdError::InvalidString));
862/// ```
863impl TryFrom<&str> for ShortId {
864 type Error = ShortIdError;
865
866 fn try_from(s: &str) -> Result<Self, Self::Error> {
867 if s.len() != 14
868 || !s
869 .chars()
870 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
871 {
872 return Err(ShortIdError::InvalidString);
873 }
874 Ok(ShortId(String::from(s)))
875 }
876}
877
878#[cfg(test)]
879mod shortid_tests {
880 use super::*;
881
882 #[test]
883 fn test_shortid_random_length() {
884 assert_eq!(ShortId::random().as_str().len(), 14);
885 }
886
887 #[cfg(feature = "std")]
888 #[test]
889 fn test_shortid_ordered_length() {
890 assert_eq!(ShortId::ordered().as_str().len(), 14);
891 }
892
893 #[test]
894 fn test_shortid_into_string() {
895 let id = ShortId::random();
896 let s: String = id.clone().into();
897 assert_eq!(s.len(), 14);
898 assert_eq!(s, id.as_str());
899 }
900
901 #[test]
902 fn test_shortid_try_from_valid() {
903 let id = ShortId::random();
904 let s: String = id.clone().into();
905 let recovered = ShortId::try_from(s).unwrap();
906 assert_eq!(id, recovered);
907 }
908
909 #[test]
910 fn test_shortid_try_from_str_valid() {
911 let id = ShortId::random();
912 let recovered = ShortId::try_from(id.as_str()).unwrap();
913 assert_eq!(id, recovered);
914 }
915
916 #[test]
917 fn test_shortid_try_from_str_invalid() {
918 assert_eq!(ShortId::try_from("bad"), Err(ShortIdError::InvalidString));
919 assert_eq!(
920 ShortId::try_from("invalid chars!"),
921 Err(ShortIdError::InvalidString)
922 );
923 }
924
925 #[test]
926 fn test_shortid_try_from_wrong_length() {
927 assert_eq!(
928 ShortId::try_from(String::from("short")),
929 Err(ShortIdError::InvalidString)
930 );
931 assert_eq!(
932 ShortId::try_from(String::from("this_is_way_too_long_to_be_valid")),
933 Err(ShortIdError::InvalidString)
934 );
935 }
936
937 #[test]
938 fn test_shortid_try_from_invalid_chars() {
939 // Exactly 14 chars but contains characters outside the URL-safe base64 alphabet
940 assert_eq!(
941 ShortId::try_from(String::from("invalid chars!")),
942 Err(ShortIdError::InvalidString)
943 );
944 }
945}