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 '{}' must be alphanumeric with hyphens, underscores, or colons",
157 value
158 )));
159 }
160
161 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 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 let stream = StreamName::new("my-stream".to_string());
228 assert!(stream.is_ok());
229 assert_eq!(stream.unwrap().as_str(), "my-stream");
230
231 let stream = StreamName::new("user:123".to_string());
233 assert!(stream.is_ok());
234
235 let stream = StreamName::new("order_stream".to_string());
237 assert!(stream.is_ok());
238
239 let stream = StreamName::new("user_account:abc-123".to_string());
241 assert!(stream.is_ok());
242
243 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 let result = StreamName::new("stream name".to_string());
280 assert!(result.is_err());
281
282 let result = StreamName::new("stream.name".to_string());
284 assert!(result.is_err());
285
286 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 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 let stream = StreamName::new("complex:id:with:colons".to_string()).unwrap();
360 assert_eq!(stream.entity_id(), Some("id:with:colons"));
361
362 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 let json = serde_json::to_string(&stream).unwrap();
450 assert_eq!(json, "\"user:123\"");
451
452 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 let stream = StreamName::new_unchecked("invalid stream!".to_string());
468 assert_eq!(stream.as_str(), "invalid stream!");
469 }
470}