Skip to main content

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(&timestamp_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}