Skip to main content

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}