use std::fmt::{self, Display, Formatter}; use serde::{Deserialize, Serialize}; use uuid::Uuid; const BASE62_CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; /// A wrapper around a UUID (from `uuid::Uuid`) that serializes to a Base62 string. /// /// Database rows have a UUID primary key, but they are encoded in Base62 to be shorter and more /// URL-friendly for the frontend. #[derive(Debug, Serialize, Deserialize, Clone, Copy)] pub struct Base62Uuid( #[serde(deserialize_with = "uuid_from_base62_str")] #[serde(serialize_with = "uuid_to_base62_str")] Uuid, ); impl Base62Uuid { pub fn as_uuid(&self) -> Uuid { self.0 } pub fn new() -> Self { Self(Uuid::new_v4()) } } impl Default for Base62Uuid { fn default() -> Self { Self::new() } } impl From for Base62Uuid { fn from(uuid: Uuid) -> Self { Self(uuid) } } impl Display for Base62Uuid { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", base62_encode(self.0.as_u128())) } } impl From<&str> for Base62Uuid { fn from(s: &str) -> Self { Self(Uuid::from_u128(base62_decode(s))) } } impl From for String { fn from(s: Base62Uuid) -> Self { base62_encode(s.0.as_u128()) } } fn uuid_to_base62_str(uuid: &Uuid, s: S) -> Result where S: serde::Serializer, { s.serialize_str(&base62_encode(uuid.as_u128())) } fn uuid_from_base62_str<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; Ok(Uuid::from_u128(base62_decode(&s))) } pub fn base62_encode(mut number: u128) -> String { let base = BASE62_CHARS.len() as u128; let mut encoded = Vec::new(); while number > 0 { let remainder = (number % base) as usize; number /= base; encoded.push(BASE62_CHARS[remainder]); } encoded.reverse(); String::from_utf8(encoded).unwrap() } pub fn base62_decode(input: &str) -> u128 { let base = BASE62_CHARS.len() as u128; let mut number = 0u128; for &byte in input.as_bytes() { number = number * base + (BASE62_CHARS.iter().position(|&ch| ch == byte).unwrap() as u128); } number } #[cfg(test)] mod tests { use uuid::Uuid; use super::*; #[test] fn test_encode_decode() { let original_uuids = [ Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4(), ]; for original_uuid in original_uuids.iter() { let encoded = base62_encode(original_uuid.as_u128()); let decoded_uuid = Uuid::from_u128(base62_decode(&encoded)); assert_eq!(*original_uuid, decoded_uuid); } } }