actix-cloud-extra 0.1.6

Extra tools for Actix Cloud.
Documentation
//! [`HyUuid`]: a strictly hyphenated UUID wrapper used as the universal ID type.

use std::fmt::{self, Display};

use anyhow::{Result, bail};
#[cfg(feature = "seaorm")]
use sea_orm::{TryFromU64, prelude::*};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use uuid::Uuid;

/// Wrapper of [`Uuid`] that is always serialized and displayed as a 36-char
/// hyphenated string.
///
/// [`Default`] returns the nil UUID; use [`HyUuid::new`] for a random v4 UUID.
/// Via `DeriveValueType` it can be used directly as a SeaORM column type.
#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, Default, DeriveValueType)]
pub struct HyUuid(pub Uuid);

impl Display for HyUuid {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.hyphenated().fmt(f)
    }
}

impl HyUuid {
    /// The nil UUID (`00000000-0000-0000-0000-000000000000`).
    pub const fn nil() -> Self {
        Self(Uuid::nil())
    }

    /// Whether this is the nil UUID.
    pub const fn is_nil(&self) -> bool {
        self.0.is_nil()
    }

    /// Generate a random UUID v4.
    pub fn new() -> Self {
        Self(Uuid::new_v4())
    }

    /// Parse a 36-char hyphenated UUID v4 string (hex is case-insensitive).
    ///
    /// # Errors
    /// Will return `Err` when `str` is not a 36-char hyphenated UUID, or not
    /// version 4.
    pub fn parse(str: &str) -> Result<Self> {
        if str.len() != 36 {
            bail!("uuid error: uuid must be 36 bytes");
        }
        let u = Uuid::parse_str(str)?;
        if u.get_version_num() != 4 {
            bail!("uuid error: only uuid v4 is allowed");
        }
        Ok(Self(u))
    }
}

// Deliberately unimplemented: SeaORM only requires `TryFromU64` for
// auto-increment primary keys, while `HyUuid` IDs are generated by the
// application (e.g. via `entity_id`) and must be declared
// `auto_increment = false`.
#[cfg(feature = "seaorm")]
impl TryFromU64 for HyUuid {
    fn try_from_u64(_: u64) -> Result<Self, DbErr> {
        Err(DbErr::ConvertFromU64(stringify!(HyUuid)))
    }
}

// Always serialize as the lowercase 36-char hyphenated form.
impl Serialize for HyUuid {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.0.hyphenated().encode_lower(&mut Uuid::encode_buffer()))
    }
}

// Strictly deserialize from a 36-char hyphenated string (see [`HyUuid::parse`]);
// other textual formats (simple, braced, URN) are rejected.
impl<'de> Deserialize<'de> for HyUuid {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        struct UuidVisitor;

        impl de::Visitor<'_> for UuidVisitor {
            type Value = HyUuid;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(formatter, "a UUID v4 string with hyphens")
            }

            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                HyUuid::parse(value).map_err(|e| de::Error::custom(e))
            }
        }

        deserializer.deserialize_str(UuidVisitor)
    }
}

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

    #[test]
    fn nil() {
        let n = HyUuid::nil();
        assert!(n.is_nil());
        assert_eq!(n.to_string(), "00000000-0000-0000-0000-000000000000");
    }

    #[test]
    fn new_not_nil() {
        let a = HyUuid::new();
        assert!(!a.is_nil());
    }

    #[test]
    fn parse_ok() {
        let s = "550e8400-e29b-41d4-a716-446655440000";
        let u = HyUuid::parse(s).unwrap();
        assert_eq!(u.to_string(), s);
    }

    #[test]
    fn parse_wrong_len() {
        assert!(HyUuid::parse("550e8400-e29b-41d4-a716-44665544000").is_err());
        assert!(HyUuid::parse("550e8400-e29b-41d4-a716-4466554400000").is_err());
    }

    #[test]
    fn parse_bad_hex() {
        assert!(HyUuid::parse("550e8400-e29b-41d4-a716-44665544000z").is_err());
    }

    #[test]
    fn parse_wrong_version() {
        // Structurally valid v1 / v7 UUIDs must be rejected.
        assert!(HyUuid::parse("550e8400-e29b-11d4-a716-446655440000").is_err());
        assert!(HyUuid::parse("550e8400-e29b-71d4-a716-446655440000").is_err());
    }

    #[test]
    fn serde_roundtrip() {
        let u = HyUuid::new();
        let s = serde_json::to_string(&u).unwrap();
        let back: HyUuid = serde_json::from_str(&s).unwrap();
        assert_eq!(u, back);
    }

    #[test]
    fn serde_bad_input() {
        assert!(serde_json::from_str::<HyUuid>("\"not-a-uuid\"").is_err());
    }
}