diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a657157..eedcc6e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: Continuous integration env: VERSION_FEATURES: "v1 v3 v4 v5 v6 v7 v8" - DEP_FEATURES: "slog serde arbitrary borsh zerocopy bytemuck" + DEP_FEATURES: "slog serde arbitrary borsh zerocopy bytemuck nonzero" on: pull_request: diff --git a/Cargo.toml b/Cargo.toml index 04a8cc36..e4ef3ae3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ rust-version = "1.63.0" rustc-args = ["--cfg", "uuid_unstable"] rustdoc-args = ["--cfg", "uuid_unstable"] targets = ["x86_64-unknown-linux-gnu"] -features = ["serde", "arbitrary", "slog", "borsh", "v1", "v3", "v4", "v5", "v6", "v7", "v8"] +features = ["serde", "arbitrary", "slog", "borsh", "nonzero", "v1", "v3", "v4", "v5", "v6", "v7", "v8"] [package.metadata.playground] features = ["serde", "v1", "v3", "v4", "v5", "v6", "v7", "v8"] @@ -79,6 +79,8 @@ atomic = ["dep:atomic"] borsh = ["dep:borsh", "dep:borsh-derive"] +nonzero = [] + # Public: Used in trait impls on `Uuid` [dependencies.bytemuck] version = "1.14.0" diff --git a/src/external.rs b/src/external.rs index 6f20d8fd..ba6abb0d 100644 --- a/src/external.rs +++ b/src/external.rs @@ -2,6 +2,8 @@ pub(crate) mod arbitrary_support; #[cfg(feature = "borsh")] pub(crate) mod borsh_support; +#[cfg(feature = "nonzero")] +pub(crate) mod nonzero_support; #[cfg(feature = "serde")] pub(crate) mod serde_support; #[cfg(feature = "slog")] diff --git a/src/external/nonzero_support.rs b/src/external/nonzero_support.rs new file mode 100644 index 00000000..67571cb6 --- /dev/null +++ b/src/external/nonzero_support.rs @@ -0,0 +1,121 @@ +//! A wrapper type for non-zero UUIDs that provides a more memory-efficient +//! `Option` representation. + +use std::convert::TryFrom; +use std::fmt; +use std::num::NonZeroU128; +use std::ops::Deref; + +use crate::Uuid; + +/// A UUID that is guaranteed not to be the nil UUID. +/// +/// This is useful for representing optional UUIDs more efficiently, as `Option` +/// takes up the same space as `Uuid`. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct NonZeroUuid(NonZeroU128); + +/// Error returned when attempting to create a `NonZeroUuid` from a nil UUID. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct NonZeroUuidError; + +impl fmt::Display for NonZeroUuidError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "attempted to create NonZeroUuid from nil UUID") + } +} + +impl std::error::Error for NonZeroUuidError {} + +impl NonZeroUuid { + /// Creates a non-zero UUID. Returns `None` if the given UUID is the nil UUID. + #[inline] + pub fn new(uuid: Uuid) -> Option { + let bits = uuid.as_u128(); + NonZeroU128::new(bits).map(Self) + } + + /// Creates a non-zero UUID without checking if it's the nil UUID. + /// + /// # Safety + /// + /// The caller must ensure that the UUID is not the nil UUID. + /// If this constraint is violated, it may lead to undefined behavior when + /// the resulting NonZeroUuid is used. + #[inline] + pub const unsafe fn new_unchecked(uuid: Uuid) -> Self { + let bits = uuid.as_u128(); + Self(NonZeroU128::new_unchecked(bits)) + } + + /// Returns the underlying `Uuid`. + #[inline] + pub fn get(self) -> Uuid { + Uuid::from_u128(self.0.get()) + } +} + +impl TryFrom for NonZeroUuid { + type Error = NonZeroUuidError; + + fn try_from(uuid: Uuid) -> Result { + NonZeroUuid::new(uuid).ok_or(NonZeroUuidError) + } +} + +impl From for Uuid { + fn from(nz_uuid: NonZeroUuid) -> Self { + nz_uuid.get() + } +} + +impl Deref for NonZeroUuid { + type Target = Uuid; + + #[inline] + fn deref(&self) -> &Self::Target { + // SAFETY: We know the bits are valid UUID bits since we only construct + // NonZeroUuid from valid Uuid values. + let bits = self.0.get(); + unsafe { &*((&bits as *const u128) as *const Uuid) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nonzero_uuid_option_size() { + assert_eq!( + std::mem::size_of::>(), + std::mem::size_of::() + ); + } + + #[test] + fn test_new_with_non_nil() { + let uuid = Uuid::new_v4(); + let nz_uuid = NonZeroUuid::new(uuid); + assert!(nz_uuid.is_some()); + assert_eq!(nz_uuid.unwrap().get(), uuid); + } + + #[test] + fn test_new_with_nil() { + let nil_uuid = Uuid::nil(); + let nz_uuid = NonZeroUuid::new(nil_uuid); + assert!(nz_uuid.is_none()); + } + + #[test] + fn test_try_from() { + let uuid = Uuid::new_v4(); + let nz_uuid = NonZeroUuid::try_from(uuid); + assert!(nz_uuid.is_ok()); + + let nil_uuid = Uuid::nil(); + let nz_nil = NonZeroUuid::try_from(nil_uuid); + assert!(nz_nil.is_err()); + } +}