upid/lib.rs
1//! # upid
2//!
3//! Rust implementation of UPID, an alternative to UUID and ULID
4//! that includes a useful four-character prefix.
5//!
6//! It is still stored as a `u128` binary, is still sortable by date,
7//! and has 64 bits of randomness. It uses a modified form of
8//! Crockford's base32 alphabet that uses lower-case and prioritises
9//! letters so that any four-letter alpha prefix can be specified.
10//!
11//! ## Quickstart
12//!
13//! ```rust
14//! use upid::Upid;
15//! let upid = Upid::new("user");
16//!
17//! let text = upid.to_string();
18//!
19//! let same = Upid::from_string(&text);
20//! assert_eq!(upid, same.unwrap());
21//! ```
22//!
23//! If an invalid prefix is specified, it will be handled as follows:
24//! - invalid letters (not in the [`ENCODE`] alphabet) replaced by 'z'
25//! - too short will be right-padded with 'z'
26//! - too long will be clipped to four characters
27//! ```rust
28//! use upid::Upid;
29//! let upid = Upid::new("00");
30//! assert_eq!(upid.prefix(), "zzzz");
31//! ```
32
33// The code below is derived from the following:
34// https://github.com/dylanhart/ulid-rs
35
36mod b32;
37#[cfg(feature = "uuid")]
38mod uuid;
39
40pub use crate::b32::{DecodeError, ENCODE};
41
42use std::fmt;
43use std::str::FromStr;
44use std::time::{Duration, SystemTime};
45
46use rand::RngExt;
47
48const VERSION: &str = "a";
49
50fn now() -> std::time::SystemTime {
51 std::time::SystemTime::now()
52}
53
54/// A Upid is a unique 128-bit identifier is sortable and has a useful prefix.
55///
56/// It is encoded as a 26 character string using a custom base32 alphabet based
57/// on Crockford's, but with lower-case and prioritising letters over numerals.
58/// In the binary, the first 40 bits are a unix timestamp with 256ms precision,
59/// the next 64 are random bits, and the last 24 are the prefix and version identifier.
60#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy)]
61pub struct Upid(pub u128);
62
63impl Upid {
64 /// Creates a new Upid with the provided prefix and current time (UTC)
65 ///
66 /// The prefix should only contain lower-case latin alphabet characters.
67 /// # Example
68 /// ```rust
69 /// use upid::Upid;
70 ///
71 /// let my_upid = Upid::new("user");
72 /// ```
73 pub fn new(prefix: &str) -> Upid {
74 Upid::from_prefix(prefix)
75 }
76
77 /// Creates a Upid with the provided prefix and current time (UTC)
78 ///
79 /// The prefix should contain four lower-case latin alphabet characters.
80 /// # Example
81 /// ```rust
82 /// use upid::Upid;
83 ///
84 /// let my_upid = Upid::from_prefix("user");
85 /// ```
86 pub fn from_prefix(prefix: &str) -> Upid {
87 Upid::from_prefix_and_datetime(prefix, now())
88 }
89
90 /// Creates a new Upid with the given prefix and datetime
91 ///
92 /// The prefix should only contain lower-case latin alphabet characters.
93 ///
94 /// This will take the maximum of the `[SystemTime]` argument and `[SystemTime::UNIX_EPOCH]`
95 /// as earlier times are not valid for a Upid timestamp
96 ///
97 /// # Example
98 /// ```rust
99 /// use std::time::{SystemTime, Duration};
100 /// use upid::Upid;
101 ///
102 /// let upid = Upid::from_prefix_and_datetime("user", SystemTime::now());
103 /// ```
104 pub fn from_prefix_and_datetime(prefix: &str, datetime: SystemTime) -> Upid {
105 let milliseconds = datetime
106 .duration_since(SystemTime::UNIX_EPOCH)
107 .unwrap_or(Duration::ZERO)
108 .as_millis();
109 Upid::from_prefix_and_milliseconds(prefix, milliseconds)
110 }
111
112 /// Creates a new Upid with the given prefix and timestamp in millisecons
113 ///
114 /// The prefix should only contain lower-case latin alphabet characters.
115 ///
116 /// # Example
117 /// ```rust
118 /// use upid::Upid;
119 ///
120 /// let ms: u128 = 1720568902000;
121 /// let upid = Upid::from_prefix_and_milliseconds("user", ms);
122 /// ```
123 pub fn from_prefix_and_milliseconds(prefix: &str, milliseconds: u128) -> Upid {
124 // cut off the 8 lsb drops precision to 256 ms
125 // future version could play with this differently
126 // eg drop 4 bits on each side
127 let time_bits = milliseconds >> 8;
128
129 // get 64 bits of randomness on lsb side of a u128
130 let mut source = rand::rng();
131 let random = source.random::<u64>() as u128;
132
133 // pad with 'z' if shorter than 4, cut to 4 if longer
134 let prefix = format!("{:z<4}", prefix);
135 let prefix: String = prefix.chars().take(4).collect();
136 let prefix = format!("{}{}", prefix, VERSION);
137
138 // decode_prefix Errors if the last character is past 'j' in the b32 alphabet
139 // and we control that with the VERSION variable
140 // If the prefix has characters from outside the alphabet, they will be wrapped into 'z's
141 // And we have ensured above that it is exactly 5 characters long
142 let p = b32::decode_prefix(prefix.as_bytes())
143 .expect("decode_prefix failed with version character overflow");
144
145 let res = (time_bits << 88)
146 | (random << 24)
147 | ((p[0] as u128) << 16)
148 | ((p[1] as u128) << 8)
149 | p[2] as u128;
150
151 Upid(res)
152 }
153
154 /// Creates a Upid from a Base32 encoded string
155 ///
156 /// # Example
157 /// ```rust
158 /// use upid::Upid;
159 ///
160 /// let text = "user_aaccvpp5guht4dts56je5a";
161 /// let result = Upid::from_string(text);
162 ///
163 /// assert_eq!(&result.unwrap().to_string(), text);
164 /// ```
165 pub fn from_string(encoded: &str) -> Result<Upid, DecodeError> {
166 match b32::decode(encoded) {
167 Ok(int_val) => Ok(Upid(int_val)),
168 Err(err) => Err(err),
169 }
170 }
171
172 /// Gets the datetime of when this Upid was created accurate to around 256ms
173 ///
174 /// # Example
175 /// ```rust
176 /// use std::time::{SystemTime, Duration};
177 /// use upid::Upid;
178 ///
179 /// let dt = SystemTime::now();
180 /// let upid = Upid::from_prefix_and_datetime("user", dt);
181 ///
182 /// assert!(dt + Duration::from_millis(257) >= upid.datetime());
183 /// ```
184 pub fn datetime(&self) -> SystemTime {
185 let stamp = self.milliseconds();
186 SystemTime::UNIX_EPOCH + Duration::from_millis(stamp)
187 }
188
189 /// Gets the prefix of this upid
190 ///
191 /// # Example
192 /// ```rust
193 /// use upid::Upid;
194 ///
195 /// let prefix = "user";
196 /// let upid = Upid::from_prefix(prefix);
197 ///
198 /// assert_eq!(upid.prefix(), prefix);
199 /// ```
200 pub fn prefix(&self) -> String {
201 let bytes: [u8; 16] = self.0.to_be_bytes();
202 let (prefix, _) = b32::encode_prefix(&bytes[b32::END_RANDO_BIN..]);
203 prefix
204 }
205
206 /// Gets the timestamp section of this upid
207 ///
208 /// # Example
209 /// ```rust
210 /// use upid::Upid;
211 ///
212 /// let ms: u128 = 1720568902000;
213 /// let upid = Upid::from_prefix_and_milliseconds("user", ms);
214 ///
215 /// assert!(ms - u128::from(upid.milliseconds()) < 257);
216 /// ```
217 pub const fn milliseconds(&self) -> u64 {
218 ((self.0 >> 88) << 8) as u64
219 }
220
221 /// Creates a Base32 encoded string that represents this Upid
222 ///
223 /// # Example
224 /// ```rust
225 /// use upid::Upid;
226 ///
227 /// let text = "user_aaccvpp5guht4dts56je5a";
228 /// let upid = Upid::from_string(text).unwrap();
229 ///
230 /// assert_eq!(&upid.to_string(), text);
231 /// ```
232 #[allow(clippy::inherent_to_string_shadow_display)] // Significantly faster than Display::to_string
233 pub fn to_string(&self) -> String {
234 b32::encode(self.0)
235 }
236
237 /// Creates a Upid using the provided bytes array.
238 ///
239 /// # Example
240 /// ```rust
241 /// use upid::Upid;
242 /// let bytes = [0xFF; 16];
243 ///
244 /// let upid = Upid::from_bytes(bytes);
245 /// ```
246 pub const fn from_bytes(bytes: [u8; 16]) -> Upid {
247 Self(u128::from_be_bytes(bytes))
248 }
249
250 /// Returns the bytes of the Upid in big-endian order.
251 ///
252 /// # Example
253 /// ```rust
254 /// use upid::Upid;
255 ///
256 /// let text = "user_aaccvpp5guht4dts56je5a";
257 /// let upid = Upid::from_string(text).unwrap();
258 /// ```
259 pub const fn to_bytes(&self) -> [u8; 16] {
260 self.0.to_be_bytes()
261 }
262}
263
264impl Default for Upid {
265 fn default() -> Self {
266 Upid::new("")
267 }
268}
269
270impl From<Upid> for String {
271 fn from(upid: Upid) -> String {
272 upid.to_string()
273 }
274}
275
276impl From<u128> for Upid {
277 fn from(value: u128) -> Upid {
278 Upid(value)
279 }
280}
281
282impl From<Upid> for u128 {
283 fn from(upid: Upid) -> u128 {
284 upid.0
285 }
286}
287
288impl FromStr for Upid {
289 type Err = DecodeError;
290
291 fn from_str(s: &str) -> Result<Self, Self::Err> {
292 Upid::from_string(s)
293 }
294}
295
296impl fmt::Display for Upid {
297 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
298 write!(f, "{}", self.to_string())
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 const EPS: u128 = 257;
307
308 #[test]
309 fn can_into_thing() {
310 let want = Upid::from_str("user_aaccvpp5guht4dts56je5a").unwrap();
311 let s: String = want.into();
312 let u: u128 = want.into();
313
314 assert_eq!(Upid::from_str(&s).unwrap(), want);
315 assert_eq!(Upid::from(u), want);
316 }
317
318 #[test]
319 fn can_display_things() {
320 println!("{}", DecodeError::InvalidLength);
321 println!("{}", DecodeError::InvalidChar);
322 }
323
324 #[test]
325 fn test_dynamic() {
326 let upid = Upid::new("user");
327 let encoded = upid.to_string();
328 let upid2 = Upid::from_string(&encoded).expect("failed to deserialize");
329 assert_eq!(upid, upid2);
330 }
331
332 #[test]
333 fn test_order() {
334 let dt = SystemTime::now();
335 let upid1 = Upid::from_prefix_and_datetime("user", dt);
336 let upid2 = Upid::from_prefix_and_datetime("user", dt + Duration::from_millis(EPS as u64));
337 assert!(upid1 < upid2);
338 }
339
340 #[test]
341 fn test_timestamp() {
342 let dt = SystemTime::now();
343 let want = dt
344 .duration_since(SystemTime::UNIX_EPOCH)
345 .unwrap()
346 .as_millis();
347 let upid = Upid::from_prefix_and_milliseconds("user", want);
348 let got = u128::from(upid.milliseconds());
349
350 assert!(want - got < EPS);
351 }
352
353 #[test]
354 fn test_datetime() {
355 let dt = SystemTime::now();
356 let upid = Upid::from_prefix_and_datetime("user", dt);
357
358 assert!(upid.datetime() <= dt);
359 assert!(upid.datetime() + Duration::from_millis(EPS as u64) >= dt);
360 }
361
362 #[test]
363 fn test_invalid_prefix() {
364 // Invalid characters just become 'zzzz'
365 let want = "zzzz";
366
367 // even if too long
368 let got = Upid::from_prefix("[0#/]]1,").prefix();
369 assert_eq!(got, want);
370
371 // or too short
372 let got = Upid::from_prefix("[0").prefix();
373 assert_eq!(got, want);
374 }
375}