Content-Length: 514335 | pFad | http://github.com/CosmicMind/entid

BE GitHub - CosmicMind/entid: An entity id generator and validator for Rust models.
Skip to content

An entity id generator and validator for Rust models.

License

Notifications You must be signed in to change notification settings

CosmicMind/entid

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 Cannot retrieve latest commit at this time.

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

entid

A Rust library for generating and validating type-safe, prefixed entity identifiers based on UUIDs and ULIDs.

Crates.io Documentation License: MIT

Features

  • Type-safe entity IDs: Create distinct ID types for different entities
  • Multiple identifier formats: Support for both UUID and ULID
  • Prefix support: Automatically add entity-specific prefixes to IDs
  • Performance optimized: Thread-safe caching of string representations
  • Serde compatible: Seamless serialization and deserialization
  • Comprehensive error handling: Clear error types for all operations
  • Zero-cost abstractions: Minimal runtime overhead
  • Derive macro for implementing the Prefix trait: Optional

Installation

Add this to your Cargo.toml:

[dependencies]
entid = "0.4.3"

To use the derive macro for implementing the Prefix trait, enable the derive feature:

[dependencies]
entid = { version = "0.4.3", features = ["derive"] }

API Overview

The EntityId type provides several methods for working with entity IDs:

type UserId = UuidEntityId::<User>;

// Create a new EntityId
let user_id = UserId::generate();

// Get the full ID string with prefix (e.g., "user_123e4567-e89b-12d3-a456-426614174000")
let full_id = user_id.as_str();

// Get just the identifier part without the prefix (e.g., "123e4567-e89b-12d3-a456-426614174000")
let raw_id = user_id.id_str();

// Get a reference to the underlying identifier object
let identifier = user_id.identifier();

// Get the identifier string directly from the identifier
let id_str = user_id.identifier().as_str();

// Get the prefix for this entity type
let prefix = UserId::prefix(); // "user"

// Get the delimiter for this entity type
let delimiter = UserId::delimiter(); // "_"

// For ULID-based IDs, get the timestamp
if let Some(timestamp_ms) = ulid_id.timestamp_ms() {
    println!("ID created at: {} ms since epoch", timestamp_ms);
}

Flexible Creation Methods

The library provides multiple ways to create entity IDs:

use entid::{EntityId, Identifier, Prefix, UuidEntityId, UlidEntityId, Uuid, Ulid};

type UserId = UuidEntityId::<User>;

// Using the generate method
let user_id1 = UserId::generate();

// Using the new method with flexible string types (with prefix)
let id_str = "user_123e4567-e89b-12d3-a456-426614174000";
let user_id2 = UserId::new(id_str).unwrap();
let user_id3 = UserId::new(id_str.to_string()).unwrap();

// Using from_raw_str to parse a raw identifier string (without prefix)
let raw_uuid = "123e4567-e89b-12d3-a456-426614174000";
let user_id4 = UserId::from_raw_str(raw_uuid).unwrap();

// Using parse_raw_str with custom error handling
let user_id5 = UserId::parse_raw_str(raw_uuid, |e| format!("Invalid UUID: {}", e)).unwrap();

// Using TryFrom trait
let user_id6 = UserId::try_from(id_str).unwrap();
let user_id7 = UserId::try_from(id_str.to_string()).unwrap();

// Using FromStr trait
let user_id8 = id_str.parse::<UserId>().unwrap();

// Using convenience methods
let uuid = Uuid::new_v4();
let user_id9 = UserId::with_uuid(uuid);
let user_id10 = UserId::new_v4();
let user_id11 = UserId::new_v5(&Uuid::NAMESPACE_DNS, "example.com");

// Using the builder pattern
let user_id12 = UserId::builder().build();
let user_id13 = UserId::builder().with_uuid(uuid).build();
let user_id14 = UserId::builder().with_uuid_v4().build();
let user_id15 = UserId::builder().with_uuid_v5(&Uuid::NAMESPACE_DNS, "example.com").build();

// For ULID-based IDs
type PostId = UlidEntityId::<Post>;

