Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add NonZeroUuid type for optimized Option<Uuid> representation #779

Merged
merged 4 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = []
KodrAus marked this conversation as resolved.
Show resolved Hide resolved

# 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;
KodrAus marked this conversation as resolved.
Show resolved Hide resolved
#[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);
KodrAus marked this conversation as resolved.
Show resolved Hide resolved

/// Error returned when attempting to create a `NonZeroUuid` from a nil UUID.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct NonZeroUuidError;
KodrAus marked this conversation as resolved.
Show resolved Hide resolved

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 {
KodrAus marked this conversation as resolved.
Show resolved Hide resolved
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());
}
}