entid 0.4.3

A library for generating and validating type-safe, prefixed entity identifiers based on UUIDs and ULIDs
Documentation
/* Copyright © 2025, CosmicMind, Inc. */

use entid::{EntityId, Prefix, UlidEntityId, UlidIdentifier, Uuid, UuidEntityId, UuidIdentifier};

// Define test entity types
#[derive(Debug)]
struct User;
impl Prefix for User {
    fn prefix() -> &'static str {
        "user"
    }
}

#[derive(Debug)]
struct Post;
impl Prefix for Post {
    fn prefix() -> &'static str {
        "post"
    }

    fn delimiter() -> &'static str {
        "-"
    }
}

#[test]
fn test_uuid_entity_id() {
    // Generate a UUID-based entity ID
    let user_id = UuidEntityId::<User>::generate();

    // Test string representation
    let id_str = user_id.as_str();
    assert!(id_str.starts_with("user_"));

    // Test parsing
    let parsed_id = UuidEntityId::<User>::new(id_str).unwrap();
    assert_eq!(user_id, parsed_id);

    // Test error handling
    let result = UuidEntityId::<User>::new("invalid");
    assert!(result.is_err());

    let result = UuidEntityId::<User>::new("post_123e4567-e89b-12d3-a456-426614174000");
    assert!(result.is_err());
}

#[test]
fn test_ulid_entity_id() {
    // Generate a ULID-based entity ID
    let post_id = UlidEntityId::<Post>::generate();

    // Test string representation
    let id_str = post_id.as_str();
    assert!(id_str.starts_with("post-"));

    // Test parsing
    let parsed_id = UlidEntityId::<Post>::new(id_str).unwrap();
    assert_eq!(post_id, parsed_id);

    // Test timestamp
    assert!(post_id.timestamp_ms().is_some());
}

#[test]
fn test_uuid_v5_deterministic() {
    // Create a namespace
    let namespace = Uuid::NAMESPACE_DNS;

    // Create deterministic UUIDs
    let uuid1 = UuidIdentifier::new_v5(&namespace, "example.com");
    let uuid2 = UuidIdentifier::new_v5(&namespace, "example.com");

    // Same input should produce same UUID
    assert_eq!(uuid1, uuid2);

    // Create entity IDs
    let id1 = EntityId::<User, UuidIdentifier>::from_identifier(uuid1);
    let id2 = EntityId::<User, UuidIdentifier>::from_identifier(uuid2);

    assert_eq!(id1, id2);
}

#[test]
fn test_ulid_monotonic() {
    // Create a sequence of monotonic ULIDs
    let id1 = UlidEntityId::<User>::generate();

    // Create a monotonic ULID based on the previous one
    let ulid2 = UlidIdentifier::monotonic_from(Some(id1.identifier()));
    let id2 = EntityId::<User, UlidIdentifier>::from_identifier(ulid2);

    // Second ID should be greater than first (compare timestamps)
    let ts1 = id1.timestamp_ms().unwrap();
    let ts2 = id2.timestamp_ms().unwrap();
    assert!(ts2 >= ts1);

    // Compare the raw ULIDs
    assert!(id2.identifier().ulid() > id1.identifier().ulid());
}

#[test]
fn test_type_safety() {
    let _user_id = UuidEntityId::<User>::generate();
    let _post_id = UlidEntityId::<Post>::generate();

    // Different entity types cannot be compared
    // This would not compile:
    // assert_ne!(user_id, post_id);

    // Different identifier types cannot be compared
    // This would not compile:
    // let user_ulid_id = UlidEntityId::<User>::generate();
    // assert_ne!(user_id, user_ulid_id);
}

#[test]
fn test_serde() {
    // Test UUID serialization/deserialization
    let user_id = UuidEntityId::<User>::generate();
    let serialized = serde_json::to_string(&user_id).unwrap();
    let deserialized: UuidEntityId<User> = serde_json::from_str(&serialized).unwrap();
    assert_eq!(user_id, deserialized);

    // Test ULID serialization/deserialization
    let post_id = UlidEntityId::<Post>::generate();
    let serialized = serde_json::to_string(&post_id).unwrap();
    let deserialized: UlidEntityId<Post> = serde_json::from_str(&serialized).unwrap();
    assert_eq!(post_id, deserialized);
}

