#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(doc_cfg, feature(doc_cfg))]
#[cfg(feature = "alloc")]
extern crate alloc;
#[doc(hidden)]
pub mod macro_support {
#[cfg(feature = "schemars08")]
pub use schemars as schemars08;
#[cfg(feature = "schemars08")]
pub use serde_json;
}
use core::{
cmp::Ordering,
fmt,
hash::{Hash, Hasher},
marker::PhantomData,
str::FromStr,
};
#[cfg(feature = "v7")]
pub use uuid::Timestamp;
use uuid::{Uuid, Version};
#[repr(transparent)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent, bound = ""))]
pub struct TypedUuid<T: TypedUuidKind> {
uuid: Uuid,
_phantom: PhantomData<T>,
}
impl<T: TypedUuidKind> TypedUuid<T> {
#[inline]
#[must_use]
pub const fn nil() -> Self {
Self {
uuid: Uuid::nil(),
_phantom: PhantomData,
}
}
#[inline]
#[must_use]
pub const fn max() -> Self {
Self {
uuid: Uuid::max(),
_phantom: PhantomData,
}
}
#[inline]
#[must_use]
pub const fn from_fields(d1: u32, d2: u16, d3: u16, d4: [u8; 8]) -> Self {
Self {
uuid: Uuid::from_fields(d1, d2, d3, &d4),
_phantom: PhantomData,
}
}
#[inline]
#[must_use]
pub const fn from_fields_le(d1: u32, d2: u16, d3: u16, d4: [u8; 8]) -> Self {
Self {
uuid: Uuid::from_fields_le(d1, d2, d3, &d4),
_phantom: PhantomData,
}
}
#[inline]
#[must_use]
pub const fn from_u128(value: u128) -> Self {
Self {
uuid: Uuid::from_u128(value),
_phantom: PhantomData,
}
}
#[inline]
#[must_use]
pub const fn from_u128_le(value: u128) -> Self {
Self {
uuid: Uuid::from_u128_le(value),
_phantom: PhantomData,
}
}
#[inline]
#[must_use]
pub const fn from_u64_pair(d1: u64, d2: u64) -> Self {
Self {
uuid: Uuid::from_u64_pair(d1, d2),
_phantom: PhantomData,
}
}
#[inline]
#[must_use]
pub const fn from_bytes(bytes: uuid::Bytes) -> Self {
Self {
uuid: Uuid::from_bytes(bytes),
_phantom: PhantomData,
}
}
#[inline]
#[must_use]
pub const fn from_bytes_le(bytes: uuid::Bytes) -> Self {
Self {
uuid: Uuid::from_bytes_le(bytes),
_phantom: PhantomData,
}
}
#[inline]
#[cfg(feature = "v4")]
#[must_use]
pub fn new_v4() -> Self {
Self::from_untyped_uuid(Uuid::new_v4())
}
#[inline]
#[cfg(feature = "v7")]
#[must_use]
pub fn new_v7(ts: uuid::Timestamp) -> Self {
Self::from_untyped_uuid(Uuid::new_v7(ts))
}
#[inline]
pub const fn get_version_num(&self) -> usize {
self.uuid.get_version_num()
}
#[inline]
pub fn get_version(&self) -> Option<Version> {
self.uuid.get_version()
}
#[inline]
pub const fn is_nil(&self) -> bool {
self.uuid.is_nil()
}
#[inline]
pub const fn is_max(&self) -> bool {
self.uuid.is_max()
}
#[inline]
pub fn as_fields(&self) -> (u32, u16, u16, &[u8; 8]) {
self.uuid.as_fields()
}
#[inline]
pub fn to_fields_le(&self) -> (u32, u16, u16, &[u8; 8]) {
self.uuid.to_fields_le()
}
#[inline]
pub const fn as_u128(&self) -> u128 {
self.uuid.as_u128()
}
#[inline]
pub fn to_u128_le(&self) -> u128 {
self.uuid.to_u128_le()
}
#[inline]
pub const fn as_u64_pair(&self) -> (u64, u64) {
self.uuid.as_u64_pair()
}
#[inline]
pub const fn as_bytes(&self) -> &uuid::Bytes {
self.uuid.as_bytes()
}
#[inline]
#[must_use]
pub const fn into_bytes(self) -> uuid::Bytes {
self.uuid.into_bytes()
}
#[inline]
pub fn to_bytes_le(&self) -> uuid::Bytes {
self.uuid.to_bytes_le()
}
#[inline]
#[must_use]
pub const fn upcast<U: TypedUuidKind>(self) -> TypedUuid<U>
where
T: Into<U>,
{
TypedUuid {
uuid: self.uuid,
_phantom: PhantomData,
}
}
}
impl<T: TypedUuidKind> PartialEq for TypedUuid<T> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.uuid.eq(&other.uuid)
}
}
impl<T: TypedUuidKind> Eq for TypedUuid<T> {}
impl<T: TypedUuidKind> PartialOrd for TypedUuid<T> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<T: TypedUuidKind> Ord for TypedUuid<T> {
#[inline]
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.uuid.cmp(&other.uuid)
}
}
impl<T: TypedUuidKind> Hash for TypedUuid<T> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.uuid.hash(state);
}
}
impl<T: TypedUuidKind> fmt::Debug for TypedUuid<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.uuid.fmt(f)?;
write!(f, " ({})", T::tag())
}
}
impl<T: TypedUuidKind> fmt::Display for TypedUuid<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.uuid.fmt(f)
}
}
impl<T: TypedUuidKind> Clone for TypedUuid<T> {
#[inline]
fn clone(&self) -> Self {
*self
}
}
impl<T: TypedUuidKind> Copy for TypedUuid<T> {}
impl<T: TypedUuidKind> FromStr for TypedUuid<T> {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let uuid = Uuid::from_str(s).map_err(|error| ParseError {
error,
tag: T::tag(),
})?;
Ok(Self::from_untyped_uuid(uuid))
}
}
impl<T: TypedUuidKind> Default for TypedUuid<T> {
#[inline]
fn default() -> Self {
Self::from_untyped_uuid(Uuid::default())
}
}
impl<T: TypedUuidKind> AsRef<[u8]> for TypedUuid<T> {
#[inline]
fn as_ref(&self) -> &[u8] {
self.uuid.as_ref()
}
}
#[cfg(feature = "alloc")]
impl<T: TypedUuidKind> From<TypedUuid<T>> for alloc::vec::Vec<u8> {
#[inline]
fn from(typed_uuid: TypedUuid<T>) -> Self {
typed_uuid.into_untyped_uuid().into_bytes().to_vec()
}
}
#[cfg(feature = "schemars08")]
mod schemars08_imp {
use super::*;
use schemars::{
JsonSchema, SchemaGenerator,
schema::{InstanceType, Schema, SchemaObject},
schema_for,
};
const CRATE_NAME: &str = "newtype-uuid";
const CRATE_VERSION: &str = "1";
const CRATE_PATH: &str = "newtype_uuid::TypedUuid";
impl<T> JsonSchema for TypedUuid<T>
where
T: TypedUuidKind + JsonSchema,
{
#[inline]
fn schema_name() -> String {
if let Some(alias) = T::alias() {
alias.to_owned()
} else {
format!("TypedUuidFor{}", T::schema_name())
}
}
#[inline]
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Owned(format!("newtype_uuid::TypedUuid<{}>", T::schema_id()))
}
#[inline]
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
let t_schema = schema_for!(T);
if let Some(schema) = lift_json_schema(&t_schema.schema, T::alias()) {
return schema.into();
}
SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some("uuid".to_string()),
extensions: [(
"x-rust-type".to_string(),
serde_json::json!({
"crate": CRATE_NAME,
"version": CRATE_VERSION,
"path": CRATE_PATH,
"parameters": [generator.subschema_for::<T>()]
}),
)]
.into_iter()
.collect(),
..Default::default()
}
.into()
}
}
#[allow(clippy::question_mark)]
fn lift_json_schema(schema: &SchemaObject, alias: Option<&str>) -> Option<SchemaObject> {
let Some(alias) = alias else {
return None;
};
let Some(v) = schema.extensions.get("x-rust-type") else {
return None;
};
let Some(crate_) = v.get("crate") else {
return None;
};
let Some(version) = v.get("version") else {
return None;
};
let Some(path) = v.get("path").and_then(|p| p.as_str()) else {
return None;
};
let Some((module_path, _)) = path.rsplit_once("::") else {
return None;
};
let alias_path = format!("{module_path}::{alias}");
Some(SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some("uuid".to_string()),
extensions: [(
"x-rust-type".to_string(),
serde_json::json!({
"crate": crate_,
"version": version,
"path": alias_path,
}),
)]
.into_iter()
.collect(),
..Default::default()
})
}
}
#[cfg(feature = "proptest1")]
mod proptest1_imp {
use super::*;
use proptest::{
arbitrary::{Arbitrary, any},
strategy::{BoxedStrategy, Strategy},
};
#[derive(Clone, Debug, Default)]
pub struct TypedUuidParams(());
impl<T> Arbitrary for TypedUuid<T>
where
T: TypedUuidKind,
{
type Parameters = TypedUuidParams;
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
let bytes = any::<[u8; 16]>();
bytes
.prop_map(|b| {
let uuid = uuid::Builder::from_random_bytes(b).into_uuid();
TypedUuid::<T>::from_untyped_uuid(uuid)
})
.boxed()
}
}
}
pub trait TypedUuidKind: Send + Sync + 'static {
fn tag() -> TypedUuidTag;
#[inline]
fn alias() -> Option<&'static str> {
None
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TypedUuidTag(&'static str);
impl TypedUuidTag {
#[must_use]
pub const fn new(tag: &'static str) -> Self {
match Self::try_new_impl(tag) {
Ok(tag) => tag,
Err(message) => panic!("{}", message),
}
}
pub const fn try_new(tag: &'static str) -> Result<Self, TagError> {
match Self::try_new_impl(tag) {
Ok(tag) => Ok(tag),
Err(message) => Err(TagError {
input: tag,
message,
}),
}
}
const fn try_new_impl(tag: &'static str) -> Result<Self, &'static str> {
if tag.is_empty() {
return Err("tag must not be empty");
}
let bytes = tag.as_bytes();
if !(bytes[0].is_ascii_alphabetic() || bytes[0] == b'_') {
return Err("first character of tag must be an ASCII letter or underscore");
}
let mut bytes = match bytes {
[_, rest @ ..] => rest,
[] => panic!("already checked that it's non-empty"),
};
while let [rest @ .., last] = &bytes {
if !(last.is_ascii_alphanumeric() || *last == b'_' || *last == b'-') {
break;
}
bytes = rest;
}
if !bytes.is_empty() {
return Err("tag must only contain ASCII letters, digits, underscores, or hyphens");
}
Ok(Self(tag))
}
pub const fn as_str(&self) -> &'static str {
self.0
}
}
impl fmt::Display for TypedUuidTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
impl AsRef<str> for TypedUuidTag {
fn as_ref(&self) -> &str {
self.0
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct TagError {
pub input: &'static str,
pub message: &'static str,
}
impl fmt::Display for TagError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"error creating tag from '{}': {}",
self.input, self.message
)
}
}
impl core::error::Error for TagError {}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ParseError {
pub error: uuid::Error,
pub tag: TypedUuidTag,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "error parsing UUID ({})", self.tag)
}
}
impl core::error::Error for ParseError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
Some(&self.error)
}
}
pub trait GenericUuid {
#[must_use]
fn from_untyped_uuid(uuid: Uuid) -> Self
where
Self: Sized;
#[must_use]
fn into_untyped_uuid(self) -> Uuid
where
Self: Sized;
fn as_untyped_uuid(&self) -> &Uuid;
}
impl GenericUuid for Uuid {
#[inline]
fn from_untyped_uuid(uuid: Uuid) -> Self {
uuid
}
#[inline]
fn into_untyped_uuid(self) -> Uuid {
self
}
#[inline]
fn as_untyped_uuid(&self) -> &Uuid {
self
}
}
impl<T: TypedUuidKind> GenericUuid for TypedUuid<T> {
#[inline]
fn from_untyped_uuid(uuid: Uuid) -> Self {
Self {
uuid,
_phantom: PhantomData,
}
}
#[inline]
fn into_untyped_uuid(self) -> Uuid {
self.uuid
}
#[inline]
fn as_untyped_uuid(&self) -> &Uuid {
&self.uuid
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_tags() {
for &valid_tag in &[
"a", "a-", "a_", "a-b", "a_b", "a1", "a1-", "a1_", "a1-b", "a1_b", "_a",
] {
TypedUuidTag::try_new(valid_tag).expect("tag is valid");
_ = TypedUuidTag::new(valid_tag);
}
for invalid_tag in &["", "1", "-", "a1b!", "a1-b!", "a1_b:", "\u{1f4a9}"] {
TypedUuidTag::try_new(invalid_tag).unwrap_err();
}
}
#[test]
#[cfg(all(feature = "v4", feature = "std"))]
fn test_generic_uuid_object_safe() {
let uuid = Uuid::new_v4();
let box_uuid = Box::new(uuid) as Box<dyn GenericUuid>;
assert_eq!(box_uuid.as_untyped_uuid(), &uuid);
}
}