Skip to main content

allsource_core/domain/value_objects/
stream_name.rs

1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5/// Value Object: StreamName
6///
7/// Represents the name of an event stream in the event store.
8/// Streams are logical groupings of events, typically by entity or aggregate.
9///
10/// Domain Rules:
11/// - Cannot be empty
12/// - Must be between 1 and 256 characters
13/// - Must follow naming convention: alphanumeric with hyphens/underscores/colons
14/// - Case-sensitive
15/// - Immutable once created
16///
17/// Common patterns:
18/// - `entity-type:entity-id` (e.g., "user:123", "order:abc-456")
19/// - `aggregate-type-aggregate-id` (e.g., "user-123", "order-abc-456")
20///
21/// This is a Value Object:
22/// - Defined by its value, not identity
23/// - Immutable
24/// - Self-validating
25/// - Compared by value equality
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct StreamName(String);
28
29impl StreamName {
30    /// Create a new StreamName with validation
31    ///
32    /// # Errors
33    /// Returns error if:
34    /// - Name is empty
35    /// - Name is longer than 256 characters
36    /// - Name contains invalid characters
37    ///
38    /// # Examples
39    /// ```
40    /// use allsource_core::domain::value_objects::StreamName;
41    ///
42    /// let stream = StreamName::new("user:123".to_string()).unwrap();
43    /// assert_eq!(stream.as_str(), "user:123");
44    /// ```
45    pub fn new(value: String) -> Result<Self> {
46        Self::validate(&value)?;
47        Ok(Self(value))
48    }
49
50    /// Create StreamName without validation (for internal use, e.g., from trusted storage)
51    ///
52    /// # Safety
53    /// This bypasses validation. Only use when loading from trusted sources
54    /// where validation has already occurred.
55    pub(crate) fn new_unchecked(value: String) -> Self {
56        Self(value)
57    }
58
59    /// Create a StreamName from entity type and entity ID
60    ///
61    /// # Examples
62    /// ```
63    /// use allsource_core::domain::value_objects::StreamName;
64    ///
65    /// let stream = StreamName::for_entity("user", "123").unwrap();
66    /// assert_eq!(stream.as_str(), "user:123");
67    /// ```
68    pub fn for_entity(entity_type: &str, entity_id: &str) -> Result<Self> {
69        Self::new(format!("{}:{}", entity_type, entity_id))
70    }
71
72    /// Get the string value
73    pub fn as_str(&self) -> &str {
74        &self.0
75    }
76
77    /// Get the inner String (consumes self)
78    pub fn into_inner(self) -> String {
79        self.0
80    }
81
82    /// Extract entity type if stream follows "type:id" pattern
83    ///
84    /// # Examples
85    /// ```
86    /// use allsource_core::domain::value_objects::StreamName;
87    ///
88    /// let stream = StreamName::new("user:123".to_string()).unwrap();
89    /// assert_eq!(stream.entity_type(), Some("user"));
90    ///
91    /// let stream = StreamName::new("simple-stream".to_string()).unwrap();
92    /// assert_eq!(stream.entity_type(), None);
93    /// ```
94    pub fn entity_type(&self) -> Option<&str> {
95        self.0.split(':').next().filter(|_| self.0.contains(':'))
96    }
97
98    /// Extract entity ID if stream follows "type:id" pattern
99    ///
100    /// # Examples
101    /// ```
102    /// use allsource_core::domain::value_objects::StreamName;
103    ///
104    /// let stream = StreamName::new("user:123".to_string()).unwrap();
105    /// assert_eq!(stream.entity_id(), Some("123"));
106    ///
107    /// let stream = StreamName::new("simple-stream".to_string()).unwrap();
108    /// assert_eq!(stream.entity_id(), None);
109    /// ```
110    pub fn entity_id(&self) -> Option<&str> {
111        self.0.split_once(':').map(|x| x.1)
112    }
113
114    /// Check if this stream is for a specific entity type
115    ///
116    /// # Examples
117    /// ```
118    /// use allsource_core::domain::value_objects::StreamName;
119    ///
120    /// let stream = StreamName::new("user:123".to_string()).unwrap();
121    /// assert!(stream.is_entity_type("user"));
122    /// assert!(!stream.is_entity_type("order"));
123    /// ```
124    pub fn is_entity_type(&self, entity_type: &str) -> bool {
125        self.entity_type() == Some(entity_type)
126    }
127
128    /// Check if this stream name starts with a prefix
129    pub fn starts_with(&self, prefix: &str) -> bool {
130        self.0.starts_with(prefix)
131    }
132
133    /// Validate a stream name string
134    fn validate(value: &str) -> Result<()> {
135        // Rule: Cannot be empty
136        if value.is_empty() {
137            return Err(crate::error::AllSourceError::InvalidInput(
138                "Stream name cannot be empty".to_string(),
139            ));
140        }
141
142        // Rule: Maximum length 256 characters
143        if value.len() > 256 {
144            return Err(crate::error::AllSourceError::InvalidInput(format!(
145                "Stream name cannot exceed 256 characters, got {}",
146                value.len()
147            )));
148        }
149
150        // Rule: Only alphanumeric, hyphens, underscores, and colons
151        if !value
152            .chars()
153            .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == ':')
154        {
155            return Err(crate::error::AllSourceError::InvalidInput(format!(
156                "Stream name '{}' must be alphanumeric with hyphens, underscores, or colons",
157                value
158            )));
159        }
160
161        // Rule: Cannot start or end with special characters
162        if value.starts_with(':')
163            || value.starts_with('-')
164            || value.starts_with('_')
165            || value.ends_with(':')
166            || value.ends_with('-')
167            || value.ends_with('_')
168        {
169            return Err(crate::error::AllSourceError::InvalidInput(format!(
170                "Stream name '{}' cannot start or end with special characters",
171                value
172            )));
173        }
174
175        // Rule: Cannot have consecutive special characters
176        if value.contains("::")
177            || value.contains("--")
178            || value.contains("__")
179            || value.contains(":-")
180            || value.contains("-:")
181        {
182            return Err(crate::error::AllSourceError::InvalidInput(format!(
183                "Stream name '{}' cannot have consecutive special characters",
184                value
185            )));
186        }
187
188        Ok(())
189    }
190}
191
192impl fmt::Display for StreamName {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        write!(f, "{}", self.0)
195    }
196}
197
198impl TryFrom<&str> for StreamName {
199    type Error = crate::error::AllSourceError;
200
201    fn try_from(value: &str) -> Result<Self> {
202        StreamName::new(value.to_string())
203    }
204}
205
206impl TryFrom<String> for StreamName {
207    type Error = crate::error::AllSourceError;
208
209    fn try_from(value: String) -> Result<Self> {
210        StreamName::new(value)
211    }
212}
213
214impl AsRef<str> for StreamName {
215    fn as_ref(&self) -> &str {
216        &self.0
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_create_valid_stream_names() {
226        // Simple name
227        let stream = StreamName::new("my-stream".to_string());
228        assert!(stream.is_ok());
229        assert_eq!(stream.unwrap().as_str(), "my-stream");
230
231        // With colon (entity pattern)
232        let stream = StreamName::new("user:123".to_string());
233        assert!(stream.is_ok());
234
235        // With underscore
236        let stream = StreamName::new("order_stream".to_string());
237        assert!(stream.is_ok());
238
239        // Mixed
240        let stream = StreamName::new("user_account:abc-123".to_string());
241        assert!(stream.is_ok());
242
243        // Just alphanumeric
244        let stream = StreamName::new("stream123".to_string());
245        assert!(stream.is_ok());
246    }
247
248    #[test]
249    fn test_reject_empty_stream_name() {
250        let result = StreamName::new("".to_string());
251        assert!(result.is_err());
252
253        if let Err(e) = result {
254            assert!(e.to_string().contains("cannot be empty"));
255        }
256    }
257
258    #[test]
259    fn test_reject_too_long_stream_name() {
260        let long_name = "a".repeat(257);
261        let result = StreamName::new(long_name);
262        assert!(result.is_err());
263
264        if let Err(e) = result {
265            assert!(e.to_string().contains("cannot exceed 256 characters"));
266        }
267    }
268
269    #[test]
270    fn test_accept_max_length_stream_name() {
271        let max_name = "a".repeat(256);
272        let result = StreamName::new(max_name);
273        assert!(result.is_ok());
274    }
275
276    #[test]
277    fn test_reject_invalid_characters() {
278        // Space
279        let result = StreamName::new("stream name".to_string());
280        assert!(result.is_err());
281
282        // Dot
283        let result = StreamName::new("stream.name".to_string());
284        assert!(result.is_err());
285
286        // Special characters
287        let result = StreamName::new("stream@name".to_string());
288        assert!(result.is_err());
289
290        let result = StreamName::new("stream/name".to_string());
291        assert!(result.is_err());
292    }
293
294    #[test]
295    fn test_reject_starting_with_special_char() {
296        let result = StreamName::new(":stream".to_string());
297        assert!(result.is_err());
298
299        let result = StreamName::new("-stream".to_string());
300        assert!(result.is_err());
301
302        let result = StreamName::new("_stream".to_string());
303        assert!(result.is_err());
304    }
305
306    #[test]
307    fn test_reject_ending_with_special_char() {
308        let result = StreamName::new("stream:".to_string());
309        assert!(result.is_err());
310
311        let result = StreamName::new("stream-".to_string());
312        assert!(result.is_err());
313
314        let result = StreamName::new("stream_".to_string());
315        assert!(result.is_err());
316    }
317
318    #[test]
319    fn test_reject_consecutive_special_chars() {
320        let result = StreamName::new("stream::id".to_string());
321        assert!(result.is_err());
322
323        let result = StreamName::new("stream--name".to_string());
324        assert!(result.is_err());
325
326        let result = StreamName::new("stream__name".to_string());
327        assert!(result.is_err());
328    }
329
330    #[test]
331    fn test_for_entity() {
332        let stream = StreamName::for_entity("user", "123");
333        assert!(stream.is_ok());
334        assert_eq!(stream.unwrap().as_str(), "user:123");
335    }
336
337    #[test]
338    fn test_entity_type_extraction() {
339        let stream = StreamName::new("user:123".to_string()).unwrap();
340        assert_eq!(stream.entity_type(), Some("user"));
341
342        let stream = StreamName::new("order:abc-456".to_string()).unwrap();
343        assert_eq!(stream.entity_type(), Some("order"));
344
345        // No colon
346        let stream = StreamName::new("simple-stream".to_string()).unwrap();
347        assert_eq!(stream.entity_type(), None);
348    }
349
350    #[test]
351    fn test_entity_id_extraction() {
352        let stream = StreamName::new("user:123".to_string()).unwrap();
353        assert_eq!(stream.entity_id(), Some("123"));
354
355        let stream = StreamName::new("order:abc-456".to_string()).unwrap();
356        assert_eq!(stream.entity_id(), Some("abc-456"));
357
358        // Multiple colons - take everything after first colon
359        let stream = StreamName::new("complex:id:with:colons".to_string()).unwrap();
360        assert_eq!(stream.entity_id(), Some("id:with:colons"));
361
362        // No colon
363        let stream = StreamName::new("simple-stream".to_string()).unwrap();
364        assert_eq!(stream.entity_id(), None);
365    }
366
367    #[test]
368    fn test_is_entity_type() {
369        let stream = StreamName::new("user:123".to_string()).unwrap();
370        assert!(stream.is_entity_type("user"));
371        assert!(!stream.is_entity_type("order"));
372    }
373
374    #[test]
375    fn test_starts_with() {
376        let stream = StreamName::new("user:123".to_string()).unwrap();
377        assert!(stream.starts_with("user"));
378        assert!(stream.starts_with("user:"));
379        assert!(!stream.starts_with("order"));
380    }
381
382    #[test]
383    fn test_display_trait() {
384        let stream = StreamName::new("user:123".to_string()).unwrap();
385        assert_eq!(format!("{}", stream), "user:123");
386    }
387
388    #[test]
389    fn test_try_from_str() {
390        let stream: Result<StreamName> = "user:123".try_into();
391        assert!(stream.is_ok());
392        assert_eq!(stream.unwrap().as_str(), "user:123");
393
394        let invalid: Result<StreamName> = "".try_into();
395        assert!(invalid.is_err());
396    }
397
398    #[test]
399    fn test_try_from_string() {
400        let stream: Result<StreamName> = "order:456".to_string().try_into();
401        assert!(stream.is_ok());
402
403        let invalid: Result<StreamName> = String::new().try_into();
404        assert!(invalid.is_err());
405    }
406
407    #[test]
408    fn test_into_inner() {
409        let stream = StreamName::new("test-stream".to_string()).unwrap();
410        let inner = stream.into_inner();
411        assert_eq!(inner, "test-stream");
412    }
413
414    #[test]
415    fn test_equality() {
416        let stream1 = StreamName::new("user:123".to_string()).unwrap();
417        let stream2 = StreamName::new("user:123".to_string()).unwrap();
418        let stream3 = StreamName::new("order:456".to_string()).unwrap();
419
420        assert_eq!(stream1, stream2);
421        assert_ne!(stream1, stream3);
422    }
423
424    #[test]
425    fn test_cloning() {
426        let stream1 = StreamName::new("test-stream".to_string()).unwrap();
427        let stream2 = stream1.clone();
428        assert_eq!(stream1, stream2);
429    }
430
431    #[test]
432    fn test_hash_consistency() {
433        use std::collections::HashSet;
434
435        let stream1 = StreamName::new("user:123".to_string()).unwrap();
436        let stream2 = StreamName::new("user:123".to_string()).unwrap();
437
438        let mut set = HashSet::new();
439        set.insert(stream1);
440
441        assert!(set.contains(&stream2));
442    }
443
444    #[test]
445    fn test_serde_serialization() {
446        let stream = StreamName::new("user:123".to_string()).unwrap();
447
448        // Serialize
449        let json = serde_json::to_string(&stream).unwrap();
450        assert_eq!(json, "\"user:123\"");
451
452        // Deserialize
453        let deserialized: StreamName = serde_json::from_str(&json).unwrap();
454        assert_eq!(deserialized, stream);
455    }
456
457    #[test]
458    fn test_as_ref() {
459        let stream = StreamName::new("test-stream".to_string()).unwrap();
460        let str_ref: &str = stream.as_ref();
461        assert_eq!(str_ref, "test-stream");
462    }
463
464    #[test]
465    fn test_new_unchecked() {
466        // Should create without validation (for internal use)
467        let stream = StreamName::new_unchecked("invalid stream!".to_string());
468        assert_eq!(stream.as_str(), "invalid stream!");
469    }
470}