allsource_core/domain/value_objects/
stream_name.rs1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct StreamName(String);
28
29impl StreamName {
30 pub fn new(value: String) -> Result<Self> {
46 Self::validate(&value)?;
47 Ok(Self(value))
48 }
49
50 pub(crate) fn new_unchecked(value: String) -> Self {
56 Self(value)
57 }
58
59 pub fn for_entity(entity_type: &str, entity_id: &str) -> Result<Self> {
69 Self::new(format!("{entity_type}:{entity_id}"))
70 }
71
72 pub fn as_str(&self) -> &str {
74 &self.0
75 }
76
77 pub fn into_inner(self) -> String {
79 self.0
80 }
81
82 pub fn entity_type(&self) -> Option<&str> {
95 self.0.split(':').next().filter(|_| self.0.contains(':'))
96 }
97
98 pub fn entity_id(&self) -> Option<&str> {
111 self.0.split_once(':').map(|x| x.1)
112 }
113
114 pub fn is_entity_type(&self, entity_type: &str) -> bool {
125 self.entity_type() == Some(entity_type)
126 }
127
128 pub fn starts_with(&self, prefix: &str) -> bool {
130 self.0.starts_with(prefix)
131 }
132
133 fn validate(value: &str) -> Result<()> {
135 if value.is_empty() {
137 return Err(crate::error::AllSourceError::InvalidInput(
138 "Stream name cannot be empty".to_string(),
139 ));
140 }
141
142 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 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 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 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 let stream = StreamName::new("my-stream".to_string());
225 assert!(stream.is_ok());
226 assert_eq!(stream.unwrap().as_str(), "my-stream");
227
228 let stream = StreamName::new("user:123".to_string());
230 assert!(stream.is_ok());
231
232 let stream = StreamName::new("order_stream".to_string());
234 assert!(stream.is_ok());
235
236 let stream = StreamName::new("user_account:abc-123".to_string());
238 assert!(stream.is_ok());
239
240 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 let result = StreamName::new("stream name".to_string());
277 assert!(result.is_err());
278
279 let result = StreamName::new("stream.name".to_string());
281 assert!(result.is_err());
282
283 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 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 let stream = StreamName::new("complex:id:with:colons".to_string()).unwrap();
357 assert_eq!(stream.entity_id(), Some("id:with:colons"));
358
359 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 let json = serde_json::to_string(&stream).unwrap();
447 assert_eq!(json, "\"user:123\"");
448
449 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 let stream = StreamName::new_unchecked("invalid stream!".to_string());
465 assert_eq!(stream.as_str(), "invalid stream!");
466 }
467}