let ulid = Ulid::new();
let post_id1 = PostId::with_ulid(ulid);
let post_id2 = PostId::with_timestamp(1625097600000); // July 1, 2021
let post_id3 = PostId::monotonic_from(Some(&post_id2));

// Using the builder pattern for ULID
let post_id4 = PostId::builder().with_ulid(ulid).build();
let post_id5 = PostId::builder().with_timestamp(1625097600000).build();
let post_id6 = PostId::builder().with_monotonic_from(Some(&post_id5)).build();

Using EntityId in Collections

The EntityId type implements Borrow<str> and AsRef<str>, making it easy to use in collections:

use std::collections::{HashMap, HashSet};

// Use EntityId as a key in a HashMap
let mut user_map = HashMap::new();
user_map.insert(user_id1, "John Doe");

// Look up by string
let user = user_map.get(id_str);

// Use EntityId in a HashSet
let mut user_set = HashSet::new();
user_set.insert(user_id1);

// Check if a string is in the set
let contains = user_set.contains(id_str);

Usage

Basic Example with UUID

use entid::{EntityId, Prefix, UuidIdentifier, UuidEntityId};

type UserId = UuidEntityId::<User>;

// Define your entity types with custom prefixes
struct User;
impl Prefix for User {
    fn prefix() -> &'static str {
        "user"
    }

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

type PostId = EntityId::<Post, UuidIdentifier>;

struct Post;
impl Prefix for Post {
    fn prefix() -> &'static str {
        "post"
    }
    
    // Optional: Override the default delimiter
    fn delimiter() -> &'static str {
        "-"
    }
}

fn main() {
    // Generate random IDs with UUID
    let user_id = UserId::generate();
    let post_id = PostId::generate();
    
    // Print the IDs
    println!("User ID: {}", user_id); // e.g., "user_6ba7b810-9dad-11d1-80b4-00c04fd430c8"
    println!("Post ID: {}", post_id); // e.g., "post-123e4567-e89b-12d3-a456-426614174000"
    
    // Parse existing IDs
    let parsed_user_id = UserId::new("user_6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap();
    
    // Type safety prevents mixing different entity IDs
    // This won't compile:
    // let wrong: UuidEntityId<Post> = user_id;
}

Using the Derive Macro

With the derive feature enabled, you can use the derive macro to implement the Prefix trait:

use entid::{Prefix, UuidEntityId, UlidEntityId};

type UserId = UuidEntityId::<User>;

#[derive(Prefix)]
#[entid(prefix = "user", delimiter = "_")]
struct User;

type PostId = UlidEntityId::<Post>;

#[derive(Prefix)]
#[entid(prefix = "post", delimiter = "-")]
struct Post;

type CommentId = UuidEntityId::<Comment>;

// The delimiter is optional and defaults to "_"
#[derive(Prefix)]
#[entid(prefix = "comment")]
struct Comment;

fn main() {
    let user_id = UserId::generate();
    println!("User ID: {}", user_id); // e.g., "user_6ba7b810-9dad-11d1-80b4-00c04fd430c8"
    
    let post_id = PostId::generate();
    println!("Post ID: {}", post_id); // e.g., "post-01H1VECZJYJ1QV2V0D0000JJDX"
    
    let comment_id = CommentId::generate();
    println!("Comment ID: {}", comment_id); // e.g., "comment_6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}

Using ULID Instead of UUID

use entid::{EntityId, Prefix, UlidIdentifier, UlidEntityId};

type ProductId = UlidEntityId::<Product>;

struct Product;
impl Prefix for Product {
    fn prefix() -> &'static str {
        "prod"
    }
}

fn main() {
    // Generate a ULID-based ID
    let product_id = ProductId::generate();
    
    // ULIDs are lexicographically sortable by creation time
    let product_ids: Vec<UProductId> = (0..10)
        .map(|_| ProductId::generate())
        .collect();
    
    // Sorting will order by creation time
    let mut sorted_ids = product_ids.clone();
    sorted_ids.sort();
    
    // Get the timestamp from a ULID (not available with UUID)
    if let Some(timestamp_ms) = product_id.timestamp_ms() {
        println!("Product ID created at: {} ms since epoch", timestamp_ms);
    }
}

Using Deterministic UUIDs (v5)

use entid::{EntityId, Prefix, UuidIdentifier, Uuid};

type ApiKeyToken = EntityId::<ApiKey, UuidIdentifier>;

struct ApiKey;
impl Prefix for ApiKey {
    fn prefix() -> &'static str {
        "key"
    }
}

