crawlnicle/src/uuid.rs

122 lines
2.8 KiB
Rust

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<Uuid> 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<Base62Uuid> for String {
fn from(s: Base62Uuid) -> Self {
base62_encode(s.0.as_u128())
}
}
fn uuid_to_base62_str<S>(uuid: &Uuid, s: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
s.serialize_str(&base62_encode(uuid.as_u128()))
}
fn uuid_from_base62_str<'de, D>(deserializer: D) -> Result<Uuid, D::Error>
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);
}
}
}