#[test]
fn test_into_trait() {
    // Test conversion from EntityId to UuidIdentifier
    let user_id = UuidEntityId::<User>::generate();
    let uuid_id: UuidIdentifier = user_id.clone().into();
    assert_eq!(user_id.identifier(), &uuid_id);

    // Test conversion from EntityId to String
    let id_str: String = user_id.clone().into();
    assert_eq!(id_str, user_id.as_str());

    // Test conversion from &EntityId to UuidIdentifier
    let uuid_id_ref: UuidIdentifier = (&user_id).into();
    assert_eq!(user_id.identifier(), &uuid_id_ref);

    // Test conversion from &EntityId to String
    let id_str_ref: String = (&user_id).into();
    assert_eq!(id_str_ref, user_id.as_str());

    // Test conversion from UuidIdentifier to EntityId
    let uuid = UuidIdentifier::new_v4();
    let entity_id = EntityId::<User, UuidIdentifier>::from(uuid);
    assert_eq!(entity_id.identifier(), &uuid);

    // Test conversion from UlidIdentifier to EntityId
    let ulid = UlidIdentifier::new();
    let entity_id = EntityId::<User, UlidIdentifier>::from(ulid);
    assert_eq!(entity_id.identifier(), &ulid);

    // Test conversion from EntityId to UlidIdentifier
    let post_id = UlidEntityId::<Post>::generate();
    let ulid_id: UlidIdentifier = post_id.clone().into();
    assert_eq!(post_id.identifier(), &ulid_id);
}

#[test]
fn test_error_traits() {
    use entid::{EntityIdError, IdentifierError};

    // Test AsRef<str> for EntityIdError
    let err = EntityIdError::InvalidFormat;
    let err_str: &str = err.as_ref();
    assert_eq!(err_str, "The provided ID has an invalid format");

    // Test Into<String> for EntityIdError
    let err_string: String = err.into();
    assert_eq!(err_string, "The provided ID has an invalid format");

    // Test Into<String> for &EntityIdError
    let err = EntityIdError::InvalidIdentifier;
    let err_string: String = (&err).into();
    assert_eq!(err_string, "ID must contain a valid identifier");

    // Test Into<String> for IdentifierError
    let uuid_err = uuid::Uuid::parse_str("not-a-uuid").unwrap_err();
    let err = IdentifierError::Uuid(uuid_err);
    let err_string: String = err.into();
    assert!(err_string.starts_with("Invalid UUID format:"));

    // Create a new error for testing methods
    let uuid_err = uuid::Uuid::parse_str("not-a-uuid").unwrap_err();
    let err = IdentifierError::Uuid(uuid_err.clone());

    // Test error_message method
    let err_message = err.error_message();
    assert_eq!(err_message, uuid_err.to_string());

    // Test uuid_error method
    assert!(err.uuid_error().is_some());
    assert!(err.ulid_error().is_none());

    // Test Into<String> for &IdentifierError
    let ulid_err = ulid::DecodeError::InvalidLength;
    let err = IdentifierError::Ulid(ulid_err.clone());
    let err_string: String = (&err).into();
    assert!(err_string.starts_with("Invalid ULID format:"));

    // Test ulid_error method
    assert!(err.ulid_error().is_some());
    assert!(err.uuid_error().is_none());

    // Test error_message method for ULID error
    let err_message = err.error_message();
    assert_eq!(err_message, ulid_err.to_string());

    // Test Display implementation
    let err_string = format!("{}", err);
    assert!(err_string.starts_with("Invalid ULID format:"));
}

#[test]
fn test_conversion_methods() {
    // Test to_raw_string method
    let user_id = UuidEntityId::<User>::generate();
    let raw_string = user_id.to_raw_string();
    assert_eq!(raw_string, user_id.id_str().to_string());

    // Test to_identifier method
    let uuid_identifier = user_id.to_identifier();
    assert_eq!(uuid_identifier, *user_id.identifier());

    // Test AsRef<str> implementation
    let id_str: &str = user_id.as_ref();
    assert_eq!(id_str, user_id.as_str());

    // Test Borrow<str> implementation
    use std::borrow::Borrow;
    let id_str_borrow: &str = user_id.borrow();
    assert_eq!(id_str_borrow, user_id.as_str());
}