#![cfg_attr(coverage, feature(no_coverage))]
#![deny(
clippy::pedantic,
clippy::all,
missing_docs,
nonstandard_style,
rust_2018_idioms,
rust_2018_compatibility,
rust_2021_compatibility,
rustdoc::all,
clippy::cargo,
clippy::cargo_common_metadata
)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TinyIdError {
InvalidLength,
InvalidCharacters,
Conversion(String),
GenerationFailure,
}
impl std::fmt::Display for TinyIdError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TinyIdError::InvalidLength => write!(f, "Invalid length"),
TinyIdError::InvalidCharacters => write!(f, "Invalid characters"),
TinyIdError::Conversion(s) => write!(f, "Conversion error: {s}"),
TinyIdError::GenerationFailure => write!(f, "TinyId generation failed"),
}
}
}
impl From<std::array::TryFromSliceError> for TinyIdError {
fn from(err: std::array::TryFromSliceError) -> Self {
Self::Conversion(err.to_string())
}
}
impl std::error::Error for TinyIdError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TinyId {
data: [u8; 8],
}
impl TinyId {
pub const LETTER_COUNT: usize = 64;
pub const LETTERS: [u8; Self::LETTER_COUNT] = [
b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o',
b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', b'A', b'B', b'C', b'D',
b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S',
b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8',
b'9', b'0', b'_', b'-',
];
pub const NULL_CHAR: u8 = b'\0';
pub const NULL_DATA: [u8; 8] = [Self::NULL_CHAR; 8];
#[must_use]
pub const fn is_valid_byte(byte: u8) -> bool {
if byte == Self::NULL_CHAR {
return false;
}
if byte == 45u8 {
return true;
}
if byte >= 48u8 && byte <= 57u8 {
return true;
}
if byte >= 65u8 && byte <= 90u8 {
return true;
}
if byte == 95 {
return true;
}
if byte >= 97u8 && byte <= 122u8 {
return true;
}
false
}
#[must_use]
pub fn null() -> Self {
Self {
data: Self::NULL_DATA,
}
}
#[must_use]
pub fn random() -> Self {
Self::random_fastrand2()
}
#[must_use]
pub fn is_valid(self) -> bool {
!self.is_null() && self.data.iter().all(|&ch| Self::is_valid_byte(ch))
}
#[must_use]
pub fn is_null(self) -> bool {
self.data == Self::NULL_DATA
}
pub fn make_null(&mut self) {
self.data = Self::NULL_DATA;
}
fn from_str(s: &str) -> std::result::Result<Self, TinyIdError> {
use std::char::TryFromCharError;
if s.len() != 8 {
return Err(TinyIdError::InvalidLength);
}
let mut data = Self::NULL_DATA;
for (i, ch) in s.chars().enumerate() {
let byte: u8 = ch
.try_into()
.map_err(|err: TryFromCharError| TinyIdError::Conversion(err.to_string()))?;
if !Self::is_valid_byte(byte) {
return Err(TinyIdError::InvalidCharacters);
}
data[i] = byte;
}
Ok(Self { data })
}
#[must_use]
pub fn from_str_unchecked(s: &str) -> Self {
let mut data = Self::NULL_DATA;
for (i, ch) in s.bytes().enumerate() {
data[i] = ch;
}
Self { data }
}
#[must_use]
pub fn to_bytes(self) -> [u8; 8] {
self.data
}
pub fn from_u64(n: u64) -> Result<Self, TinyIdError> {
let bytes: [u8; 8] = n.to_be_bytes();
Self::from_bytes(bytes)
}
#[must_use]
pub fn from_u64_unchecked(n: u64) -> Self {
let data: [u8; 8] = n.to_be_bytes();
Self { data }
}
#[must_use]
pub fn to_u64(self) -> u64 {
u64::from_be_bytes(self.data)
}
pub fn from_bytes(bytes: [u8; 8]) -> Result<Self, TinyIdError> {
let id = Self { data: bytes };
if id.is_valid() {
Ok(id)
} else {
Err(TinyIdError::InvalidCharacters)
}
}
#[must_use]
pub fn from_bytes_unchecked(bytes: [u8; 8]) -> Self {
Self { data: bytes }
}
#[must_use]
pub fn starts_with(&self, input: &str) -> bool {
match input.len() {
0 => true,
1..=8 => {
let s = self.to_string();
s.starts_with(input)
}
_ => false,
}
}
#[must_use]
pub fn ends_with(&self, input: &str) -> bool {
match input.len() {
0 => true,
1..=8 => {
let s = self.to_string();
s.ends_with(input)
}
_ => false,
}
}
#[allow(clippy::cast_possible_truncation, unused)]
#[cfg_attr(coverage, no_coverage)]
#[must_use]
pub(crate) fn random_fastrand() -> Self {
const LETTER_COUNT_U8: u8 = TinyId::LETTER_COUNT as u8;
let mut data = Self::NULL_DATA;
for ch in &mut data {
*ch = Self::LETTERS[fastrand::u8(0..LETTER_COUNT_U8) as usize];
}
Self { data }
}
#[must_use]
pub(crate) fn random_fastrand2() -> Self {
let seed = fastrand::u64(..);
let mut data: [u8; 8] = seed.to_be_bytes();
for b in &mut data {
*b = Self::LETTERS[*b as usize % Self::LETTER_COUNT];
}
Self { data }
}
}
impl TryFrom<[u8; 8]> for TinyId {
type Error = TinyIdError;
fn try_from(value: [u8; 8]) -> std::result::Result<Self, Self::Error> {
let data = value;
if data.iter().any(|&ch| !Self::is_valid_byte(ch)) {
Err(TinyIdError::InvalidCharacters)
} else {
Ok(Self { data })
}
}
}
impl TryFrom<&[u8; 8]> for TinyId {
type Error = TinyIdError;
fn try_from(value: &[u8; 8]) -> std::result::Result<Self, Self::Error> {
let data = *value;
if data.iter().any(|&ch| !Self::is_valid_byte(ch)) {
Err(TinyIdError::InvalidCharacters)
} else {
Ok(Self { data })
}
}
}
impl<'a> TryFrom<&'a [u8]> for TinyId {
type Error = TinyIdError;
fn try_from(value: &'a [u8]) -> std::result::Result<Self, Self::Error> {
let data = <[u8; 8]>::try_from(value)?;
if data.iter().any(|&ch| !Self::is_valid_byte(ch)) {
return Err(TinyIdError::InvalidCharacters);
}
Ok(Self { data })
}
}
impl TryFrom<u64> for TinyId {
type Error = TinyIdError;
fn try_from(value: u64) -> std::result::Result<Self, Self::Error> {
Self::from_u64(value)
}
}
impl std::str::FromStr for TinyId {
type Err = TinyIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_str(s)
}
}
impl std::fmt::Display for TinyId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for ch in &self.data {
write!(f, "{}", *ch as char)?;
}
Ok(())
}
}
impl Default for TinyId {
fn default() -> Self {
Self::null()
}
}
impl PartialEq<TinyId> for [u8; 8] {
fn eq(&self, other: &TinyId) -> bool {
self == &other.data
}
}
impl PartialEq<[u8; 8]> for TinyId {
fn eq(&self, other: &[u8; 8]) -> bool {
self.data == *other
}
}
impl PartialEq<[u8; 8]> for &TinyId {
fn eq(&self, other: &[u8; 8]) -> bool {
self.data == *other
}
}
impl PartialEq<TinyId> for &[u8; 8] {
fn eq(&self, other: &TinyId) -> bool {
**self == other.data
}
}
impl PartialEq<&[u8; 8]> for TinyId {
fn eq(&self, other: &&[u8; 8]) -> bool {
self.data == **other
}
}
impl PartialEq<TinyId> for &[u8] {
fn eq(&self, other: &TinyId) -> bool {
*self == other.data.as_slice()
}
}
impl PartialEq<&[u8]> for TinyId {
fn eq(&self, other: &&[u8]) -> bool {
self.data == *other
}
}
impl PartialEq<TinyId> for Vec<u8> {
fn eq(&self, other: &TinyId) -> bool {
self == other.data.as_slice()
}
}
impl PartialEq<Vec<u8>> for TinyId {
fn eq(&self, other: &Vec<u8>) -> bool {
self.data == *other.as_slice()
}
}
impl PartialEq<TinyId> for &Vec<u8> {
fn eq(&self, other: &TinyId) -> bool {
self.as_slice() == other.data.as_slice()
}
}
impl PartialEq<&Vec<u8>> for TinyId {
fn eq(&self, other: &&Vec<u8>) -> bool {
self.data == *other.as_slice()
}
}
impl PartialEq<&TinyId> for TinyId {
fn eq(&self, other: &&TinyId) -> bool {
self.data == other.data
}
}
impl PartialEq<TinyId> for &TinyId {
fn eq(&self, other: &TinyId) -> bool {
self.data == other.data
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg_attr(coverage, no_coverage)]
fn error() {
assert_eq!(TinyIdError::InvalidLength.to_string(), "Invalid length");
assert_eq!(
TinyIdError::InvalidCharacters.to_string(),
"Invalid characters"
);
assert_eq!(
TinyIdError::Conversion("Hello".to_string()).to_string(),
"Conversion error: Hello"
);
assert_eq!(
TinyIdError::GenerationFailure.to_string(),
"TinyId generation failed"
);
}
#[test]
#[cfg_attr(coverage, no_coverage)]
fn letters() {
for letter in &TinyId::LETTERS {
assert!(
TinyId::is_valid_byte(*letter),
"{} (letter {}) failed",
*letter,
*letter as char
);
}
assert!(!TinyId::is_valid_byte(TinyId::NULL_CHAR));
}
#[test]
#[cfg_attr(coverage, no_coverage)]
fn basic_usage() {
let id = TinyId::random();
assert!(id.is_valid());
assert!(!id.is_null());
let num = id.to_u64();
let back = TinyId::from_u64(num).expect("Unable to convert back to u64");
assert_eq!(id, back);
assert_eq!(num, back.to_u64());
let bytes = id.to_bytes();
let back = TinyId::from_bytes(bytes).expect("Unable to convert back to bytes");
assert_eq!(id, back);
assert_eq!(bytes, back.to_bytes());
let bad_id = TinyId::null();
assert!(!bad_id.is_valid());
assert!(bad_id.is_null());
}
#[test]
#[cfg_attr(coverage, no_coverage)]
fn collision_test_one_million() {
use std::collections::HashSet;
let mut ids = HashSet::new();
for _ in 0..1_000_000 {
let id = TinyId::random();
assert!(ids.insert(id));
}
}
#[test]
#[cfg_attr(coverage, no_coverage)]
fn froms() {
let result = TinyId::from_bytes([b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h']);
assert!(result.is_ok());
let id = result.unwrap();
assert_eq!(id.to_string(), "abcdefgh");
let result = TinyId::from_bytes([b'!', b'b', b'c', b'd', b'e', b'f', b'g', b'h']);
assert!(result.is_err());
let result = TinyId::from_u64(7_017_280_452_245_743_464_u64);
assert!(result.is_ok());
let id = result.unwrap();
assert_eq!(id.to_string(), "abcdefgh");
let result = TinyId::from_u64(u64::MAX);
assert!(result.is_err());
let result = TinyId::try_from(7_017_280_452_245_743_464_u64);
assert!(result.is_ok());
let id = result.unwrap();
assert_eq!(id.to_string(), "abcdefgh");
let result = TinyId::try_from(u64::MAX);
assert!(result.is_err());
let result = <TinyId as std::str::FromStr>::from_str("abcdefgh");
assert!(result.is_ok());
let id = result.unwrap();
assert_eq!(id.to_string(), "abcdefgh");
let result = <TinyId as std::str::FromStr>::from_str("abcdefghijklmnopqrstuvwxyz");
assert!(result.is_err());
let result = <TinyId as std::str::FromStr>::from_str("!@#$%^&*");
assert!(result.is_err());
let result = TinyId::try_from([b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h']);
assert!(result.is_ok());
let id = result.unwrap();
assert_eq!(id.to_string(), "abcdefgh");
let result = TinyId::try_from([b'!', b'b', b'c', b'd', b'e', b'f', b'g', b'h']);
assert!(result.is_err());
let result = TinyId::try_from(&[b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h']);
assert!(result.is_ok());
let id = result.unwrap();
assert_eq!(id.to_string(), "abcdefgh");
let result = TinyId::try_from(&[b'!', b'b', b'c', b'd', b'e', b'f', b'g', b'h']);
assert!(result.is_err());
let result = TinyId::try_from(&[b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h'] as &[u8]);
assert!(result.is_ok());
let id = result.unwrap();
assert_eq!(id.to_string(), "abcdefgh");
let result = TinyId::try_from(&[b'!', b'b', b'c', b'd', b'e', b'f', b'g', b'h'] as &[u8]);
assert!(result.is_err());
let result = TinyId::try_from(&[b'!', b'b', b'c', b'd', b'e', b'f', b'g'] as &[u8]);
assert!(result.is_err());
}
#[test]
#[cfg_attr(coverage, no_coverage)]
fn bad_froms() {
let id = TinyId::from_bytes_unchecked([b'!', b'b', b'c', b'd', b'e', b'f', b'g', b'h']);
assert!(!id.is_valid());
let id = TinyId::from_u64_unchecked(u64::MAX);
assert!(!id.is_valid());
let id = TinyId::from_str_unchecked("abcdefg!");
assert!(!id.is_valid());
}
#[test]
#[should_panic]
#[cfg_attr(coverage, no_coverage)]
fn bad_froms_panic1() {
let _id = TinyId::from_str_unchecked("oopsie poopsie!");
}
#[test]
#[cfg_attr(coverage, no_coverage)]
#[allow(clippy::op_ref)]
fn eqs() {
let mut id = TinyId::from_bytes_unchecked([b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h']);
let id2 = TinyId::from_u64_unchecked(id.to_u64());
let id3 = TinyId::from_str_unchecked("abcdefgh");
assert!(id.is_valid());
assert!(id2.is_valid());
assert!(id3.is_valid());
assert!(id == id2);
assert!(&id == id);
assert!(id == &id2);
assert!(&id2 == &id3);
assert!(id2 == &id3.data);
assert!(id == id2.data);
assert!(id == id3.data.as_slice());
assert!(id2 == id3.data.as_ref());
assert!(id2 == id3.data.to_vec());
assert!(id2 == &id.data.to_vec());
assert!(id3 == id.data);
assert!(id3 == [b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h'] as [u8; 8]);
assert!(id == &[b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h'] as &[u8; 8]);
assert!(id2 == &[b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h'] as &[u8]);
assert!(&id3 == [b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h'] as [u8; 8]);
assert!(&id == &[b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h'] as &[u8; 8]);
let bytes: [u8; 8] = [b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h'];
assert!(id == bytes);
id.make_null();
assert!(!id.is_valid());
assert!(id.is_null());
assert!(id.data == TinyId::NULL_DATA);
assert!(id == TinyId::default());
}
}