uvb-core 0.2.1

Core error types and shared primitives for the UVB authentication platform
Documentation
use serde::{Deserialize, Serialize};
use std::fmt;

/// Type-safe tenant identifier wrapper to prevent cross-tenant data leakage
///
/// This newtype provides compile-time guarantees that:
/// 1. Tenant IDs cannot be accidentally mixed with other string types
/// 2. All tenant-scoped operations explicitly require a TenantId
/// 3. Cross-tenant operations are caught at compile time
///
/// # Security
///
/// By making TenantId a distinct type, we enforce that all storage operations,
/// queries, and business logic explicitly handle tenant isolation. This prevents
/// entire classes of security vulnerabilities where tenant_id strings might be
/// accidentally omitted, swapped, or compared incorrectly.
///
/// # Examples
///
/// ```
/// use uvb_core::TenantId;
///
/// // Create a tenant ID
/// let tenant_id = TenantId::new("tenant_123");
///
/// // Access the inner value when needed
/// assert_eq!(tenant_id.as_str(), "tenant_123");
///
/// // Clone is cheap (wraps a String)
/// let cloned = tenant_id.clone();
/// assert_eq!(tenant_id, cloned);
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TenantId(String);

impl TenantId {
    /// Create a new TenantId from a string
    ///
    /// # Arguments
    ///
    /// * `id` - The tenant identifier string
    ///
    /// # Examples
    ///
    /// ```
    /// use uvb_core::TenantId;
    ///
    /// let tenant_id = TenantId::new("tenant_123");
    /// ```
    pub fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }

    /// Get the tenant ID as a string slice
    ///
    /// # Examples
    ///
    /// ```
    /// use uvb_core::TenantId;
    ///
    /// let tenant_id = TenantId::new("tenant_123");
    /// assert_eq!(tenant_id.as_str(), "tenant_123");
    /// ```
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Consume the TenantId and return the inner String
    ///
    /// # Examples
    ///
    /// ```
    /// use uvb_core::TenantId;
    ///
    /// let tenant_id = TenantId::new("tenant_123");
    /// let inner: String = tenant_id.into_inner();
    /// assert_eq!(inner, "tenant_123");
    /// ```
    pub fn into_inner(self) -> String {
        self.0
    }

    /// Validate that the tenant ID is well-formed
    ///
    /// Returns `true` if the tenant ID:
    /// - Is not empty
    /// - Contains only alphanumeric characters, hyphens, and underscores
    /// - Is between 1 and 255 characters long
    ///
    /// # Examples
    ///
    /// ```
    /// use uvb_core::TenantId;
    ///
    /// assert!(TenantId::new("tenant_123").is_valid());
    /// assert!(TenantId::new("tenant-abc-123").is_valid());
    /// assert!(!TenantId::new("").is_valid());
    /// assert!(!TenantId::new("tenant with spaces").is_valid());
    /// ```
    pub fn is_valid(&self) -> bool {
        !self.0.is_empty()
            && self.0.len() <= 255
            && self
                .0
                .chars()
                .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
    }
}

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

impl AsRef<str> for TenantId {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl From<String> for TenantId {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl From<&str> for TenantId {
    fn from(s: &str) -> Self {
        Self(s.to_string())
    }
}

impl From<TenantId> for String {
    fn from(tenant_id: TenantId) -> Self {
        tenant_id.0
    }
}

// Allow comparing TenantId with &str and String for convenience
impl PartialEq<str> for TenantId {
    fn eq(&self, other: &str) -> bool {
        self.0 == other
    }
}

impl PartialEq<&str> for TenantId {
    fn eq(&self, other: &&str) -> bool {
        self.0 == *other
    }
}

impl PartialEq<String> for TenantId {
    fn eq(&self, other: &String) -> bool {
        &self.0 == other
    }
}

impl PartialEq<TenantId> for String {
    fn eq(&self, other: &TenantId) -> bool {
        self == &other.0
    }
}

impl PartialEq<TenantId> for &str {
    fn eq(&self, other: &TenantId) -> bool {
        *self == other.0.as_str()
    }
}

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

    #[test]
    fn test_tenant_id_creation() {
        let tenant_id = TenantId::new("tenant_123");
        assert_eq!(tenant_id.as_str(), "tenant_123");
        assert_eq!(tenant_id.to_string(), "tenant_123");
    }

    #[test]
    fn test_tenant_id_clone() {
        let tenant_id = TenantId::new("tenant_123");
        let cloned = tenant_id.clone();
        assert_eq!(tenant_id, cloned);
    }

    #[test]
    fn test_tenant_id_from_string() {
        let s = String::from("tenant_123");
        let tenant_id: TenantId = s.into();
        assert_eq!(tenant_id.as_str(), "tenant_123");
    }

    #[test]
    fn test_tenant_id_from_str() {
        let tenant_id: TenantId = "tenant_123".into();
        assert_eq!(tenant_id.as_str(), "tenant_123");
    }

    #[test]
    fn test_tenant_id_into_string() {
        let tenant_id = TenantId::new("tenant_123");
        let s: String = tenant_id.into();
        assert_eq!(s, "tenant_123");
    }

    #[test]
    fn test_tenant_id_validation() {
        // Valid cases
        assert!(TenantId::new("tenant_123").is_valid());
        assert!(TenantId::new("tenant-abc-123").is_valid());
        assert!(TenantId::new("a").is_valid());
        assert!(TenantId::new("123").is_valid());
        assert!(TenantId::new("tenant_with_underscores").is_valid());
        assert!(TenantId::new("tenant-with-hyphens").is_valid());

        // Invalid cases
        assert!(!TenantId::new("").is_valid());
        assert!(!TenantId::new("tenant with spaces").is_valid());
        assert!(!TenantId::new("tenant@example.com").is_valid());
        assert!(!TenantId::new("tenant/path").is_valid());
        assert!(!TenantId::new("x".repeat(256)).is_valid());
    }

    #[test]
    fn test_tenant_id_equality() {
        let tenant_id1 = TenantId::new("tenant_123");
        let tenant_id2 = TenantId::new("tenant_123");
        let tenant_id3 = TenantId::new("tenant_456");

        assert_eq!(tenant_id1, tenant_id2);
        assert_ne!(tenant_id1, tenant_id3);
    }

    #[test]
    fn test_tenant_id_serialization() {
        let tenant_id = TenantId::new("tenant_123");
        let json = serde_json::to_string(&tenant_id).unwrap();
        assert_eq!(json, r#""tenant_123""#);

        let deserialized: TenantId = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, tenant_id);
    }

    #[test]
    fn test_tenant_id_hash() {
        use std::collections::HashMap;

        let mut map = HashMap::new();
        let tenant_id = TenantId::new("tenant_123");
        map.insert(tenant_id.clone(), "value");

        assert_eq!(map.get(&tenant_id), Some(&"value"));
    }

    #[test]
    fn test_tenant_id_string_comparison() {
        let tenant_id = TenantId::new("tenant_123");
        let string = String::from("tenant_123");
        let str_ref = "tenant_123";

        // TenantId == String
        assert_eq!(tenant_id, string);
        assert_eq!(string, tenant_id);

        // TenantId == &str
        assert_eq!(tenant_id, str_ref);
        assert_eq!(str_ref, tenant_id);

        // TenantId == str (through deref)
        assert!(tenant_id == *str_ref);

        // Inequality
        assert_ne!(tenant_id, "different_tenant");
        assert_ne!(String::from("different_tenant"), tenant_id);
    }
}