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 '{value}' must be alphanumeric with hyphens, underscores, or colons"
157            )));
158        }
159
160        // Rule: Cannot start or end with special characters
161        if value.starts_with(':')
162            || value.starts_with('-')
163            || value.starts_with('_')
164            || value.ends_with(':')
165            || value.ends_with('-')
166            || value.ends_with('_')
167        {
168            return Err(crate::error::AllSourceError::InvalidInput(format!(
169                "Stream name '{value}' cannot start or end with special characters"
170            )));
171        }
172
173        // Rule: Cannot have consecutive special characters
174        if value.contains("::")
175            || value.contains("--")
176            || value.contains("__")
177            || value.contains(":-")
178            || value.contains("-:")
179        {
180            return Err(crate::error::AllSourceError::InvalidInput(format!(
181                "Stream name '{value}' cannot have consecutive special characters"
182            )));
183        }
184
185        Ok(())
186    }
187}
188
189impl fmt::Display for StreamName {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        write!(f, "{}", self.0)
192    }
193}
194
195impl TryFrom<&str> for StreamName {
196    type Error = crate::error::AllSourceError;
197
198    fn try_from(value: &str) -> Result<Self> {
199        StreamName::new(value.to_string())
200    }
201}
202
203impl TryFrom<String> for StreamName {
204    type Error = crate::error::AllSourceError;
205
206    fn try_from(value: String) -> Result<Self> {
207        StreamName::new(value)
208    }
209}
210
211impl AsRef<str> for StreamName {
212    fn as_ref(&self) -> &str {
213        &self.0
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_create_valid_stream_names() {
223        // Simple name
224        let stream = StreamName::new("my-stream".to_string());
225        assert!(stream.is_ok());
226        assert_eq!(stream.unwrap().as_str(), "my-stream");
227
228        // With colon (entity pattern)
229        let stream = StreamName::new("user:123".to_string());
230        assert!(stream.is_ok());
231
232        // With underscore
233        let stream = StreamName::new("order_stream".to_string());
234        assert!(stream.is_ok());
235
236        // Mixed
237        let stream = StreamName::new("user_account:abc-123".to_string());
238        assert!(stream.is_ok());
239
240        // Just alphanumeric
241        let stream = StreamName::new("stream123".to_string());
242        assert!(stream.is_ok());
243    }
244
245    #[test]
246    fn test_reject_empty_stream_name() {
247        let result = StreamName::new(String::new());
248        assert!(result.is_err());
249
250        if let Err(e) = result {
251            assert!(e.to_string().contains("cannot be empty"));
252        }
253    }
254
255    #[test]
256    fn test_reject_too_long_stream_name() {
257        let long_name = "a".repeat(257);
258        let result = StreamName::new(long_name);
259        assert!(result.is_err());
260
261        if let Err(e) = result {
262            assert!(e.to_string().contains("cannot exceed 256 characters"));
263        }
264    }
265
266    #[test]
267    fn test_accept_max_length_stream_name() {
268        let max_name = "a".repeat(256);
269        let result = StreamName::new(max_name);
270        assert!(result.is_ok());
271    }
272
273    #[test]
274    fn test_reject_invalid_characters() {
275        // Space
276        let result = StreamName::new("stream name".to_string());
277        assert!(result.is_err());
278
279        // Dot
280        let result = StreamName::new("stream.name".to_string());
281        assert!(result.is_err());
282
283        // Special characters
284        let result = StreamName::new("stream@name".to_string());
285        assert!(result.is_err());
286
287        let result = StreamName::new("stream/name".to_string());
288        assert!(result.is_err());
289    }
290
291    #[test]
292    fn test_reject_starting_with_special_char() {
293        let result = StreamName::new(":stream".to_string());
294        assert!(result.is_err());
295
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
303    #[test]
304    fn test_reject_ending_with_special_char() {
305        let result = StreamName::new("stream:".to_string());
306        assert!(result.is_err());
307
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
315    #[test]
316    fn test_reject_consecutive_special_chars() {
317        let result = StreamName::new("stream::id".to_string());
318        assert!(result.is_err());
319
320        let result = StreamName::new("stream--name".to_string());
321        assert!(result.is_err());
322
323        let result = StreamName::new("stream__name".to_string());
324        assert!(result.is_err());
325    }
326
327    #[test]
328    fn test_for_entity() {
329        let stream = StreamName::for_entity("user", "123");
330        assert!(stream.is_ok());
331        assert_eq!(stream.unwrap().as_str(), "user:123");
332    }
333
334    #[test]
335    fn test_entity_type_extraction() {
336        let stream = StreamName::new("user:123".to_string()).unwrap();
337        assert_eq!(stream.entity_type(), Some("user"));
338
339        let stream = StreamName::new("order:abc-456".to_string()).unwrap();
340        assert_eq!(stream.entity_type(), Some("order"));
341
342        // No colon
343        let stream = StreamName::new("simple-stream".to_string()).unwrap();
344        assert_eq!(stream.entity_type(), None);
345    }
346
347    #[test]
348    fn test_entity_id_extraction() {
349        let stream = StreamName::new("user:123".to_string()).unwrap();
350        assert_eq!(stream.entity_id(), Some("123"));
351
352        let stream = StreamName::new("order:abc-456".to_string()).unwrap();
353        assert_eq!(stream.entity_id(), Some("abc-456"));
354
355        // Multiple colons - take everything after first colon
356        let stream = StreamName::new("complex:id:with:colons".to_string()).unwrap();
357        assert_eq!(stream.entity_id(), Some("id:with:colons"));
358
359        // No colon
360        let stream = StreamName::new("simple-stream".to_string()).unwrap();
361        assert_eq!(stream.entity_id(), None);
362    }
363
364    #[test]
365    fn test_is_entity_type() {
366        let stream = StreamName::new("user:123".to_string()).unwrap();
367        assert!(stream.is_entity_type("user"));
368        assert!(!stream.is_entity_type("order"));
369    }
370
371    #[test]
372    fn test_starts_with() {
373        let stream = StreamName::new("user:123".to_string()).unwrap();
374        assert!(stream.starts_with("user"));
375        assert!(stream.starts_with("user:"));
376        assert!(!stream.starts_with("order"));
377    }
378
379    #[test]
380    fn test_display_trait() {
381        let stream = StreamName::new("user:123".to_string()).unwrap();
382        assert_eq!(format!("{stream}"), "user:123");
383    }
384
385    #[test]
386    fn test_try_from_str() {
387        let stream: Result<StreamName> = "user:123".try_into();
388        assert!(stream.is_ok());
389        assert_eq!(stream.unwrap().as_str(), "user:123");
390
391        let invalid: Result<StreamName> = "".try_into();
392        assert!(invalid.is_err());
393    }
394
395    #[test]
396    fn test_try_from_string() {
397        let stream: Result<StreamName> = "order:456".to_string().try_into();
398        assert!(stream.is_ok());
399
400        let invalid: Result<StreamName> = String::new().try_into();
401        assert!(invalid.is_err());
402    }
403
404    #[test]
405    fn test_into_inner() {
406        let stream = StreamName::new("test-stream".to_string()).unwrap();
407        let inner = stream.into_inner();
408        assert_eq!(inner, "test-stream");
409    }
410
411    #[test]
412    fn test_equality() {
413        let stream1 = StreamName::new("user:123".to_string()).unwrap();
414        let stream2 = StreamName::new("user:123".to_string()).unwrap();
415        let stream3 = StreamName::new("order:456".to_string()).unwrap();
416
417        assert_eq!(stream1, stream2);
418        assert_ne!(stream1, stream3);
419    }
420
421    #[test]
422    fn test_cloning() {
423        let stream1 = StreamName::new("test-stream".to_string()).unwrap();
424        let stream2 = stream1.clone();
425        assert_eq!(stream1, stream2);
426    }
427
428    #[test]
429    fn test_hash_consistency() {
430        use std::collections::HashSet;
431
432        let stream1 = StreamName::new("user:123".to_string()).unwrap();
433        let stream2 = StreamName::new("user:123".to_string()).unwrap();
434
435        let mut set = HashSet::new();
436        set.insert(stream1);
437
438        assert!(set.contains(&stream2));
439    }
440
441    #[test]
442    fn test_serde_serialization() {
443        let stream = StreamName::new("user:123".to_string()).unwrap();
444
445        // Serialize
446        let json = serde_json::to_string(&stream).unwrap();
447        assert_eq!(json, "\"user:123\"");
448
449        // Deserialize
450        let deserialized: StreamName = serde_json::from_str(&json).unwrap();
451        assert_eq!(deserialized, stream);
452    }
453
454    #[test]
455    fn test_as_ref() {
456        let stream = StreamName::new("test-stream".to_string()).unwrap();
457        let str_ref: &str = stream.as_ref();
458        assert_eq!(str_ref, "test-stream");
459    }
460
461    #[test]
462    fn test_new_unchecked() {
463        // Should create without validation (for internal use)
464        let stream = StreamName::new_unchecked("invalid stream!".to_string());
465        assert_eq!(stream.as_str(), "invalid stream!");
466    }
467}