const SAFE: [char; 62] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'W', 'X', 'Y', 'Z',
];
pub fn format(prefix: &str, size: usize) -> String {
format!("{}_{}", prefix, nanoid::nanoid!(size, &SAFE))
}
#[macro_export]
macro_rules! prefixid {
($prefix:expr) => {
$crate::format($prefix, 21)
};
($prefix:expr, $size:expr) => {
$crate::format($prefix, $size)
};
}
#[derive(Debug, thiserror::Error)]
pub enum CreateIdError {
#[error("invalid size")]
InvalidSize,
#[error("invalid prefix")]
InvalidPrefix,
}
#[doc(hidden)]
#[macro_export]
#[cfg(feature = "serde")]
macro_rules! impl_serde {
($name:ident) => {
impl serde::Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: &str = serde::Deserialize::deserialize(deserializer)?;
let id = s.parse().map_err(serde::de::Error::custom)?;
Ok(id)
}
}
};
}
#[doc(hidden)]
#[cfg(not(feature = "serde"))]
#[macro_export]
macro_rules! impl_serde {
($name:ident) => {};
}
#[cfg(feature = "diesel")]
#[macro_export]
macro_rules! __impl_diesel {
($name:ident) => {
impl<B> diesel::serialize::ToSql<diesel::sql_types::Text, B> for $name
where
B: diesel::backend::Backend,
str: diesel::serialize::ToSql<diesel::sql_types::Text, B>,
{
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, B>,
) -> diesel::serialize::Result {
(self as &str).to_sql(out)
}
}
impl<B> diesel::deserialize::FromSql<diesel::sql_types::Text, B> for $name
where
B: diesel::backend::Backend,
String: diesel::deserialize::FromSql<diesel::sql_types::Text, B>,
{
fn from_sql(
bytes: <B as diesel::backend::Backend>::RawValue<'_>,
) -> diesel::deserialize::Result<Self> {
let s: String =
diesel::deserialize::FromSql::<diesel::sql_types::Text, B>::from_sql(bytes)?;
s.parse::<$name>().map_err(|err| err.into())
}
}
};
}
#[doc(hidden)]
#[macro_export]
#[cfg(not(feature = "diesel"))]
macro_rules! __impl_diesel {
($name:ident) => {};
}
#[doc(hidden)]
#[macro_export]
#[cfg(feature = "diesel")]
macro_rules! __impl_diesel_macros {
($name:ident) => {
#[derive(diesel::AsExpression, diesel::FromSqlRow, Clone, PartialEq, Eq, Hash)]
#[diesel(sql_type = diesel::sql_types::Text)]
pub struct $name(smol_str::SmolStr);
};
}
#[doc(hidden)]
#[macro_export]
#[cfg(not(feature = "diesel"))]
macro_rules! __impl_diesel_macros {
($name:ident) => {
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct $name(smol_str::SmolStr);
};
}
#[macro_export]
macro_rules! create_id {
($name:ident, $prefix:expr) => {
create_id!($name, $prefix, 21);
};
($name:ident, $prefix:expr, $size:expr) => {
$crate::__impl_diesel_macros!($name);
impl $name {
pub fn new() -> Self {
Self(smol_str::SmolStr::new(&crate::prefixid!($prefix, $size)))
}
#[inline]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl ::core::convert::Into<String> for $name {
fn into(self) -> String {
self.0.into()
}
}
impl std::str::FromStr for $name {
type Err = crate::CreateIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let total_len = $size + $prefix.len() + 1;
if s.len() != total_len {
return Err(crate::CreateIdError::InvalidSize);
}
if !s.starts_with($prefix) {
return Err(crate::CreateIdError::InvalidPrefix);
}
Ok(Self(smol_str::SmolStr::new(s)))
}
}
impl Default for $name {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::fmt::Debug for $name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl ::core::ops::Deref for $name {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0.as_str()
}
}
$crate::impl_serde!($name);
$crate::__impl_diesel!($name);
};
}
#[cfg(test)]
mod test_macros {
use super::*;
#[test]
fn simple() {
let id: String = prefixid!("im");
assert_eq!(id.len(), 24);
assert_eq!(&id[..3], "im_");
}
#[test]
fn size() {
let id: String = prefixid!("im", 10);
assert_eq!(id.len(), 13);
assert_eq!(&id[..3], "im_");
}
#[test]
fn simple_with_prefix() {
create_id!(ImId, "im");
let id = ImId::new();
assert_eq!(id.len(), 24);
assert_eq!(&id[..3], "im_");
}
#[test]
fn simple_with_prefix_and_size() {
create_id!(ImId, "im", 10);
let id = ImId::new();
assert_eq!(id.len(), 13);
assert_eq!(&id[..3], "im_");
}
#[test]
#[should_panic]
fn throw_invalid_size() {
create_id!(ImId, "im", 10);
let id: ImId = "im_123456789".parse().unwrap();
}
#[test]
#[should_panic]
fn throw_invalid_prefix() {
create_id!(ImId, "im", 10);
let id: ImId = "in_1234567890".parse().unwrap();
}
#[test]
fn test_as_str() {
create_id!(ImId, "im", 10);
let id = ImId::new();
assert_eq!(id.as_str(), &id[..]);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_serialize() {
create_id!(ImId, "im", 10);
let id = ImId::new();
let serialized = serde_json::to_string(&id).unwrap();
assert_eq!(serialized, format!("\"{}\"", id));
}
#[test]
#[cfg(feature = "serde")]
fn test_serde_deserialize() {
create_id!(ImId, "im", 10);
let as_str = "\"im_1234567890\"";
let id: ImId = serde_json::from_str(as_str).unwrap();
assert_eq!(id.as_str(), "im_1234567890");
}
}