Skip to main content

scru128/
lib.rs

1//! # SCRU128: Sortable, Clock and Random number-based Unique identifier
2//!
3//! SCRU128 ID is yet another attempt to supersede [UUID] for the users who need
4//! decentralized, globally unique time-ordered identifiers. SCRU128 is inspired by
5//! [ULID] and [KSUID] and has the following features:
6//!
7//! - 128-bit unsigned integer type
8//! - Sortable by generation time (as integer and as text)
9//! - 25-digit case-insensitive textual representation (Base36)
10//! - 48-bit millisecond Unix timestamp that ensures useful life until year 10889
11//! - Up to 281 trillion time-ordered but unpredictable unique IDs per millisecond
12//! - 80-bit three-layer randomness for global uniqueness
13//!
14//! ```rust
15//! # #[cfg(feature = "global_gen")]
16//! # {
17//! // generate a new identifier object
18//! let x = scru128::new();
19//! println!("{}", x); // e.g., "036z951mhjikzik2gsl81gr7l"
20//! println!("{}", x.to_u128()); // as a 128-bit unsigned integer
21//!
22//! // generate a textual representation directly
23//! println!("{}", scru128::new_string()); // e.g., "036z951mhzx67t63mq9xe6q0j"
24//!
25//! # }
26//! ```
27//!
28//! See [SCRU128 Specification] for details.
29//!
30//! [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier
31//! [ULID]: https://github.com/ulid/spec
32//! [KSUID]: https://github.com/segmentio/ksuid
33//! [SCRU128 Specification]: https://github.com/scru128/spec
34//!
35//! ## Crate features
36//!
37//! Default features:
38//!
39//! - `std` enables, among others, the default timestamp source for [`Generator`]
40//!   using [`std::time`]. Without `std`, users must provide their own time source
41//!   implementing the [`TimeSource`](generator::TimeSource) trait.
42//! - `global_gen` (implies `std`) provides the process-wide default SCRU128
43//!   generator and enables the [`new()`] and [`new_string()`] functions.
44//!
45//! Optional features:
46//!
47//! - `serde` enables serialization/deserialization of [`Id`] via serde.
48//! - `rand010` enables an adapter for `rand::Rng` to use `rand` (v0.10) and any
49//!   other conforming random number generators with [`Generator`].
50
51#![cfg_attr(not(feature = "std"), no_std)]
52#![cfg_attr(docsrs, feature(doc_cfg))]
53
54mod global_gen;
55#[cfg(feature = "global_gen")]
56pub use global_gen::{new, new_string};
57
58pub mod id;
59pub use id::Id;
60
61pub mod generator;
62pub use generator::Generator;
63
64/// The maximum value of 48-bit `timestamp` field.
65const MAX_TIMESTAMP: u64 = 0xffff_ffff_ffff;
66
67/// The maximum value of 24-bit `counter_hi` field.
68const MAX_COUNTER_HI: u32 = 0xff_ffff;
69
70/// The maximum value of 24-bit `counter_lo` field.
71const MAX_COUNTER_LO: u32 = 0xff_ffff;
72
73#[cfg(all(test, feature = "std"))]
74mod tests {
75    use std::{collections, sync, time};
76
77    use crate::{Generator, Id};
78
79    static SAMPLES: sync::LazyLock<Vec<String>> = sync::LazyLock::new(|| {
80        Generator::for_testing()
81            .iter()
82            .map(String::from)
83            .take(100_000)
84            .collect()
85    });
86
87    /// Generates 25-digit canonical string
88    #[test]
89    fn generates_25_digit_canonical_string() {
90        let re = regex::Regex::new(r"^[0-9a-z]{25}$").unwrap();
91        for e in &SAMPLES[..] {
92            assert!(re.is_match(e));
93        }
94    }
95
96    /// Generates 100k identifiers without collision
97    #[test]
98    fn generates_100k_identifiers_without_collision() {
99        let s: collections::HashSet<&String> = SAMPLES.iter().collect();
100        assert_eq!(s.len(), SAMPLES.len());
101    }
102
103    /// Generates sortable string representation by creation time
104    #[test]
105    fn generates_sortable_string_representation_by_creation_time() {
106        for i in 1..SAMPLES.len() {
107            assert!(SAMPLES[i - 1] < SAMPLES[i]);
108        }
109    }
110
111    /// Encodes up-to-date timestamp
112    #[test]
113    fn encodes_up_to_date_timestamp() {
114        let mut g = Generator::for_testing();
115        for _ in 0..10_000 {
116            let ts_now = (time::SystemTime::now()
117                .duration_since(time::UNIX_EPOCH)
118                .expect("clock may have gone backwards")
119                .as_millis()) as i64;
120            let timestamp = g.generate().timestamp() as i64;
121            assert!((ts_now - timestamp).abs() < 16);
122        }
123    }
124
125    /// Encodes unique sortable tuple of timestamp and counters
126    #[test]
127    fn encodes_unique_sortable_tuple_of_timestamp_and_counters() {
128        let mut prev = SAMPLES[0].parse::<Id>().unwrap();
129        for e in &SAMPLES[1..] {
130            let curr = e.parse::<Id>().unwrap();
131            assert!(
132                prev.timestamp() < curr.timestamp()
133                    || (prev.timestamp() == curr.timestamp()
134                        && prev.counter_hi() < curr.counter_hi())
135                    || (prev.timestamp() == curr.timestamp()
136                        && prev.counter_hi() == curr.counter_hi()
137                        && prev.counter_lo() < curr.counter_lo())
138            );
139            prev = curr;
140        }
141    }
142}