tnid 0.2.0

A UUID compatible ID with static type checking
Documentation
/// A wrapper for 128-bit values that may or may not be valid TNIDs.
///
/// This type provides a way to work with 128-bit UUID-like values without the strict
/// validation that [`Tnid`](crate::Tnid) requires. Unlike [`Tnid`](crate::Tnid), which
/// only accepts values that conform to the TNID specification (correct UUIDv8 version/variant
/// bits and valid name encoding), `UuidLike` accepts any 128-bit value.
///
/// This makes `UuidLike` useful for:
/// - Inspecting potentially invalid TNIDs to understand why they don't parse
/// - Converting between different UUID representations (u128, hex strings) without validation
/// - Working with UUIDs from external systems that may not be TNIDs
/// - Debugging and troubleshooting TNID-related issues
///
/// # Examples
///
/// Basic usage:
/// ```rust
/// use tnid::{Case, UuidLike};
///
/// // Create from any 128-bit value
/// let uuid_like = UuidLike::new(0x12345678_1234_1234_1234_123456789abc);
///
/// // Convert to different representations
/// let as_u128 = uuid_like.as_u128();
/// let as_string = uuid_like.to_uuid_string(Case::Lower);
/// ```
///
/// Inspecting potentially invalid TNIDs:
/// ```rust
/// use tnid::{UuidLike, Tnid, TnidName, NameStr};
///
/// struct User;
/// impl TnidName for User {
///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
/// }
///
/// // Parse a UUID string that might not be a valid TNID
/// let uuid_str = "cab1952a-f09d-86d9-928e-96ea03dc6af3";
/// let uuid_like = UuidLike::parse_uuid_string(uuid_str).unwrap();
///
/// // Try to convert to TNID - this performs validation
/// match Tnid::<User>::from_u128(uuid_like.as_u128()) {
///     Ok(tnid) => println!("Valid TNID: {}", tnid),
///     Err(e) => println!("Not a valid TNID: {}", e),
/// }
/// ```
///
use crate::utils;

/// Error when parsing a UUID string.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseUuidStringError {
    /// The string is not 36 characters long.
    /// Contains the actual length.
    WrongLength(usize),
    /// A hyphen is missing at the expected position.
    /// Contains the position (8, 13, 18, or 23).
    MissingHyphen(usize),
    /// An invalid hexadecimal character was found.
    /// Contains the position and the invalid Unicode scalar value.
    InvalidHexChar {
        /// Byte index into the input string.
        position: usize,
        /// The invalid character (as read from the input).
        character: char,
    },
}

impl std::fmt::Display for ParseUuidStringError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::WrongLength(len) => {
                write!(f, "UUID string must be 36 characters, got {len}")
            }
            Self::MissingHyphen(pos) => {
                write!(f, "missing hyphen at position {pos}")
            }
            Self::InvalidHexChar {
                position,
                character,
            } => {
                write!(
                    f,
                    "invalid hex character '{}' (U+{:04X}) at position {position}",
                    character, *character as u32
                )
            }
        }
    }
}

impl std::error::Error for ParseUuidStringError {}

const UUID_HYPHEN_POSITIONS: [usize; 4] = [8, 13, 18, 23];

/// The case (upper/lower) for hexadecimal string formatting.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Case {
    /// Use lowercase hex digits (`a-f`).
    Lower,
    /// Use uppercase hex digits (`A-F`).
    Upper,
}

/// A wrapper for 128-bit values that may or may not be valid TNIDs (or UUIDs for that matter).
///
/// `UuidLike` provides parsing and formatting of UUID strings and conversion to/from `u128`,
/// without enforcing validation rules.
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct UuidLike(u128);

impl std::fmt::Debug for UuidLike {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.to_uuid_string(Case::Lower))
    }
}

impl UuidLike {
    /// Returns the raw 128-bit value.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use tnid::UuidLike;
    ///
    /// let uuid_like = UuidLike::new(0x12345678_1234_1234_1234_123456789abc);
    /// assert_eq!(uuid_like.as_u128(), 0x12345678_1234_1234_1234_123456789abc);
    /// ```
    pub fn as_u128(&self) -> u128 {
        self.0
    }

    /// Creates a new `UuidLike` from a 128-bit value.
    ///
    /// Accepts any `u128` value without validation.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use tnid::UuidLike;
    ///
    /// let uuid_like = UuidLike::new(0x12345678_1234_1234_1234_123456789abc);
    /// assert_eq!(uuid_like.as_u128(), 0x12345678_1234_1234_1234_123456789abc);
    /// ```
    pub fn new(id: u128) -> Self {
        Self(id)
    }