fn main() {
    // Create a namespace for your application
    let namespace = Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap();
    
    // Create a UUID v5 identifier
    let uuid_id = UuidIdentifier::new_v5(&namespace, "user@example.com");
    
    // Create an entity ID from the identifier
    let api_key = ApiKeyToken::from_identifier(uuid_id);
    
    // Same input produces the same ID
    let uuid_id2 = UuidIdentifier::new_v5(&namespace, "user@example.com");
    let api_key2 = ApiKeyToken::from_identifier(uuid_id2);
    
    assert_eq!(api_key, api_key2);
}

Enhanced Error Handling

The library provides detailed error information and convenient methods for error handling:

use entid::{EntityId, EntityIdError, IdentifierError, Prefix, UuidEntityId};
use std::error::Error;

#[derive(Prefix)]
#[entid(prefix = "user")]
struct User;

type UserId = UuidEntityId<User>;

// Convert errors to strings
fn parse_user_id(input: &str) -> Result<UserId, String> {
    UserId::new(input).map_err(|e| e.to_string()) // Use Display trait
}

// Get the specific error type
fn handle_id_error(input: &str) -> Result<UserId, String> {
    match UserId::from_raw_str(input) {
        Ok(id) => Ok(id),
        Err(EntityIdError::InvalidIdentifier) => {
            // Try to parse as UUID to get more specific error
            match uuid::Uuid::parse_str(input) {
                Err(uuid_err) => Err(format!("Invalid UUID: {}", uuid_err)),
                _ => Err("Unknown identifier error".to_string()),
            }
        },
        Err(e) => Err(e.to_string()),
    }
}

// Access the underlying error directly
fn process_with_detailed_errors<S: AsRef<str>>(input: S) -> Result<UserId, String> {
    UserId::from_raw_str(input.as_ref()).map_err(|e| {
        match e {
            EntityIdError::InvalidIdentifier => {
                // Try to parse directly to get the specific error
                match uuid::Uuid::parse_str(input.as_ref()) {
                    Err(uuid_err) => {
                        let id_err = IdentifierError::Uuid(uuid_err);
                        
                        // Get the underlying UUID error
                        if let Some(uuid_err) = id_err.uuid_error() {
                            format!("UUID parsing failed: {}", uuid_err)
                        } else {
                            // Get the error message directly
                            format!("UUID parsing failed: {}", id_err.error_message())
                        }
                    },
                    _ => "Unknown identifier error".to_string(),
                }
            },
            _ => e.to_string(),
        }
    })
}

// Use the standard Error trait methods
fn log_error_details(err: &EntityIdError) {
    println!("Error: {}", err);
    
    if let Some(source) = err.source() {
        println!("Caused by: {}", source);
    }
}

Error Handling

use entid::{EntityId, EntityIdError, IdentifierError, Prefix, UuidIdentifier};

type UserId = EntityId<User, UuidIdentifier>;

struct User;
impl Prefix for User {
    fn prefix() -> &'static str {
        "user"
    }
}

fn parse_id(input: &str) -> Result<(), Box<dyn std::error::Error>> {
    // Parse an entity ID string
    match UserId::new(input) {
        Ok(id) => {
            println!("Successfully parsed ID: {}", id);
            Ok(())
        },
        Err(EntityIdError::InvalidFormat) => {
            // Handle invalid format (missing prefix or delimiter)
            println!("Invalid ID format: {}", input);
            Err(Box::new(EntityIdError::InvalidFormat))
        },
        Err(EntityIdError::InvalidIdentifier) => {
            // Handle invalid identifier (not a valid UUID/ULID)
            println!("Invalid identifier part in ID: {}", input);
            Err(Box::new(EntityIdError::InvalidIdentifier))
        }
    }
}

