#![deny(unsafe_code)]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub use uuid::Uuid;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdError {
Empty(&'static str),
NotAUuid(&'static str),
Reserved(&'static str),
}
impl core::fmt::Display for IdError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
IdError::Empty(kind) => write!(f, "{kind} must be non-empty"),
IdError::NotAUuid(kind) => write!(f, "{kind} must be a valid Uuid"),
IdError::Reserved(kind) => write!(f, "{kind} matches reserved system identifier"),
}
}
}
impl std::error::Error for IdError {}
pub fn mint_v4<R: axess_rng::SecureRng>(rng: &R) -> Uuid {
let mut bytes = [0u8; 16];
rng.fill_bytes(&mut bytes);
uuid::Builder::from_random_bytes(bytes).into_uuid()
}
type RngFiller = Box<dyn FnMut(&mut [u8])>;
std::thread_local! {
static THREAD_LOCAL_RNG: std::cell::RefCell<Option<RngFiller>>
= const { std::cell::RefCell::new(None) };
}
pub fn mint_v4_default() -> Uuid {
let mut bytes = [0u8; 16];
THREAD_LOCAL_RNG.with(|cell| {
let mut opt = cell.borrow_mut();
if let Some(filler) = opt.as_mut() {
filler(&mut bytes);
} else {
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
use axess_rng::SecureRng;
axess_rng::SystemRng.fill_bytes(&mut bytes);
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
panic!(
"mint_v4_default() on wasm32-unknown-unknown requires a thread-local RNG \
override (see with_thread_local_rng); the OS RNG is not available on that target."
);
}
}
});
uuid::Builder::from_random_bytes(bytes).into_uuid()
}
pub fn with_thread_local_rng<R, F, T>(rng: R, f: F) -> T
where
R: axess_rng::SecureRng,
F: FnOnce() -> T,
{
struct Guard(Option<RngFiller>);
impl Drop for Guard {
fn drop(&mut self) {
THREAD_LOCAL_RNG.with(|cell| {
*cell.borrow_mut() = self.0.take();
});
}
}
let prev = THREAD_LOCAL_RNG.with(|cell| {
cell.borrow_mut().replace(Box::new(move |dest: &mut [u8]| {
rng.fill_bytes(dest);
}))
});
let restore_guard = Guard(prev);
let result = f();
drop(restore_guard);
result
}
#[macro_export]
macro_rules! define_id {
($(#[$meta:meta])* pub $name:ident) => {
$(#[$meta])*
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(feature = "serde", serde(transparent))]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name($crate::Uuid);
impl $name {
pub const NIL: Self = Self($crate::Uuid::nil());
#[inline]
pub fn new<R: axess_rng::SecureRng>(rng: &R) -> Self {
Self($crate::mint_v4(rng))
}
#[doc = concat!(
"Construct from a string (which must be a valid hyphenated Uuid). ",
"Returns [`IdError::Empty`] for empty input or ",
"[`IdError::NotAUuid`] if parsing fails.",
)]
pub fn try_new(value: impl AsRef<str>) -> ::std::result::Result<Self, $crate::IdError> {
let s = value.as_ref();
if s.is_empty() {
return Err($crate::IdError::Empty(stringify!($name)));
}
$crate::Uuid::parse_str(s)
.map(Self)
.map_err(|_| $crate::IdError::NotAUuid(stringify!($name)))
}
#[inline]
pub const fn from_uuid(uuid: $crate::Uuid) -> Self {
Self(uuid)
}
#[inline]
pub const fn from_bytes(bytes: [u8; 16]) -> Self {
Self($crate::Uuid::from_bytes(bytes))
}
#[inline]
pub fn from_random_bytes(bytes: [u8; 16]) -> Self {
Self(uuid::Builder::from_random_bytes(bytes).into_uuid())
}
#[inline]
pub fn from_namespaced_str(namespace: $crate::Uuid, name: &str) -> Self {
Self($crate::Uuid::new_v5(&namespace, name.as_bytes()))
}
#[inline]
pub const fn as_bytes(&self) -> &[u8; 16] {
self.0.as_bytes()
}
#[inline]
pub const fn as_uuid(&self) -> $crate::Uuid {
self.0
}
#[inline]
pub fn is_nil(&self) -> bool {
self.0.is_nil()
}
}
impl ::std::fmt::Display for $name {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
::std::fmt::Display::fmt(&self.0, f)
}
}
impl ::std::convert::From<$crate::Uuid> for $name {
#[inline]
fn from(uuid: $crate::Uuid) -> Self {
Self(uuid)
}
}
impl ::std::convert::From<$name> for $crate::Uuid {
#[inline]
fn from(id: $name) -> Self {
id.0
}
}
impl ::std::convert::From<[u8; 16]> for $name {
#[inline]
fn from(bytes: [u8; 16]) -> Self {
Self($crate::Uuid::from_bytes(bytes))
}
}
impl ::std::str::FromStr for $name {
type Err = $crate::IdError;
fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
Self::try_new(s)
}
}
#[cfg(feature = "sqlx")]
impl<DB> ::sqlx::Type<DB> for $name
where
DB: ::sqlx::Database,
::std::string::String: ::sqlx::Type<DB>,
{
fn type_info() -> <DB as ::sqlx::Database>::TypeInfo {
<::std::string::String as ::sqlx::Type<DB>>::type_info()
}
fn compatible(ty: &<DB as ::sqlx::Database>::TypeInfo) -> bool {
<::std::string::String as ::sqlx::Type<DB>>::compatible(ty)
}
}
#[cfg(feature = "sqlx")]
impl<'q, DB> ::sqlx::Encode<'q, DB> for $name
where
DB: ::sqlx::Database,
::std::string::String: ::sqlx::Encode<'q, DB>,
{
fn encode_by_ref(
&self,
buf: &mut <DB as ::sqlx::Database>::ArgumentBuffer,
) -> ::std::result::Result<
::sqlx::encode::IsNull,
::sqlx::error::BoxDynError,
> {
<::std::string::String as ::sqlx::Encode<'q, DB>>::encode_by_ref(
&self.0.to_string(),
buf,
)
}
}
#[cfg(feature = "sqlx")]
impl<'r, DB> ::sqlx::Decode<'r, DB> for $name
where
DB: ::sqlx::Database,
::std::string::String: ::sqlx::Decode<'r, DB>,
{
fn decode(
value: <DB as ::sqlx::Database>::ValueRef<'r>,
) -> ::std::result::Result<Self, ::sqlx::error::BoxDynError> {
let s = <::std::string::String as ::sqlx::Decode<'r, DB>>::decode(value)?;
Self::try_new(s).map_err(::std::convert::Into::into)
}
}
};
}
define_id! {
pub TenantId
}
impl TenantId {
pub const SYSTEM_STR: &'static str = "00000000-0000-0000-0000-000000000000";
pub const SYSTEM: Self = Self(Uuid::nil());
#[inline]
pub const fn system() -> Self {
Self::SYSTEM
}
#[inline]
pub fn is_system(&self) -> bool {
self.is_nil()
}
}
define_id! {
pub UserId
}
impl UserId {
pub const SYSTEM_STR: &'static str = "00000000-0000-0000-0000-000000000001";
pub const SYSTEM: Self = Self(Uuid::from_bytes([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
]));
#[inline]
pub const fn system() -> Self {
Self::SYSTEM
}
#[inline]
pub fn is_system(&self) -> bool {
*self == Self::SYSTEM
}
}
define_id! {
pub DeviceId
}
define_id! {
pub SessionId
}
define_id! {
pub EventId
}
pub fn ensure_user_id_not_reserved(user_id: &UserId, tenant_id: &TenantId) -> Result<(), IdError> {
if user_id.is_system() {
return Err(IdError::Reserved("UserId"));
}
if tenant_id.is_system() {
return Err(IdError::Reserved("TenantId"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing;
use axess_rng::testing::MockRng;
#[test]
fn system_tenant_is_nil() {
assert!(TenantId::SYSTEM.is_system());
assert!(TenantId::system().is_nil());
assert_eq!(TenantId::SYSTEM.to_string(), TenantId::SYSTEM_STR);
}
#[test]
fn system_user_distinct_from_system_tenant() {
assert_ne!(UserId::SYSTEM.as_uuid(), TenantId::SYSTEM.as_uuid());
assert!(UserId::SYSTEM.is_system());
assert_eq!(UserId::SYSTEM.to_string(), UserId::SYSTEM_STR);
}
#[test]
fn try_new_rejects_empty() {
assert_eq!(TenantId::try_new(""), Err(IdError::Empty("TenantId")));
assert_eq!(UserId::try_new(""), Err(IdError::Empty("UserId")));
}
#[test]
fn try_new_rejects_non_uuid() {
assert_eq!(
TenantId::try_new("not-a-uuid"),
Err(IdError::NotAUuid("TenantId"))
);
}
#[test]
fn try_new_accepts_uuid_string() {
let t = TenantId::try_new("1f0a7b2e-4c91-4e3f-9b2a-8d0123456789").unwrap();
assert!(!t.is_system());
}
#[test]
fn new_is_dst_reproducible() {
let a = MockRng::new(42);
let b = MockRng::new(42);
assert_eq!(TenantId::new(&a), TenantId::new(&b));
assert_eq!(
SessionId::new(&MockRng::new(7)).as_uuid().get_version_num(),
4
);
}
#[test]
fn from_namespaced_str_is_deterministic() {
let ns = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
let a = TenantId::from_namespaced_str(ns, "ekekrantz");
let b = TenantId::from_namespaced_str(ns, "ekekrantz");
let c = TenantId::from_namespaced_str(ns, "wctest");
assert_eq!(a, b);
assert_ne!(a, c);
assert_eq!(a.as_uuid().get_version_num(), 5);
}
#[test]
fn ensure_user_id_not_reserved_blocks_system_user() {
let res = ensure_user_id_not_reserved(&UserId::SYSTEM, &testing::tenant("t1"));
assert_eq!(res, Err(IdError::Reserved("UserId")));
}
#[test]
fn ensure_user_id_not_reserved_blocks_system_tenant() {
let res = ensure_user_id_not_reserved(&testing::user("u1"), &TenantId::SYSTEM);
assert_eq!(res, Err(IdError::Reserved("TenantId")));
}
#[test]
fn ensure_user_id_not_reserved_accepts_normal_pair() {
let res = ensure_user_id_not_reserved(&testing::user("u1"), &testing::tenant("t1"));
assert!(res.is_ok());
}
#[test]
fn testing_helpers_are_deterministic() {
assert_eq!(testing::tenant("alice"), testing::tenant("alice"));
assert_eq!(testing::user("alice"), testing::user("alice"));
assert_eq!(testing::device("alice"), testing::device("alice"));
assert_eq!(testing::session("alice"), testing::session("alice"));
assert_eq!(testing::event("alice"), testing::event("alice"));
}
#[cfg(feature = "serde")]
#[test]
fn serde_wire_is_hyphenated_string() {
let id =
TenantId::from_uuid(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap());
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "\"550e8400-e29b-41d4-a716-446655440000\"");
let back: TenantId = serde_json::from_str(&json).unwrap();
assert_eq!(id, back);
}
#[cfg(feature = "serde")]
#[test]
fn id_error_serialise_shape_is_stable() {
for (err, expected_json) in [
(IdError::Empty("TenantId"), r#"{"Empty":"TenantId"}"#),
(IdError::NotAUuid("UserId"), r#"{"NotAUuid":"UserId"}"#),
(IdError::Reserved("DeviceId"), r#"{"Reserved":"DeviceId"}"#),
] {
let json = serde_json::to_string(&err).unwrap();
assert_eq!(json, expected_json, "IdError JSON shape drifted");
}
}
#[test]
fn no_default_impl() {
fn assert_not_default<T>()
where
T: Sized,
{
}
assert_not_default::<TenantId>();
assert_not_default::<UserId>();
assert_not_default::<SessionId>();
assert_not_default::<DeviceId>();
assert_not_default::<EventId>();
}
define_id! {
pub TestId
}
#[test]
fn define_id_macro_yields_v4_via_new() {
let rng = MockRng::new(7);
let id = TestId::new(&rng);
assert_eq!(id.as_uuid().get_version_num(), 4);
assert!(!id.is_nil());
}
#[test]
fn mint_v4_default_is_dst_reproducible_under_override() {
let a = with_thread_local_rng(MockRng::new(99), mint_v4_default);
let b = with_thread_local_rng(MockRng::new(99), mint_v4_default);
assert_eq!(a, b);
let c = mint_v4_default();
assert_ne!(a, c);
assert_eq!(a.get_version_num(), 4);
assert_eq!(b.get_version_num(), 4);
assert_eq!(c.get_version_num(), 4);
}
#[cfg(feature = "sqlx")]
#[tokio::test]
async fn sqlx_text_column_roundtrip() {
use sqlx::sqlite::SqlitePoolOptions;
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
sqlx::query("CREATE TABLE ids (id TEXT NOT NULL PRIMARY KEY)")
.execute(&pool)
.await
.unwrap();
let rng = MockRng::new(13);
let original = TenantId::new(&rng);
sqlx::query("INSERT INTO ids (id) VALUES (?1)")
.bind(original)
.execute(&pool)
.await
.unwrap();
let row: (TenantId,) = sqlx::query_as("SELECT id FROM ids LIMIT 1")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(row.0, original);
}
#[test]
fn id_error_display_pins_each_variant_string() {
assert_eq!(IdError::Empty("Foo").to_string(), "Foo must be non-empty");
assert_eq!(
IdError::NotAUuid("Bar").to_string(),
"Bar must be a valid Uuid"
);
assert_eq!(
IdError::Reserved("Baz").to_string(),
"Baz matches reserved system identifier"
);
}
#[test]
fn with_thread_local_rng_drop_restores_previous_override() {
let outer_step1 = with_thread_local_rng(MockRng::new(1), mint_v4_default);
let post_inner = with_thread_local_rng(MockRng::new(1), || {
with_thread_local_rng(MockRng::new(99), || {
for _ in 0..5 {
let _ = mint_v4_default();
}
});
mint_v4_default()
});
assert_eq!(
outer_step1, post_inner,
"Drop on Guard MUST restore the outer override; \
otherwise the inner override leaks across the scope boundary"
);
}
}