adaptive_pipeline_domain/value_objects/generic_id.rs
1// /////////////////////////////////////////////////////////////////////////////
2// Adaptive Pipeline
3// Copyright (c) 2025 Michael Gardner, A Bit of Help, Inc.
4// SPDX-License-Identifier: BSD-3-Clause
5// See LICENSE file in the project root.
6// /////////////////////////////////////////////////////////////////////////////
7
8//! # Generic ID Value Object
9//!
10//! This module provides a generic, type-safe ID value object system for the
11//! adaptive pipeline system. It uses ULID (Universally Unique Lexicographically
12//! Sortable Identifier) with phantom types to create type-safe,
13//! category-specific identifiers.
14//!
15//! ## Overview
16//!
17//! The generic ID system provides:
18//!
19//! - **Type Safety**: Compile-time enforcement of ID categories
20//! - **ULID-Based**: Uses ULID for sortable, unique identifiers
21//! - **Category Validation**: Category-specific validation rules
22//! - **Zero-Cost Abstractions**: Phantom types with no runtime overhead
23//! - **Serialization**: Support for persistence and transmission
24//!
25//! ## Architecture
26//!
27//! The ID system follows Domain-Driven Design principles:
28//!
29//! - **Value Object**: Immutable value object with equality semantics
30//! - **Type Safety**: Phantom types prevent ID category mixing at compile time
31//! - **Rich Domain Model**: Encapsulates ID-related business logic
32//! - **Validation**: Comprehensive validation of ID formats and constraints
33//!
34//! ## Key Features
35//!
36//! ### ULID Properties
37//!
38//! - **Sortable**: Lexicographically sortable by creation time
39//! - **Unique**: Globally unique identifiers
40//! - **Compact**: 26-character string representation
41//! - **URL-Safe**: Safe for use in URLs and file names
42//! - **Case-Insensitive**: Base32 encoding is case-insensitive
43//!
44//! ### Type Safety
45//!
46//! - **Compile-Time Checking**: Prevent mixing different ID types
47//! - **Category Enforcement**: Each ID category has specific validation
48//! - **Zero Runtime Cost**: Phantom types have no runtime overhead
49//! - **Rich Type System**: Leverage Rust's type system for correctness
50//!
51//! ### Validation and Constraints
52//!
53//! - **Format Validation**: Validate ULID format and structure
54//! - **Category Validation**: Category-specific validation rules
55//! - **Nil Handling**: Configurable nil value handling per category
56//! - **Custom Constraints**: Support for custom validation logic
57//!
58//! ## Usage Examples
59//!
60//! ### Basic ID Creation
61
62//!
63//! ### ID Parsing and Validation
64
65//!
66//! ### Type Safety Demonstration
67
68//!
69//! ### Custom ID Categories
70
71//!
72//! ### ID Collections and Sorting
73
74//!
75//! ### Serialization and Deserialization
76
77//!
78//! ## ID Categories
79//!
80//! ### Built-in Categories
81//!
82//! - **PipelineIdCategory**: For pipeline instances
83//! - Validation: Standard ULID validation
84//! - Use case: Identify pipeline execution instances
85//!
86//! - **FileIdCategory**: For file references
87//! - Validation: Standard ULID validation
88//! - Use case: Identify files in the system
89//!
90//! - **UserIdCategory**: For user identification
91//! - Validation: Standard ULID validation
92//! - Use case: Identify users and sessions
93//!
94//! - **StageIdCategory**: For pipeline stages
95//! - Validation: Standard ULID validation
96//! - Use case: Identify individual pipeline stages
97//!
98//! ### Custom Categories
99//!
100//! Create custom ID categories by implementing the `IdCategory` trait:
101//!
102//! - **Category Name**: Unique identifier for the category
103//! - **Validation Logic**: Custom validation rules
104//! - **Nil Handling**: Configure whether nil values are allowed
105//!
106//! ## ULID Properties
107//!
108//! ### Format
109//!
110//! - **Length**: 26 characters
111//! - **Encoding**: Base32 (Crockford's Base32)
112//! - **Case**: Case-insensitive
113//! - **Characters**: 0-9, A-Z (excluding I, L, O, U)
114//!
115//! ### Structure
116//!
117//! ```text
118//! 01AN4Z07BY 79KA1307SR9X4MV3
119//! |----------| |----------------|
120//! Timestamp Randomness
121//! 48bits 80bits
122//! ```
123//!
124//! ### Properties
125//!
126//! - **Sortable**: Lexicographically sortable by timestamp
127//! - **Unique**: 80 bits of randomness ensure uniqueness
128//! - **Compact**: More compact than UUID strings
129//! - **URL-Safe**: Safe for use in URLs without encoding
130//!
131//! ## Validation Rules
132//!
133//! ### Format Validation
134//!
135//! - **Length**: Must be exactly 26 characters
136//! - **Characters**: Must contain only valid Base32 characters
137//! - **Structure**: Must follow ULID structure
138//!
139//! ### Category Validation
140//!
141//! - **Nil Handling**: Check if nil values are allowed
142//! - **Custom Rules**: Apply category-specific validation
143//! - **Timestamp Validation**: Validate timestamp ranges
144//!
145//! ### Security Considerations
146//!
147//! - **Randomness**: Ensure sufficient randomness
148//! - **Predictability**: Prevent ID prediction attacks
149//! - **Information Leakage**: Minimize information leakage
150//!
151//! ## Error Handling
152//!
153//! ### ID Errors
154//!
155//! - **Invalid Format**: ID format is invalid
156//! - **Parse Error**: Cannot parse ID from string
157//! - **Validation Error**: ID fails category validation
158//! - **Nil Error**: Nil ID where not allowed
159//!
160//! ### Category Errors
161//!
162//! - **Unknown Category**: Category is not recognized
163//! - **Validation Failure**: Category-specific validation failed
164//! - **Constraint Violation**: ID violates category constraints
165//!
166//! ## Performance Considerations
167//!
168//! ### Memory Usage
169//!
170//! - **Compact Storage**: ULID is more compact than UUID
171//! - **Zero-Cost Types**: Phantom types have no runtime cost
172//! - **Efficient Comparison**: Fast comparison operations
173//!
174//! ### Generation Performance
175//!
176//! - **Fast Generation**: ULID generation is very fast
177//! - **No Network**: No network calls required
178//! - **Thread-Safe**: Safe for concurrent generation
179//!
180//! ### Parsing Performance
181//!
182//! - **Fast Parsing**: Efficient Base32 parsing
183//! - **Validation**: Fast validation algorithms
184//! - **Caching**: Cache validation results when appropriate
185//!
186//! ## Integration
187//!
188//! The generic ID system integrates with:
189//!
190//! - **Domain Entities**: Identify domain entities uniquely
191//! - **Database**: Use as primary keys and foreign keys
192//! - **API**: Consistent ID format across APIs
193//! - **Logging**: Include IDs in logs for tracing
194//!
195//! ## Thread Safety
196//!
197//! The generic ID system is thread-safe:
198//!
199//! - **Immutable**: IDs are immutable after creation
200//! - **Safe Generation**: ULID generation is thread-safe
201//! - **Concurrent Access**: Safe concurrent access to ID data
202//!
203//! ## Future Enhancements
204//!
205//! Planned enhancements include:
206//!
207//! - **ID Registry**: Centralized ID category registry
208//! - **Migration Support**: Support for migrating between ID formats
209//! - **Performance Optimization**: Further performance optimizations
210//! - **Enhanced Validation**: More sophisticated validation rules
211
212use serde::{Deserialize, Serialize};
213use std::fmt::{self, Display};
214use std::hash::{Hash, Hasher};
215use std::str::FromStr;
216use ulid::Ulid;
217
218use crate::PipelineError;
219
220/// ID category trait for type-specific behavior
221///
222/// This trait defines the interface for ID categories, allowing different
223/// types of IDs to have category-specific validation rules and behavior.
224///
225/// # Key Features
226///
227/// - **Category Identification**: Unique name for each ID category
228/// - **Custom Validation**: Category-specific validation logic
229/// - **Nil Handling**: Configure whether nil values are allowed
230/// - **Extensibility**: Easy to add new ID categories
231///
232/// # Examples
233pub trait IdCategory {
234 /// Gets the category name for this ID type
235 fn category_name() -> &'static str;
236
237 /// Validates category-specific constraints
238 fn validate_id(ulid: &Ulid) -> Result<(), PipelineError> {
239 // Default implementation - can be overridden
240 if *ulid == Ulid::nil() {
241 return Err(PipelineError::InvalidConfiguration(format!(
242 "{} ID cannot be nil",
243 Self::category_name()
244 )));
245 }
246 Ok(())
247 }
248
249 /// Checks if this ID type should allow nil values
250 fn allows_nil() -> bool {
251 false // Default: IDs cannot be nil
252 }
253}
254
255/// Generic identifier value object for domain entities
256///
257/// # Purpose
258/// Provides the foundational ID implementation that all specific entity IDs
259/// build upon. This generic approach ensures consistency while allowing
260/// type-safe specialization.
261///
262/// # Design Principles
263/// - **Type Safety**: Each entity gets its own distinct ID type
264/// - **Validation**: Consistent validation rules across all ID types
265/// - **Serialization**: Uniform JSON/database representation
266/// - **Cross-Language**: Clear specification for Go implementation
267///
268/// # Architecture Notes
269/// This is the base implementation that specific ID value objects compose.
270/// It should not be used directly - instead use the specific ID types like
271/// `PipelineId`, `StageId`, etc.
272///
273/// # Cross-Language Mapping
274/// - **Rust**: `GenericId<T>` with phantom type parameter
275/// - **Go**: `GenericID[T any]` with type parameter
276/// - **JSON**: String representation of ULID
277/// - **SQLite**: TEXT column with ULID string
278/// - **Time-Ordered**: Natural chronological sorting
279#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
280pub struct GenericId<T: IdCategory> {
281 value: Ulid,
282 _phantom: std::marker::PhantomData<T>,
283}
284
285// Custom serialization to use simple string format instead of JSON object
286impl<T: IdCategory> Serialize for GenericId<T> {
287 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
288 where
289 S: serde::Serializer,
290 {
291 self.value.to_string().serialize(serializer)
292 }
293}
294
295impl<'de, T: IdCategory> Deserialize<'de> for GenericId<T> {
296 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
297 where
298 D: serde::Deserializer<'de>,
299 {
300 let s = String::deserialize(deserializer)?;
301 let ulid = Ulid::from_string(&s).map_err(|e| serde::de::Error::custom(e.to_string()))?;
302 Ok(Self {
303 value: ulid,
304 _phantom: std::marker::PhantomData,
305 })
306 }
307}
308
309impl<T: IdCategory> GenericId<T> {
310 /// Creates a new time-ordered entity ID with category validation
311 ///
312 /// # Time Ordering
313 /// ULIDs are naturally sorted by creation time, making them perfect for:
314 /// - Database indexes (sequential inserts)
315 /// - Event ordering
316 /// - Chronological queries
317 /// - Debugging (time-based ID inspection)
318 pub fn new() -> Self {
319 let ulid = Ulid::new();
320 // For new IDs, we assume they're valid since we just created them
321 Self {
322 value: ulid,
323 _phantom: std::marker::PhantomData,
324 }
325 }
326
327 /// Creates an entity ID from an existing ULID with validation
328 pub fn from_ulid(ulid: Ulid) -> Result<Self, PipelineError> {
329 T::validate_id(&ulid)?;
330 Ok(Self {
331 value: ulid,
332 _phantom: std::marker::PhantomData,
333 })
334 }
335
336 /// Creates an entity ID from a timestamp (useful for range queries)
337 ///
338 /// # Use Cases
339 /// - Creating boundary IDs for time-range queries
340 /// - Testing with specific timestamps
341 /// - Migration scenarios requiring specific timestamp IDs
342 pub fn from_timestamp_ms(timestamp_ms: u64) -> Result<Self, PipelineError> {
343 // Generate random bits for the ULID
344 let random = rand::random::<u128>() & ((1u128 << 80) - 1); // Mask to 80 bits
345 let ulid = Ulid::from_parts(timestamp_ms, random);
346 T::validate_id(&ulid)?;
347 Ok(Self {
348 value: ulid,
349 _phantom: std::marker::PhantomData,
350 })
351 }
352
353 /// Creates an entity ID from a string representation
354 ///
355 /// # Format
356 /// Accepts standard ULID string format (26 characters, base32 encoded)
357 /// Example: "01ARZ3NDEKTSV4RRFFQ69G5FAV"
358 pub fn from_string(s: &str) -> Result<Self, PipelineError> {
359 let ulid = Ulid::from_str(s)
360 .map_err(|e| PipelineError::InvalidConfiguration(format!("Invalid entity ID format: {}", e)))?;
361 Self::from_ulid(ulid)
362 }
363
364 /// Gets the underlying ULID value
365 pub fn as_ulid(&self) -> Ulid {
366 self.value
367 }
368
369 /// Gets the timestamp component of the ULID
370 ///
371 /// # Returns
372 /// Milliseconds since Unix epoch when this ID was created
373 ///
374 /// # Use Cases
375 /// - Time-range queries
376 /// - Debugging creation times
377 /// - Audit trails
378 pub fn timestamp_ms(&self) -> u64 {
379 self.value.timestamp_ms()
380 }
381
382 /// Gets the creation time as a DateTime
383 ///
384 /// # Use Cases
385 /// - Human-readable timestamps
386 /// - Time-based filtering
387 /// - Audit logs
388 pub fn datetime(&self) -> chrono::DateTime<chrono::Utc> {
389 let timestamp_ms = self.timestamp_ms();
390 chrono::DateTime::from_timestamp_millis(timestamp_ms as i64).unwrap_or_else(chrono::Utc::now)
391 }
392
393 /// Converts to lowercase string representation
394 ///
395 /// # Use Cases
396 /// - Case-insensitive systems
397 /// - URL paths
398 /// - Database systems that prefer lowercase
399 pub fn to_lowercase(&self) -> String {
400 self.value.to_string().to_lowercase()
401 }
402
403 /// Gets the ID category name
404 pub fn category(&self) -> &'static str {
405 T::category_name()
406 }
407
408 /// Validates the ID using category-specific rules
409 pub fn validate(&self) -> Result<(), PipelineError> {
410 T::validate_id(&self.value)
411 }
412
413 /// Checks if this is a nil (zero) ULID
414 pub fn is_nil(&self) -> bool {
415 self.value.0 == 0
416 }
417
418 /// Creates a nil entity ID (for testing/default values)
419 #[cfg(test)]
420 pub fn nil() -> Self {
421 Self {
422 value: Ulid(0),
423 _phantom: std::marker::PhantomData,
424 }
425 }
426
427 /// Creates an entity ID with a specific timestamp (for testing)
428 #[cfg(test)]
429 pub fn from_timestamp_for_test(timestamp_ms: u64) -> Self {
430 Self::from_timestamp_ms(timestamp_ms).unwrap_or_else(|_| Self::new())
431 }
432}
433
434impl<T: IdCategory> Default for GenericId<T> {
435 fn default() -> Self {
436 Self::new()
437 }
438}
439
440impl<T: IdCategory> Display for GenericId<T> {
441 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442 write!(f, "{}", self.value)
443 }
444}
445
446impl<T: IdCategory> Hash for GenericId<T> {
447 fn hash<H: Hasher>(&self, state: &mut H) {
448 self.value.hash(state);
449 }
450}
451
452impl<T: IdCategory> FromStr for GenericId<T> {
453 type Err = PipelineError;
454
455 fn from_str(s: &str) -> Result<Self, Self::Err> {
456 Self::from_string(s)
457 }
458}
459
460// Conversion traits for interoperability
461impl<T: IdCategory> From<Ulid> for GenericId<T> {
462 fn from(ulid: Ulid) -> Self {
463 Self::from_ulid(ulid).unwrap_or_else(|_| {
464 // For From trait, we can't return an error, so create a new ID
465 Self::new()
466 })
467 }
468}
469
470impl<T: IdCategory> From<GenericId<T>> for Ulid {
471 fn from(id: GenericId<T>) -> Self {
472 id.value
473 }
474}
475
476impl<T: IdCategory> AsRef<Ulid> for GenericId<T> {
477 fn as_ref(&self) -> &Ulid {
478 &self.value
479 }
480}
481
482/// Utility functions for working with generic IDs
483pub mod generic_id_utils {
484 use super::*;
485
486 /// Generates a batch of unique entity IDs
487 ///
488 /// # Time Ordering
489 /// Generated IDs will be naturally ordered by creation time
490 pub fn generate_batch<T: IdCategory>(count: usize) -> Vec<GenericId<T>> {
491 (0..count).map(|_| GenericId::new()).collect()
492 }
493
494 /// Generates a batch of IDs with specific timestamp
495 ///
496 /// # Use Cases
497 /// - Testing with controlled timestamps
498 /// - Bulk operations with same creation time
499 /// - Migration from timestamp-based systems
500 pub fn generate_batch_at_time<T: IdCategory>(count: usize, timestamp_ms: u64) -> Vec<GenericId<T>> {
501 (0..count)
502 .map(|_| GenericId::from_timestamp_ms(timestamp_ms).unwrap_or_else(|_| GenericId::new()))
503 .collect()
504 }
505
506 /// Validates a collection of entity IDs
507 pub fn validate_batch<T: IdCategory>(ids: &[GenericId<T>]) -> Result<(), PipelineError> {
508 // Check each ID individually
509 for id in ids {
510 id.validate()?;
511 }
512
513 // Check for duplicates
514 let mut seen = std::collections::HashSet::new();
515 for id in ids {
516 if !seen.insert(id.as_ulid()) {
517 return Err(PipelineError::InvalidConfiguration(format!(
518 "Duplicate entity ID found: {}",
519 id
520 )));
521 }
522 }
523
524 Ok(())
525 }
526
527 /// Converts a collection of ULIDs to entity IDs
528 pub fn from_ulids<T: IdCategory>(ulids: Vec<Ulid>) -> Result<Vec<GenericId<T>>, PipelineError> {
529 ulids
530 .into_iter()
531 .map(GenericId::from_ulid)
532 .collect::<Result<Vec<_>, _>>()
533 }
534
535 /// Converts a collection of entity IDs to ULIDs
536 pub fn to_ulids<T: IdCategory>(ids: Vec<GenericId<T>>) -> Vec<Ulid> {
537 ids.into_iter().map(|id| id.as_ulid()).collect()
538 }
539
540 /// Creates a boundary ID for time-range queries
541 ///
542 /// # Use Cases
543 /// - "Find all entities created after this time"
544 /// - Time-based pagination
545 /// - Audit queries
546 pub fn boundary_id_for_time<T: IdCategory>(timestamp_ms: u64) -> GenericId<T> {
547 GenericId::from_timestamp_ms(timestamp_ms).unwrap_or_else(|_| GenericId::new())
548 }
549
550 /// Sorts a collection of IDs by creation time (natural ULID order)
551 ///
552 /// # Note
553 /// ULIDs are naturally time-ordered, so this is just a regular sort
554 pub fn sort_by_time<T: IdCategory + Ord>(mut ids: Vec<GenericId<T>>) -> Vec<GenericId<T>> {
555 ids.sort();
556 ids
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563 use crate::PipelineError;
564
565 #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
566 struct TestEntity;
567
568 impl IdCategory for TestEntity {
569 fn category_name() -> &'static str {
570 "test"
571 }
572
573 fn validate_id(ulid: &Ulid) -> Result<(), PipelineError> {
574 // Reject nil ULIDs (all zeros)
575 if ulid.0 == 0 {
576 return Err(PipelineError::ValidationError(
577 "Nil ULID not allowed for test entities".to_string(),
578 ));
579 }
580 Ok(())
581 }
582 }
583
584 type TestId = GenericId<TestEntity>;
585
586 /// Tests generic ID creation and uniqueness guarantees.
587 ///
588 /// This test validates that generic IDs can be created with
589 /// guaranteed uniqueness and proper time-based ordering
590 /// using ULID generation.
591 ///
592 /// # Test Coverage
593 ///
594 /// - Generic ID creation with `new()`
595 /// - Uniqueness guarantees between IDs
596 /// - ULID uniqueness verification
597 /// - Time-based ordering with millisecond resolution
598 /// - Temporal sequence validation
599 ///
600 /// # Test Scenario
601 ///
602 /// Creates two generic IDs with a small time delay between
603 /// them and verifies they are unique and properly ordered.
604 ///
605 /// # Assertions
606 ///
607 /// - IDs are unique
608 /// - Underlying ULIDs are unique
609 /// - Second ID is greater than first (time ordering)
610 /// - Millisecond resolution ordering works
611 #[test]
612 fn test_generic_id_creation() {
613 let id1 = TestId::new();
614
615 // Sleep for 1ms to ensure different timestamps
616 std::thread::sleep(std::time::Duration::from_millis(1));
617
618 let id2 = TestId::new();
619
620 // IDs should be unique
621 assert_ne!(id1, id2);
622 assert_ne!(id1.as_ulid(), id2.as_ulid());
623
624 // IDs should be time-ordered (id2 created after id1)
625 // This works because ULIDs have millisecond resolution
626 assert!(id2 > id1);
627 }
628
629 /// Tests generic ID time-based ordering with specific timestamps.
630 ///
631 /// This test validates that generic IDs can be created from
632 /// specific timestamps and maintain proper chronological
633 /// ordering for time-based queries.
634 ///
635 /// # Test Coverage
636 ///
637 /// - ID creation from specific timestamps
638 /// - Time-based ordering validation
639 /// - Timestamp preservation and retrieval
640 /// - Chronological comparison operations
641 /// - Millisecond precision handling
642 ///
643 /// # Test Scenario
644 ///
645 /// Creates two generic IDs from specific timestamps with
646 /// a one-minute difference and verifies proper ordering
647 /// and timestamp preservation.
648 ///
649 /// # Assertions
650 ///
651 /// - Later ID is greater than earlier ID
652 /// - Timestamps are preserved correctly
653 /// - Time-based ordering works as expected
654 /// - Millisecond precision is maintained
655 #[test]
656 fn test_generic_id_time_ordering() {
657 let timestamp1 = 1640995200000; // 2022-01-01
658 let timestamp2 = 1640995260000; // 2022-01-01 + 1 minute
659
660 let id1 = TestId::from_timestamp_ms(timestamp1).unwrap();
661 let id2 = TestId::from_timestamp_ms(timestamp2).unwrap();
662
663 assert!(id2 > id1);
664 assert_eq!(id1.timestamp_ms(), timestamp1);
665 assert_eq!(id2.timestamp_ms(), timestamp2);
666 }
667
668 /// Tests generic ID JSON serialization and deserialization.
669 ///
670 /// This test validates that generic IDs can be properly
671 /// serialized to JSON and deserialized back while maintaining
672 /// equality and data integrity.
673 ///
674 /// # Test Coverage
675 ///
676 /// - JSON serialization with serde
677 /// - JSON deserialization with serde
678 /// - Serialization roundtrip integrity
679 /// - Data preservation during serialization
680 /// - Type safety after deserialization
681 ///
682 /// # Test Scenario
683 ///
684 /// Creates a generic ID, serializes it to JSON, deserializes
685 /// it back, and verifies the roundtrip preserves equality.
686 ///
687 /// # Assertions
688 ///
689 /// - Serialization succeeds
690 /// - Deserialization succeeds
691 /// - Original and deserialized IDs are equal
692 /// - Data integrity is maintained
693 #[test]
694 fn test_generic_id_serialization() {
695 let id = TestId::new();
696
697 let json = serde_json::to_string(&id).unwrap();
698 let deserialized: TestId = serde_json::from_str(&json).unwrap();
699
700 assert_eq!(id, deserialized);
701 }
702
703 /// Tests generic ID validation for valid and invalid cases.
704 ///
705 /// This test validates that generic IDs can be validated
706 /// for correctness and that invalid IDs (like nil ULIDs)
707 /// are properly rejected.
708 ///
709 /// # Test Coverage
710 ///
711 /// - Valid ID validation with `validate()`
712 /// - Invalid ID detection and rejection
713 /// - Nil ULID validation failure
714 /// - Validation error handling
715 /// - ID correctness verification
716 ///
717 /// # Test Scenario
718 ///
719 /// Creates a valid generic ID and verifies it passes validation,
720 /// then creates a nil ID and verifies it fails validation.
721 ///
722 /// # Assertions
723 ///
724 /// - Valid ID passes validation
725 /// - Nil ID fails validation
726 /// - Validation logic works correctly
727 /// - Error handling is appropriate
728 #[test]
729 fn test_generic_id_validation() {
730 let valid_id = TestId::new();
731 assert!(valid_id.validate().is_ok());
732
733 // Create a nil ULID using the nil() method
734 let nil_id = TestId::nil();
735 assert!(nil_id.validate().is_err());
736 }
737
738 /// Tests ULID conversion utilities for batch operations.
739 ///
740 /// This test validates that generic IDs can be converted
741 /// to and from ULIDs in batch operations while preserving
742 /// data integrity and order.
743 ///
744 /// # Test Coverage
745 ///
746 /// - Batch ULID to generic ID conversion
747 /// - Batch generic ID to ULID conversion
748 /// - Conversion roundtrip integrity
749 /// - Order preservation during conversion
750 /// - Utility function correctness
751 ///
752 /// # Test Scenario
753 ///
754 /// Creates a batch of ULIDs, converts them to generic IDs,
755 /// then converts back to ULIDs and verifies the roundtrip
756 /// preserves the original data.
757 ///
758 /// # Assertions
759 ///
760 /// - Conversion to generic IDs succeeds
761 /// - Conversion back to ULIDs succeeds
762 /// - Original and final ULIDs are equal
763 /// - Order is preserved throughout
764 #[test]
765 fn test_ulid_conversions() {
766 use super::generic_id_utils::*;
767
768 let original_ulids = vec![Ulid::new(), Ulid::new(), Ulid::new()];
769 let entity_ids = from_ulids::<TestEntity>(original_ulids.clone()).unwrap();
770 let converted_ulids = to_ulids(entity_ids);
771
772 assert_eq!(original_ulids, converted_ulids);
773 }
774
775 /// Tests time-based range queries and boundary ID generation.
776 ///
777 /// This test validates that generic IDs support time-based
778 /// range queries by generating boundary IDs for specific
779 /// timestamps and verifying ordering relationships.
780 ///
781 /// # Test Coverage
782 ///
783 /// - Boundary ID generation for specific timestamps
784 /// - Time-based range query support
785 /// - Timestamp preservation in boundary IDs
786 /// - Ordering relationships with boundary IDs
787 /// - Time range query utilities
788 ///
789 /// # Test Scenario
790 ///
791 /// Creates a boundary ID for a specific timestamp, then
792 /// creates an ID for a later timestamp and verifies the
793 /// ordering relationship works correctly.
794 ///
795 /// # Assertions
796 ///
797 /// - Boundary ID has correct timestamp
798 /// - Later ID is greater than boundary ID
799 /// - Time-based ordering works for queries
800 /// - Boundary generation is accurate
801 #[test]
802 fn test_time_range_queries() {
803 use super::generic_id_utils::*;
804
805 let base_time = 1640995200000; // 2022-01-01
806 let boundary_id = boundary_id_for_time::<TestEntity>(base_time);
807
808 assert_eq!(boundary_id.timestamp_ms(), base_time);
809
810 // Test that IDs created after boundary are greater
811 let later_id = TestId::from_timestamp_ms(base_time + 1000).unwrap();
812 assert!(later_id > boundary_id);
813 }
814}