// Parse a raw identifier string
fn parse_raw_identifier(input: &str) -> Result<UuidIdentifier, IdentifierError> {
    UuidIdentifier::parse(input)
}

Using with Serde

use entid::{EntityId, Prefix, UlidIdentifier};
use serde::{Serialize, Deserialize};

struct Order;
impl Prefix for Order {
    fn prefix() -> &'static str {
        "order"
    }
}

type OrderId = EntityId<Order, UlidIdentifier>;

#[derive(Serialize, Deserialize)]
struct OrderRecord {
    id: OrderId,
    customer_name: String,
    amount: f64,
}

fn main() {
    let order = OrderRecord {
        id: OrderId::generate(),
        customer_name: "John Doe".to_string(),
        amount: 123.45,
    };
    
    // Serialize to JSON
    let json = serde_json::to_string(&order).unwrap();
    println!("JSON: {}", json);
    
    // Deserialize from JSON
    let deserialized: OrderRecord = serde_json::from_str(&json).unwrap();
    assert_eq!(order.id, deserialized.id);
}

Enhanced Error Handling with String Conversions

The library provides convenient error handling with string conversions through AsRef<str> and Into<String> implementations:

use entid::{EntityId, Prefix, UlidIdentifier};

type TaskId = EntityId::<Task, UlidIdentifier>;

struct Task;
impl Prefix for Task {
    fn prefix() -> &'static str {
        "task"
    }
}

fn main() {
    // Create a ULID-based entity ID
    let task1 = TaskId::generate();
    
    // Create a monotonic ULID (ensures ordering even within the same millisecond)
    let ulid2 = UlidIdentifier::monotonic_from(Some(task1.identifier()));
    let task2 = TaskId::from_identifier(ulid2);
    
    // task2 is guaranteed to sort after task1
    assert!(task2 > task1);
}

Custom Validation

use entid::{EntityId, Prefix, UuidIdentifier};

type ApiKeyToken = EntityId<ApiKey, UuidIdentifier>;

struct ApiKey;
impl Prefix for ApiKey {
    fn prefix() -> &'static str {
        "token"
    }
}

// Extend EntityId with custom validation logic
impl ApiKeyToken {
    pub fn is_valid_for_environment(&self, env: &str) -> bool {
        // Custom validation logic based on the UUID version
        match env {
            "production" => self.identifier().version() == Some(uuid::Version::Sha1),
            _ => true,
        }
    }
}

Additional Conversion Methods

The library provides additional methods for converting between different representations:

use entid::{Prefix, UuidEntityId, UuidIdentifier};

#[derive(Prefix)]
#[entid(prefix = "user")]
struct User;

type UserId = UuidEntityId<User>;

// Generate a new ID
let user_id = UserId::generate();

// Convert to raw identifier string (without prefix)
let raw_string = user_id.to_raw_string();
assert_eq!(raw_string, user_id.id_str().to_string());

// Convert to the underlying identifier type
let uuid_identifier: UuidIdentifier = user_id.to_identifier();
assert_eq!(uuid_identifier, *user_id.identifier());

// Use with functions that accept string types
fn process_string(s: impl AsRef<str>) {
    println!("Processing: {}", s.as_ref());
}

// Works directly with EntityId thanks to AsRef<str>
process_string(user_id);

Choosing Between UUID and ULID

UUID Advantages

  • Industry standard with wide adoption
  • Multiple versions for different use cases (v1, v3, v4, v5)
  • Well-supported in databases and other systems

ULID Advantages

  • Lexicographically sortable (sorts by creation time)
  • URL-safe (no special characters)
  • Shorter string representation (26 characters vs 36 for UUID)
  • Built-in timestamp component

Performance Considerations

  • String representations are cached using OnceLock for thread-safe lazy initialization
  • The EntityId type implements Hash, PartialEq, and Eq for efficient use in collections
  • Memory usage is optimized by using PhantomData for type parameters

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

An entity id generator and validator for Rust models.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages









ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/CosmicMind/entid

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy