Skip to main content

pjson_rs_domain/value_objects/
id.rs

1//! Generic UUID-based Identifier Value Object
2//!
3//! Type-safe identifier using phantom types for compile-time differentiation.
4//! Uses sealed trait pattern to prevent external marker implementations.
5
6use std::fmt;
7use std::marker::PhantomData;
8use uuid::Uuid;
9
10/// Sealed trait module preventing external implementations
11mod private {
12    pub trait Sealed {}
13}
14
15/// Marker trait for type-safe ID differentiation.
16///
17/// This trait is sealed - external crates cannot implement it.
18/// Only marker types defined in this module are valid.
19pub trait IdMarker: private::Sealed + Send + Sync + 'static {}
20
21/// Marker type for session identifiers
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct SessionMarker;
24
25/// Marker type for stream identifiers
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct StreamMarker;
28
29impl private::Sealed for SessionMarker {}
30impl private::Sealed for StreamMarker {}
31
32impl IdMarker for SessionMarker {}
33impl IdMarker for StreamMarker {}
34
35/// Generic UUID-based identifier with phantom type safety.
36///
37/// Provides compile-time type differentiation between different ID types
38/// (e.g., SessionId vs StreamId) while sharing a single implementation.
39///
40/// # Type Safety
41///
42/// The phantom type parameter `T` ensures that different ID types cannot
43/// be accidentally mixed:
44///
45/// ```compile_fail
46/// # use pjson_rs_domain::value_objects::{SessionId, StreamId};
47/// let session_id: SessionId = SessionId::new();
48/// let stream_id: StreamId = session_id;  // Compile error!
49/// ```
50///
51/// # Zero-Cost Abstraction
52///
53/// `PhantomData<T>` is a zero-sized type, so `Id<T>` has the same memory
54/// layout as a plain `Uuid`.
55///
56/// # Examples
57///
58/// ```
59/// # use pjson_rs_domain::value_objects::{SessionId, StreamId};
60/// let session_id = SessionId::new();
61/// let stream_id = StreamId::new();
62///
63/// // Type-safe: cannot compare different ID types
64/// // session_id == stream_id  // Would not compile
65/// ```
66#[derive(Clone, Copy, PartialEq, Eq, Hash)]
67pub struct Id<T: IdMarker> {
68    value: Uuid,
69    _marker: PhantomData<T>,
70}
71
72impl<T: IdMarker> Id<T> {
73    /// Create new random identifier
74    #[must_use]
75    pub fn new() -> Self {
76        Self {
77            value: Uuid::new_v4(),
78            _marker: PhantomData,
79        }
80    }
81
82    /// Create identifier from existing UUID
83    #[must_use]
84    pub fn from_uuid(uuid: Uuid) -> Self {
85        Self {
86            value: uuid,
87            _marker: PhantomData,
88        }
89    }
90
91    /// Create identifier from string representation
92    ///
93    /// # Errors
94    ///
95    /// Returns `uuid::Error` if the string is not a valid UUID.
96    pub fn from_string(s: &str) -> Result<Self, uuid::Error> {
97        Uuid::parse_str(s).map(Self::from_uuid)
98    }
99
100    /// Get underlying UUID value
101    #[must_use]
102    pub fn as_uuid(&self) -> Uuid {
103        self.value
104    }
105
106    /// Get string representation
107    #[must_use]
108    pub fn as_str(&self) -> String {
109        self.value.to_string()
110    }
111}
112
113impl<T: IdMarker> Default for Id<T> {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119impl<T: IdMarker> fmt::Debug for Id<T> {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        f.debug_tuple(std::any::type_name::<Self>())
122            .field(&self.value)
123            .finish()
124    }
125}
126
127impl<T: IdMarker> fmt::Display for Id<T> {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(f, "{}", self.value)
130    }
131}
132
133impl<T: IdMarker> From<Uuid> for Id<T> {
134    fn from(uuid: Uuid) -> Self {
135        Self::from_uuid(uuid)
136    }
137}
138
139impl<T: IdMarker> From<Id<T>> for Uuid {
140    fn from(id: Id<T>) -> Self {
141        id.value
142    }
143}
144
145/// Type alias for session identifier
146pub type SessionId = Id<SessionMarker>;
147
148/// Type alias for stream identifier
149pub type StreamId = Id<StreamMarker>;
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_id_creation() {
157        let id1 = SessionId::new();
158        let id2 = SessionId::new();
159
160        assert_ne!(id1, id2);
161        assert_eq!(id1.as_uuid().get_version_num(), 4);
162    }
163
164    #[test]
165    fn test_id_from_string() {
166        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
167        let id = SessionId::from_string(uuid_str).unwrap();
168        assert_eq!(id.as_str(), uuid_str);
169    }
170
171    #[test]
172    fn test_id_from_invalid_string() {
173        let result = SessionId::from_string("invalid-uuid");
174        assert!(result.is_err());
175    }
176
177    #[test]
178    fn test_different_id_types_are_distinct() {
179        let session_uuid = Uuid::new_v4();
180        let session_id = SessionId::from_uuid(session_uuid);
181        let stream_id = StreamId::from_uuid(session_uuid);
182
183        // Same underlying UUID, but different types
184        assert_eq!(session_id.as_uuid(), stream_id.as_uuid());
185
186        // Type system prevents: session_id == stream_id (won't compile)
187    }
188
189    #[test]
190    fn test_id_default() {
191        let id = SessionId::default();
192        assert_eq!(id.as_uuid().get_version_num(), 4);
193    }
194
195    #[test]
196    fn test_id_debug_display() {
197        let id = SessionId::new();
198        let debug_str = format!("{:?}", id);
199        assert!(debug_str.contains("Id<"));
200
201        let display_str = format!("{}", id);
202        assert!(Uuid::parse_str(&display_str).is_ok());
203    }
204
205    #[test]
206    fn test_id_from_uuid_conversion() {
207        let uuid = Uuid::new_v4();
208        let id: SessionId = uuid.into();
209        let back: Uuid = id.into();
210        assert_eq!(uuid, back);
211    }
212
213    #[test]
214    fn test_stream_id() {
215        let id1 = StreamId::new();
216        let id2 = StreamId::new();
217        assert_ne!(id1, id2);
218    }
219}