    /// Converts to UUID hex string format with specified case.
    ///
    /// Produces the standard UUID format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
    ///
    /// # Parameters
    ///
    /// - `case`: Whether to use uppercase (`A-F`) or lowercase (`a-f`) hex digits.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use tnid::{Case, UuidLike};
    ///
    /// let uuid_like = UuidLike::new(0xCAB1952A_F09D_86D9_928E_96EA03DC6AF3);
    ///
    /// let lowercase = uuid_like.to_uuid_string(Case::Lower);
    /// assert_eq!(lowercase, "cab1952a-f09d-86d9-928e-96ea03dc6af3");
    ///
    /// let uppercase = uuid_like.to_uuid_string(Case::Upper);
    /// assert_eq!(uppercase, "CAB1952A-F09D-86D9-928E-96EA03DC6AF3");
    /// ```
    pub fn to_uuid_string(&self, case: Case) -> String {
        utils::u128_to_uuid_string(self.0, case)
    }

    /// Parses a UUID hex string into a `UuidLike`.
    ///
    /// Accepts the standard UUID format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
    ///
    /// Accepts both uppercase and lowercase hex digits. Validates format but not TNID-specific requirements.
    ///
    /// Returns `Err` if the string is not a valid UUID hex string.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use tnid::UuidLike;
    ///
    /// // Parse lowercase
    /// let uuid = UuidLike::parse_uuid_string("cab1952a-f09d-86d9-928e-96ea03dc6af3");
    /// assert!(uuid.is_ok());
    ///
    /// // Parse uppercase
    /// let uuid = UuidLike::parse_uuid_string("CAB1952A-F09D-86D9-928E-96EA03DC6AF3");
    /// assert!(uuid.is_ok());
    ///
    /// // Parse mixed case
    /// let uuid = UuidLike::parse_uuid_string("CaB1952a-F09D-86d9-928E-96ea03dc6af3");
    /// assert!(uuid.is_ok());
    ///
    /// // Invalid format
    /// assert!(UuidLike::parse_uuid_string("not-a-uuid").is_err());
    /// ```
    pub fn parse_uuid_string(uuid_string: &str) -> Result<Self, ParseUuidStringError> {
        let bytes = uuid_string.as_bytes();

        if bytes.len() != 36 {
            return Err(ParseUuidStringError::WrongLength(uuid_string.len()));
        }

        for &pos in &UUID_HYPHEN_POSITIONS {
            if bytes.get(pos) != Some(&b'-') {
                return Err(ParseUuidStringError::MissingHyphen(pos));
            }
        }

        let mut iter = bytes
            .iter()
            .enumerate()
            .filter(|(i, _)| !UUID_HYPHEN_POSITIONS.contains(i));

        let mut id: u128 = 0;

        for _ in 0..16 {
            let (i1, b1) = iter
                .next()
                .ok_or(ParseUuidStringError::WrongLength(uuid_string.len()))?;
            let (i2, b2) = iter
                .next()
                .ok_or(ParseUuidStringError::WrongLength(uuid_string.len()))?;

            let Some(h) = utils::hex_char_to_nibble(*b1) else {
                return Err(ParseUuidStringError::InvalidHexChar {
                    position: i1,
                    character: *b1 as char,
                });
            };
            let Some(l) = utils::hex_char_to_nibble(*b2) else {
                return Err(ParseUuidStringError::InvalidHexChar {
                    position: i2,
                    character: *b2 as char,
                });
            };

            let byte = (h << 4) | l;
            id = (id << 8) | (byte as u128);
        }

        if iter.next().is_some() {
            return Err(ParseUuidStringError::WrongLength(uuid_string.len()));
        }

        Ok(Self(id))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_lowercase() {
        let result = UuidLike::parse_uuid_string("ffffffff-ffff-ffff-ffff-ffffffffffff");
        assert_eq!(result.expect("valid UUID").as_u128(), u128::MAX);
    }

    #[test]
    fn parse_uppercase() {
        let result = UuidLike::parse_uuid_string("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF");
        assert_eq!(result.expect("valid UUID").as_u128(), u128::MAX);
    }

    #[test]
    fn parse_mixed_case() {
        let result = UuidLike::parse_uuid_string("AaBbCcDd-1234-5678-90aB-cDeF01234567");
        assert!(result.is_ok());
    }

    #[test]
    fn parse_all_zeros() {
        let result = UuidLike::parse_uuid_string("00000000-0000-0000-0000-000000000000");
        assert_eq!(result.expect("valid UUID").as_u128(), 0);
    }
}