I recently developed my first Rust crate, a library for generating unique IDs. The idea was inspired by Twitter's snowflake and I was looking to bring a similar solution to Rust projects.
One of the key requirements I had in mind while creating this library was the ability to add a prefix to the IDs. Although unique IDs are not typically human-readable, adding a prefix can be helpful for debugging and for quickly identifying the objects associated with the IDs.
With that in mind, selecting a name for the crate was a no-brainer. I chose "puid," which stands for "prefix unique ID" :)
PUID has a similar structure to Twitter snowflake, I made up the id of the following parts:
- An 8-character prefix is chosen by the user.
impl<'a> PuidBuilder<'a> {
// [...]
pub fn prefix(self, prefix: &'a str) -> PuidResult<Self> {
if validate(prefix) {
Ok(Self { prefix, ..self })
} else {
Err(PuidError::InvalidPrefix)
}
}
}
const PREFIX_MAX_LEN: usize = 8;
const PREFIX_MIN_LEN: usize = 1;
// [...]
fn validate(prefix: &str) -> bool {
(PREFIX_MIN_LEN..=PREFIX_MAX_LEN).contains(&prefix.len())
&& prefix.chars().all(|c| c.is_ascii_alphanumeric())
}
- An underscore
_
between the prefix and the id characters.
impl<'a> PuidBuilder<'a> {
// [...]
pub fn build(self) -> PuidResult<String> {
let mut result =
String::with_capacity(self.prefix.len() + 1 + 16 + 3 + 16 + self.entropy as usize);
result.push_str(self.prefix);
result.push('_');
result.push_str(&to_base36(time()));
result.push_str(&counter().to_string());
result.push_str(&to_base36(u128::from(std::process::id())));
result.push_str(&rnd_string(self.entropy));
// [...]
}
}
- The Unix timestamp.
fn time() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis()
}
- An incrementing counter that starts again at a limit of 255.
fn counter() -> u8 {
COUNTER
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |i| match i {
i if i == u8::MAX => Some(0),
_ => Some(i + 1),
})
.unwrap()
}
- The process identifier.
std::process::id() as u128
- An alphanumeric sequence using Rand as a dependency.
fn rnd_string(elements: u8) -> String {
thread_rng()
.sample_iter(&Alphanumeric)
.take(elements as usize)
.map(char::from)
.collect()
}
Parts 3 and 5, the Unix timestamp and the process identifier. These values are converted to Base36 for compact representation. Base36 is a compact and efficient way of representing numerical values using a combination of the digits 0-9 and the letters A-Z. This makes it a good choice for situations where space is limited.
fn to_base36(mut v: u128) -> String {
// 16 characters cover most cases which is typical for base-36 encoding of a u128
let mut result = String::with_capacity(16);
while v > 0 {
result.push(
char::from_digit(
u32::try_from(v % u128::from(BASE_36)).unwrap(),
u32::from(BASE_36),
)
.unwrap(),
);
v /= u128::from(BASE_36);
}
result.chars().rev().collect()
}
The PUID crate implements a builder pattern, providing a publicly accessible builder through the Puid struct. This allows customization of the PUID output. For example:
An id with a prefix foo_
and default entropy of 12 random characters at the end.
let id = Puid::builder().prefix("foo")?.build()?;
An id with a prefix bar_
and custom entropy of 24 random characters at the end.
let id = Puid::builder().prefix("bar")?.entropy(24).build()?;
And that's a wrap! If you want to know more, just try the crate or hit up the docs and source code.