puuid 0.2.0

Prefixed UUIDs: Type-safe, string-prefixed UUIDs that behave like standard UUIDs.
Documentation
use crate::Prefix;
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
use uuid::Uuid;

/// A Prefixed UUID.
///
/// It wraps a standard [`Uuid`] but is strictly typed to a specific [`Prefix`].
///
/// # Type Parameters
/// * `T`: A unit struct implementing the [`Prefix`] trait.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Puuid<T: Prefix> {
    inner: Uuid,
    _marker: PhantomData<T>,
}

impl<T: Prefix> Puuid<T> {
    /// Create a new Puuid from an existing raw Uuid.
    pub const fn from_uuid(uuid: Uuid) -> Self {
        Self {
            inner: uuid,
            _marker: PhantomData,
        }
    }

    /// Get the underlying raw Uuid.
    pub const fn into_inner(self) -> Uuid {
        self.inner
    }

    /// Get the prefix string for this type (without the underscore).
    pub const fn prefix() -> &'static str {
        T::VALUE
    }
}

// --- Version Specific Constructors ---
impl<T: Prefix> Puuid<T> {
    #[cfg(feature = "v4")]
    pub fn new_v4() -> Self {
        Self::from_uuid(Uuid::new_v4())
    }

    #[cfg(feature = "v7")]
    pub fn new_v7() -> Self {
        Self::from_uuid(Uuid::now_v7())
    }

    #[cfg(feature = "v3")]
    pub fn new_v3(namespace: &Uuid, name: &[u8]) -> Self {
        Self::from_uuid(Uuid::new_v3(namespace, name))
    }

    #[cfg(feature = "v5")]
    pub fn new_v5(namespace: &Uuid, name: &[u8]) -> Self {
        Self::from_uuid(Uuid::new_v5(namespace, name))
    }
}

// --- Standard Trait Implementations ---

// Deref allows access to standard Uuid methods
impl<T: Prefix> std::ops::Deref for Puuid<T> {
    type Target = Uuid;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl<T: Prefix> fmt::Display for Puuid<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}_{}", T::VALUE, self.inner)
    }
}

impl<T: Prefix> FromStr for Puuid<T> {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let prefix = T::VALUE;

        // IMPROVED LOGIC: Split first, then validate.
        // This ensures "Wrong prefix" errors appear even if lengths differ.

        // 1. Split prefix
        let (found_prefix, uuid_str) = s
            .split_once('_')
            .ok_or_else(|| "Missing '_' separator".to_string())?;

        // 2. Verify prefix
        if found_prefix != prefix {
            return Err(format!(
                "Wrong prefix. Expected '{}', found '{}'",
                prefix, found_prefix
            ));
        }

        // 3. Parse UUID
        // Uuid::from_str handles length and character validation robustness
        let uuid = Uuid::from_str(uuid_str).map_err(|e| format!("Invalid UUID format: {}", e))?;

        Ok(Self::from_uuid(uuid))
    }
}

impl<T: Prefix> Default for Puuid<T> {
    fn default() -> Self {
        #[cfg(feature = "v7")]
        return Self::new_v7();
        #[cfg(all(not(feature = "v7"), feature = "v4"))]
        return Self::new_v4();
        #[cfg(all(not(feature = "v7"), not(feature = "v4")))]
        return Self::from_uuid(Uuid::nil());
    }
}

// --- Unit Tests for Wrapper ---
#[cfg(test)]
mod tests {
    use super::*;
    use crate::prefix;

    prefix!(TestPre, "test");
    type TestId = Puuid<TestPre>;

    #[test]
    fn test_display_format() {
        let raw = Uuid::nil();
        let id = TestId::from_uuid(raw);
        assert_eq!(id.to_string(), "test_00000000-0000-0000-0000-000000000000");
    }

    #[test]
    fn test_parsing_success() {
        let input = "test_018c6427-4f30-7f89-a1b2-c3d4e5f67890";
        let id = TestId::from_str(input).expect("Should parse");
        assert_eq!(id.to_string(), input);
    }

    #[test]
    fn test_parsing_wrong_prefix() {
        // "wrong" is 5 chars, "test" is 4 chars.
        // With the new logic, this correctly returns "Wrong prefix" instead of "Invalid length"
        let input = "wrong_018c6427-4f30-7f89-a1b2-c3d4e5f67890";
        let err = TestId::from_str(input).unwrap_err();
        assert!(err.contains("Wrong prefix"));
    }

    #[test]
    fn test_parsing_bad_format() {
        let input = "test_notauuid";
        let err = TestId::from_str(input).unwrap_err();
        // Updated expectation: now it fails at UUID parsing step, not length check
        assert!(err.contains("Invalid UUID"));
    }
}

#[cfg(feature = "sqlx")]
mod sqlx_impl {
    use super::*;
    use sqlx::{
        Database, Decode, Type,
        encode::{Encode, IsNull},
        error::BoxDynError,
    };

    // Type implementation - maps to TEXT
    impl<T: Prefix, DB: Database> Type<DB> for Puuid<T>
    where
        str: Type<DB>,
    {
        fn type_info() -> <DB as Database>::TypeInfo {
            <str as Type<DB>>::type_info()
        }

        fn compatible(ty: &<DB as Database>::TypeInfo) -> bool {
            <str as Type<DB>>::compatible(ty)
        }
    }

    // Decode - convert DB string to Puuid
    impl<'r, T: Prefix, DB: Database> Decode<'r, DB> for Puuid<T>
    where
        &'r str: Decode<'r, DB>,
    {
        fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
            let s = <&str as Decode<DB>>::decode(value)?;
            let puuid = Self::from_str(s).map_err(|e| BoxDynError::from(e))?;
            Ok(puuid)
        }
    }

    // Encode - convert Puuid to DB string
    impl<'q, T: Prefix, DB: Database> Encode<'q, DB> for Puuid<T>
    where
        String: Encode<'q, DB>,
    {
        fn encode_by_ref(
            &self,
            buf: &mut <DB as Database>::ArgumentBuffer<'q>,
        ) -> Result<IsNull, BoxDynError> {
            <String as Encode<DB>>::encode(self.to_string(), buf)
        }
    }
}