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}