use std::fmt;
use std::hash::Hash;
use std::str::FromStr;
use super::{DbValue, FromDb, ToDbValue};
#[diagnostic::on_unimplemented(
message = "`{Self}` can't be a model's primary key",
label = "the `id` field's type",
note = "use `i64`, `renox::db::Ulid`, `uuid::Uuid` (renox's `uuid` feature) or `String`"
)]
pub trait ModelKey:
sealed::Sealed
+ ToDbValue
+ FromDb
+ Clone
+ Eq
+ Ord
+ Hash
+ fmt::Debug
+ fmt::Display
+ FromStr
+ Default
+ Send
+ Sync
+ Unpin
+ 'static
{
fn is_unsaved(&self) -> bool;
fn generate() -> Option<Self>;
const AUTO_INCREMENT: bool = false;
}
mod sealed {
pub trait Sealed {}
impl Sealed for i64 {}
impl Sealed for String {}
impl Sealed for super::Ulid {}
#[cfg(feature = "uuid")]
impl Sealed for uuid::Uuid {}
}
impl ModelKey for i64 {
const AUTO_INCREMENT: bool = true;
fn is_unsaved(&self) -> bool {
*self == 0
}
fn generate() -> Option<Self> {
None
}
}
impl ModelKey for String {
fn is_unsaved(&self) -> bool {
self.is_empty()
}
fn generate() -> Option<Self> {
None
}
}
#[cfg(feature = "uuid")]
impl ModelKey for uuid::Uuid {
fn is_unsaved(&self) -> bool {
self.is_nil()
}
fn generate() -> Option<Self> {
Some(uuid::Uuid::now_v7())
}
}
impl ModelKey for Ulid {
fn is_unsaved(&self) -> bool {
self.0.is_empty()
}
fn generate() -> Option<Self> {
Some(Ulid::new())
}
}
#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Ulid(String);
const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
impl Ulid {
pub fn new() -> Self {
static LAST: std::sync::Mutex<(u128, u128)> = std::sync::Mutex::new((0, 0));
const RANDOM_BITS: u128 = (1 << 80) - 1;
let now = u128::try_from(crate::clock::unix_millis()).unwrap_or(0) & ((1 << 48) - 1);
let mut last = LAST.lock().unwrap_or_else(|e| e.into_inner());
let (millis, random) = if now <= last.0 && last.1 < RANDOM_BITS {
(last.0, last.1 + 1)
} else {
let mut bytes = [0u8; 10];
rand::fill(&mut bytes);
let random = bytes
.iter()
.fold(0u128, |acc, byte| (acc << 8) | u128::from(*byte));
(now.max(last.0), random)
};
*last = (millis, random);
drop(last);
let value = (millis << 80) | random;
let text = (0..26)
.rev()
.map(|i| CROCKFORD[((value >> (i * 5)) & 31) as usize] as char)
.collect();
Self(text)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct InvalidUlid;
impl fmt::Display for InvalidUlid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("not a ULID")
}
}
impl std::error::Error for InvalidUlid {}
impl FromStr for Ulid {
type Err = InvalidUlid;
fn from_str(text: &str) -> Result<Self, Self::Err> {
if text.is_empty() {
return Ok(Self::default());
}
let upper = text.to_ascii_uppercase();
let valid = upper.len() == 26
&& upper.as_bytes()[0] <= b'7'
&& upper.bytes().all(|b| CROCKFORD.contains(&b));
if valid {
Ok(Self(upper))
} else {
Err(InvalidUlid)
}
}
}
impl fmt::Display for Ulid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Debug for Ulid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Ulid({})", self.0)
}
}
impl serde::Serialize for Ulid {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de> serde::Deserialize<'de> for Ulid {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
impl ToDbValue for Ulid {
fn to_db_value(&self) -> DbValue {
DbValue::Text(self.0.clone())
}
}
crate::__db_text_type_for!(Ulid, sqlx::sqlite::Sqlite);
#[cfg(feature = "postgres")]
crate::__db_text_type_for!(Ulid, sqlx::postgres::Postgres);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ulids_are_26_sortable_characters() {
let first = Ulid::new();
assert_eq!(first.as_str().len(), 26);
assert!(first.as_str().bytes().all(|b| CROCKFORD.contains(&b)));
let second = Ulid::new(); assert!(second > first, "{second} after {first}");
let many: Vec<Ulid> = (0..1000).map(|_| Ulid::new()).collect();
assert!(many.windows(2).all(|w| w[0] < w[1]), "monotonic");
assert_eq!(first.to_string().parse::<Ulid>().unwrap(), first);
assert_eq!(
first.as_str().to_lowercase().parse::<Ulid>().unwrap(),
first
);
assert!("not-a-ulid".parse::<Ulid>().is_err());
assert!(
"81J9Z3ABCDEFGHJKMNPQRSTVWX".parse::<Ulid>().is_err(),
"past 2^48 ms"
);
assert!(Ulid::default().is_unsaved());
assert!(!first.is_unsaved());
}
#[test]
fn empty_text_is_the_unsaved_ulid() {
let empty: Ulid = "".parse().unwrap();
assert_eq!(empty, Ulid::default());
let id: Ulid = "01arz3ndektsv4rrffq69g5fav".parse().unwrap();
assert_eq!(id.to_string(), "01ARZ3NDEKTSV4RRFFQ69G5FAV");
}
}