Skip to content

Commit

Permalink
feat: Add NonZeroUuid type for optimized Option<Uuid> representation
Browse files Browse the repository at this point in the history
This commit introduces a new `NonZeroUuid` type that wraps a `Uuid` and guarantees that it is
not the nil UUID. This allows for a more memory-efficient representation of `Option<Uuid>`,
as `Option<NonZeroUuid>` takes up the same space as `Uuid`.

This change improves the memory efficiency of representing optional UUIDs and provides a more convenient way to work with non-nil UUIDs.

fixes: uuid-rs#770
  • Loading branch information
ab22593k committed Jan 4, 2025
1 parent 5cbe0ce commit 6c7e909
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
121 changes: 121 additions & 0 deletions src/external/nonzero_support.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! A wrapper type for non-zero UUIDs that provides a more memory-efficient
//! `Option<NonZeroUuid>` 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<NonZeroUuid>`
/// 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<Self> {
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<Uuid> for NonZeroUuid {
type Error = NonZeroUuidError;

fn try_from(uuid: Uuid) -> Result<Self, Self::Error> {
NonZeroUuid::new(uuid).ok_or(NonZeroUuidError)
}
}

impl From<NonZeroUuid> 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::<Option<NonZeroUuid>>(),
std::mem::size_of::<Uuid>()
);
}

#[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());
}
}

0 comments on commit 6c7e909

Please sign in to comment.