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}