1#![cfg_attr(docsrs, feature(doc_cfg))]
33#![forbid(unsafe_code)]
34
35#[cfg(feature = "arbitrary")]
36mod arbitrary;
37
38#[cfg(feature = "serde")]
39mod serde;
40
41use std::hash::Hash;
42use std::{borrow::Cow, fmt, str::FromStr};
43
44use arrayvec::ArrayString;
45use uuid::{NoContext, Uuid};
46
47#[non_exhaustive]
48#[derive(Debug, thiserror::Error)]
49pub enum Error {
50 #[error("id type is invalid")]
52 InvalidType,
53 #[error("id type {actual:?} does not match expected {expected:?}")]
55 IncorrectType {
56 actual: String,
57 expected: Cow<'static, str>,
58 },
59 #[error("id suffix is invalid")]
61 InvalidData,
62 #[error("string is not a type-id")]
64 NotATypeId,
65}
66
67pub trait StaticType: Default {
69 const TYPE: &'static str;
72
73 #[doc(hidden)]
74 const __TYPE_PREFIX_IS_VALID: bool = {
75 assert!(Self::TYPE.len() < 64);
76 let mut i = 0;
77 while i < Self::TYPE.len() {
78 let b = Self::TYPE.as_bytes()[i];
79 assert!(
80 matches!(b, b'a'..=b'z' | b'_'),
81 "type prefix must contain only lowercase ascii, or underscores"
82 );
83 i += 1;
84 }
85 if !Self::TYPE.is_empty() {
86 assert!(
87 Self::TYPE.as_bytes()[0] != b'_',
88 "type prefix must not start with an underscore"
89 );
90 assert!(
91 Self::TYPE.as_bytes()[i - 1] != b'_',
92 "type prefix must not end with an underscore"
93 );
94 }
95 true
96 };
97}
98
99pub trait Type: Sized {
101 fn try_from_type_prefix(tag: &str) -> Result<Self, Cow<'static, str>>;
104
105 fn to_type_prefix(&self) -> &str;
107}
108
109impl<T: StaticType> Type for T {
110 fn try_from_type_prefix(tag: &str) -> Result<Self, Cow<'static, str>> {
111 assert!(Self::__TYPE_PREFIX_IS_VALID);
112 if tag != Self::TYPE {
113 Err(Self::TYPE.into())
114 } else {
115 Ok(T::default())
116 }
117 }
118
119 fn to_type_prefix(&self) -> &str {
120 assert!(Self::__TYPE_PREFIX_IS_VALID);
121 Self::TYPE
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
136pub struct DynamicType(ArrayString<63>);
137
138impl DynamicType {
139 pub fn new(s: &str) -> Result<Self, Error> {
152 let tag: ArrayString<63> = s.try_into().map_err(|_| Error::InvalidType)?;
153
154 if tag.bytes().any(|b| !matches!(b, b'a'..=b'z' | b'_')) {
155 return Err(Error::InvalidType);
156 }
157 if !tag.is_empty() && (tag.as_bytes()[0] == b'_' || tag.as_bytes()[tag.len() - 1] == b'_') {
158 return Err(Error::InvalidType);
159 }
160 Ok(Self(tag))
161 }
162}
163
164impl Type for DynamicType {
165 fn try_from_type_prefix(tag: &str) -> Result<Self, Cow<'static, str>> {
166 let tag: ArrayString<63> = tag.try_into().map_err(|_| tag[..63].to_owned())?;
167 if tag.bytes().any(|b| !matches!(b, b'a'..=b'z' | b'_')) {
168 return Err(tag.to_lowercase().into());
169 }
170 if !tag.is_empty() && (tag.as_bytes()[0] == b'_' || tag.as_bytes()[tag.len() - 1] == b'_') {
171 return Err(tag.to_lowercase().into());
172 }
173 Ok(Self(tag))
174 }
175
176 fn to_type_prefix(&self) -> &str {
177 &self.0
178 }
179}
180
181#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
213pub struct TypeSafeId<T> {
214 tag: T,
215 data: Uuid,
216}
217
218impl<T: Type> fmt::Debug for TypeSafeId<T> {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 f.debug_struct("TypeSafeId")
221 .field("tag", &self.tag.to_type_prefix())
222 .field("data", &self.data)
223 .finish()
224 }
225}
226
227impl<T: Type> Hash for TypeSafeId<T> {
228 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
229 self.tag.to_type_prefix().hash(state);
230 self.data.hash(state);
231 }
232}
233
234struct Uuid128(u128);
235
236impl From<Uuid> for Uuid128 {
237 fn from(value: Uuid) -> Self {
238 Self(u128::from_be_bytes(value.into_bytes()))
239 }
240}
241impl From<Uuid128> for Uuid {
242 fn from(value: Uuid128) -> Self {
243 Uuid::from_bytes(value.0.to_be_bytes())
244 }
245}
246
247impl<T: StaticType> TypeSafeId<T> {
248 #[allow(clippy::new_without_default)]
250 pub fn new() -> Self {
251 Self::new_with_ts_rng(
252 T::default(),
253 uuid::Timestamp::now(NoContext),
254 &mut rand::rng(),
255 )
256 }
257
258 pub fn from_uuid(data: Uuid) -> Self {
260 Self::from_type_and_uuid(T::default(), data)
261 }
262
263 pub const fn static_len() -> usize {
265 T::TYPE.len() + 1 + ENCODED_UUID_LEN
268 }
269}
270
271impl<T: StaticType> From<Uuid> for TypeSafeId<T> {
272 fn from(data: Uuid) -> Self {
273 Self::from_uuid(data)
274 }
275}
276
277impl<T: Type> TypeSafeId<T> {
278 pub fn new_with_type(type_prefix: T) -> Self {
280 Self::new_with_ts_rng(
281 type_prefix,
282 uuid::Timestamp::now(NoContext),
283 &mut rand::rng(),
284 )
285 }
286
287 fn new_with_ts_rng(type_prefix: T, ts: uuid::Timestamp, rng: &mut impl rand::Rng) -> Self {
288 let (secs, nanos) = ts.to_unix();
289 let millis = (secs * 1000).saturating_add(nanos as u64 / 1_000_000);
290 Self::from_type_and_uuid(
291 type_prefix,
292 uuid::Builder::from_unix_timestamp_millis(millis, &rng.random()).into_uuid(),
293 )
294 }
295
296 pub fn from_type_and_uuid(type_prefix: T, data: Uuid) -> Self {
298 Self {
299 tag: type_prefix,
300 data,
301 }
302 }
303
304 pub fn type_prefix(&self) -> &str {
305 self.tag.to_type_prefix()
306 }
307
308 #[allow(clippy::len_without_is_empty)]
312 pub fn len(&self) -> usize {
313 self.type_prefix().len() +
314 1 + ENCODED_UUID_LEN
316 }
317
318 pub fn uuid(&self) -> Uuid {
319 self.data
320 }
321}
322
323impl<T: Type> FromStr for TypeSafeId<T> {
324 type Err = Error;
325
326 fn from_str(id: &str) -> Result<Self, Self::Err> {
327 let (tag, id) = match id.rsplit_once('_') {
328 Some(("", _)) => return Err(Error::InvalidType),
329 Some((tag, id)) => (tag, id),
330 None => ("", id),
331 };
332
333 let tag = T::try_from_type_prefix(tag).map_err(|expected| Error::IncorrectType {
334 actual: tag.into(),
335 expected,
336 })?;
337
338 Ok(Self {
339 tag,
340 data: parse_base32_uuid7(id)?.into(),
341 })
342 }
343}
344
345const ENCODED_UUID_LEN: usize = 26;
347
348fn parse_base32_uuid7(id: &str) -> Result<Uuid128, Error> {
349 let mut id: [u8; ENCODED_UUID_LEN] =
350 id.as_bytes().try_into().map_err(|_| Error::InvalidData)?;
351 let mut max = 0;
352 for b in &mut id {
353 *b = CROCKFORD_INV[*b as usize];
354 max |= *b;
355 }
356 if max > 32 || id[0] > 7 {
357 return Err(Error::InvalidData);
358 }
359
360 let mut out = 0u128;
361 for b in id {
362 out <<= 5;
363 out |= b as u128;
364 }
365
366 Ok(Uuid128(out))
367}
368
369impl<T: Type> fmt::Display for TypeSafeId<T> {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371 f.write_str(&to_array_string(self.type_prefix(), self.data.into()))
372 }
373}
374
375fn to_array_string(prefix: &str, data: Uuid128) -> ArrayString<90> {
376 let mut out = ArrayString::new();
377
378 if !prefix.is_empty() {
379 out.push_str(prefix);
380 out.push_str("_");
381 }
382
383 let mut buf = [0; ENCODED_UUID_LEN];
384 let mut data = data.0;
385 for b in buf.iter_mut().rev() {
386 *b = CROCKFORD[((data as u8) & 0x1f) as usize];
387 debug_assert!(b.is_ascii());
388 data >>= 5;
389 }
390
391 let s = std::str::from_utf8(&buf).expect("only ascii bytes should be in the buffer");
392
393 out.push_str(s);
394 out
395}
396
397const CROCKFORD: &[u8; 32] = b"0123456789abcdefghjkmnpqrstvwxyz";
398const CROCKFORD_INV: &[u8; 256] = &{
399 let mut output = [255; 256];
400
401 let mut i = 0;
402 while i < 32 {
403 output[CROCKFORD[i as usize] as usize] = i;
404 i += 1;
405 }
406
407 output
408};