allsource_core/domain/value_objects/
schema_subject.rs1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct SchemaSubject(String);
25
26impl SchemaSubject {
27 pub fn new(value: String) -> Result<Self> {
43 Self::validate(&value)?;
44 Ok(Self(value))
45 }
46
47 pub(crate) fn new_unchecked(value: String) -> Self {
53 Self(value)
54 }
55
56 pub fn as_str(&self) -> &str {
58 &self.0
59 }
60
61 pub fn into_inner(self) -> String {
63 self.0
64 }
65
66 pub fn namespace(&self) -> Option<&str> {
79 self.0.split('.').next().filter(|_| self.0.contains('.'))
80 }
81
82 pub fn action(&self) -> Option<&str> {
95 self.0.rsplit('.').next().filter(|_| self.0.contains('.'))
96 }
97
98 pub fn is_in_namespace(&self, namespace: &str) -> bool {
109 self.namespace() == Some(namespace)
110 }
111
112 pub fn starts_with(&self, prefix: &str) -> bool {
114 self.0.starts_with(prefix)
115 }
116
117 pub fn matches_pattern(&self, pattern: &str) -> bool {
131 if pattern == "**" {
132 return true;
133 }
134
135 let subject_parts: Vec<&str> = self.0.split('.').collect();
136 let pattern_parts: Vec<&str> = pattern.split('.').collect();
137
138 if pattern.contains("**") {
139 let prefix: Vec<&str> = pattern_parts
141 .iter()
142 .take_while(|&&p| p != "**")
143 .copied()
144 .collect();
145
146 if subject_parts.len() < prefix.len() {
147 return false;
148 }
149
150 for (s, p) in subject_parts.iter().zip(prefix.iter()) {
151 if *p != "*" && *s != *p {
152 return false;
153 }
154 }
155 true
156 } else {
157 if subject_parts.len() != pattern_parts.len() {
159 return false;
160 }
161
162 for (s, p) in subject_parts.iter().zip(pattern_parts.iter()) {
163 if *p != "*" && *s != *p {
164 return false;
165 }
166 }
167 true
168 }
169 }
170
171 fn validate(value: &str) -> Result<()> {
173 if value.is_empty() {
175 return Err(crate::error::AllSourceError::InvalidInput(
176 "Schema subject cannot be empty".to_string(),
177 ));
178 }
179
180 if value.len() > 256 {
182 return Err(crate::error::AllSourceError::InvalidInput(format!(
183 "Schema subject cannot exceed 256 characters, got {}",
184 value.len()
185 )));
186 }
187
188 if !value
190 .chars()
191 .all(|c| c.is_lowercase() || c.is_numeric() || c == '.' || c == '_' || c == '-')
192 {
193 return Err(crate::error::AllSourceError::InvalidInput(format!(
194 "Schema subject '{}' must be lowercase with dots, underscores, or hyphens",
195 value
196 )));
197 }
198
199 if value.starts_with('.')
201 || value.starts_with('-')
202 || value.starts_with('_')
203 || value.ends_with('.')
204 || value.ends_with('-')
205 || value.ends_with('_')
206 {
207 return Err(crate::error::AllSourceError::InvalidInput(format!(
208 "Schema subject '{}' cannot start or end with special characters",
209 value
210 )));
211 }
212
213 if value.contains("..") {
215 return Err(crate::error::AllSourceError::InvalidInput(format!(
216 "Schema subject '{}' cannot have consecutive dots",
217 value
218 )));
219 }
220
221 Ok(())
222 }
223}
224
225impl fmt::Display for SchemaSubject {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 write!(f, "{}", self.0)
228 }
229}
230
231impl TryFrom<&str> for SchemaSubject {
232 type Error = crate::error::AllSourceError;
233
234 fn try_from(value: &str) -> Result<Self> {
235 SchemaSubject::new(value.to_string())
236 }
237}
238
239impl TryFrom<String> for SchemaSubject {
240 type Error = crate::error::AllSourceError;
241
242 fn try_from(value: String) -> Result<Self> {
243 SchemaSubject::new(value)
244 }
245}
246
247impl AsRef<str> for SchemaSubject {
248 fn as_ref(&self) -> &str {
249 &self.0
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_create_valid_subjects() {
259 let subject = SchemaSubject::new("user.created".to_string());
261 assert!(subject.is_ok());
262 assert_eq!(subject.unwrap().as_str(), "user.created");
263
264 let subject = SchemaSubject::new("user.profile.updated".to_string());
266 assert!(subject.is_ok());
267
268 let subject = SchemaSubject::new("order_item.created".to_string());
270 assert!(subject.is_ok());
271
272 let subject = SchemaSubject::new("payment-processed".to_string());
274 assert!(subject.is_ok());
275
276 let subject = SchemaSubject::new("event.v2.updated".to_string());
278 assert!(subject.is_ok());
279
280 let subject = SchemaSubject::new("created".to_string());
282 assert!(subject.is_ok());
283 }
284
285 #[test]
286 fn test_reject_empty_subject() {
287 let result = SchemaSubject::new("".to_string());
288 assert!(result.is_err());
289
290 if let Err(e) = result {
291 assert!(e.to_string().contains("cannot be empty"));
292 }
293 }
294
295 #[test]
296 fn test_reject_too_long_subject() {
297 let long_subject = "a".repeat(257);
298 let result = SchemaSubject::new(long_subject);
299 assert!(result.is_err());
300
301 if let Err(e) = result {
302 assert!(e.to_string().contains("cannot exceed 256 characters"));
303 }
304 }
305
306 #[test]
307 fn test_accept_max_length_subject() {
308 let max_subject = "a".repeat(256);
309 let result = SchemaSubject::new(max_subject);
310 assert!(result.is_ok());
311 }
312
313 #[test]
314 fn test_reject_uppercase() {
315 let result = SchemaSubject::new("User.Created".to_string());
316 assert!(result.is_err());
317
318 let result = SchemaSubject::new("user.CREATED".to_string());
319 assert!(result.is_err());
320 }
321
322 #[test]
323 fn test_reject_invalid_characters() {
324 let result = SchemaSubject::new("user created".to_string());
326 assert!(result.is_err());
327
328 let result = SchemaSubject::new("user:created".to_string());
330 assert!(result.is_err());
331
332 let result = SchemaSubject::new("user@created".to_string());
334 assert!(result.is_err());
335 }
336
337 #[test]
338 fn test_reject_starting_with_special_char() {
339 let result = SchemaSubject::new(".user.created".to_string());
340 assert!(result.is_err());
341
342 let result = SchemaSubject::new("-user.created".to_string());
343 assert!(result.is_err());
344
345 let result = SchemaSubject::new("_user.created".to_string());
346 assert!(result.is_err());
347 }
348
349 #[test]
350 fn test_reject_ending_with_special_char() {
351 let result = SchemaSubject::new("user.created.".to_string());
352 assert!(result.is_err());
353
354 let result = SchemaSubject::new("user.created-".to_string());
355 assert!(result.is_err());
356
357 let result = SchemaSubject::new("user.created_".to_string());
358 assert!(result.is_err());
359 }
360
361 #[test]
362 fn test_reject_consecutive_dots() {
363 let result = SchemaSubject::new("user..created".to_string());
364 assert!(result.is_err());
365
366 if let Err(e) = result {
367 assert!(e.to_string().contains("consecutive dots"));
368 }
369 }
370
371 #[test]
372 fn test_namespace_extraction() {
373 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
374 assert_eq!(subject.namespace(), Some("user"));
375
376 let subject = SchemaSubject::new("user.profile.updated".to_string()).unwrap();
377 assert_eq!(subject.namespace(), Some("user"));
378
379 let subject = SchemaSubject::new("created".to_string()).unwrap();
381 assert_eq!(subject.namespace(), None);
382 }
383
384 #[test]
385 fn test_action_extraction() {
386 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
387 assert_eq!(subject.action(), Some("created"));
388
389 let subject = SchemaSubject::new("user.profile.updated".to_string()).unwrap();
390 assert_eq!(subject.action(), Some("updated"));
391
392 let subject = SchemaSubject::new("created".to_string()).unwrap();
394 assert_eq!(subject.action(), None);
395 }
396
397 #[test]
398 fn test_is_in_namespace() {
399 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
400 assert!(subject.is_in_namespace("user"));
401 assert!(!subject.is_in_namespace("order"));
402 }
403
404 #[test]
405 fn test_starts_with() {
406 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
407 assert!(subject.starts_with("user"));
408 assert!(subject.starts_with("user."));
409 assert!(!subject.starts_with("order"));
410 }
411
412 #[test]
413 fn test_matches_pattern_exact() {
414 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
415 assert!(subject.matches_pattern("user.created"));
416 assert!(!subject.matches_pattern("user.updated"));
417 }
418
419 #[test]
420 fn test_matches_pattern_wildcard() {
421 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
422 assert!(subject.matches_pattern("user.*"));
423 assert!(subject.matches_pattern("*.created"));
424 assert!(subject.matches_pattern("*.*"));
425 assert!(!subject.matches_pattern("order.*"));
426 }
427
428 #[test]
429 fn test_matches_pattern_double_wildcard() {
430 let subject = SchemaSubject::new("user.profile.updated".to_string()).unwrap();
431 assert!(subject.matches_pattern("**"));
432 assert!(subject.matches_pattern("user.**"));
433 assert!(subject.matches_pattern("user.profile.**"));
434 }
435
436 #[test]
437 fn test_display_trait() {
438 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
439 assert_eq!(format!("{}", subject), "user.created");
440 }
441
442 #[test]
443 fn test_try_from_str() {
444 let subject: Result<SchemaSubject> = "user.created".try_into();
445 assert!(subject.is_ok());
446 assert_eq!(subject.unwrap().as_str(), "user.created");
447
448 let invalid: Result<SchemaSubject> = "".try_into();
449 assert!(invalid.is_err());
450 }
451
452 #[test]
453 fn test_try_from_string() {
454 let subject: Result<SchemaSubject> = "order.placed".to_string().try_into();
455 assert!(subject.is_ok());
456
457 let invalid: Result<SchemaSubject> = String::new().try_into();
458 assert!(invalid.is_err());
459 }
460
461 #[test]
462 fn test_into_inner() {
463 let subject = SchemaSubject::new("test.subject".to_string()).unwrap();
464 let inner = subject.into_inner();
465 assert_eq!(inner, "test.subject");
466 }
467
468 #[test]
469 fn test_equality() {
470 let subject1 = SchemaSubject::new("user.created".to_string()).unwrap();
471 let subject2 = SchemaSubject::new("user.created".to_string()).unwrap();
472 let subject3 = SchemaSubject::new("order.placed".to_string()).unwrap();
473
474 assert_eq!(subject1, subject2);
475 assert_ne!(subject1, subject3);
476 }
477
478 #[test]
479 fn test_cloning() {
480 let subject1 = SchemaSubject::new("test.subject".to_string()).unwrap();
481 let subject2 = subject1.clone();
482 assert_eq!(subject1, subject2);
483 }
484
485 #[test]
486 fn test_hash_consistency() {
487 use std::collections::HashSet;
488
489 let subject1 = SchemaSubject::new("user.created".to_string()).unwrap();
490 let subject2 = SchemaSubject::new("user.created".to_string()).unwrap();
491
492 let mut set = HashSet::new();
493 set.insert(subject1);
494
495 assert!(set.contains(&subject2));
496 }
497
498 #[test]
499 fn test_serde_serialization() {
500 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
501
502 let json = serde_json::to_string(&subject).unwrap();
504 assert_eq!(json, "\"user.created\"");
505
506 let deserialized: SchemaSubject = serde_json::from_str(&json).unwrap();
508 assert_eq!(deserialized, subject);
509 }
510
511 #[test]
512 fn test_as_ref() {
513 let subject = SchemaSubject::new("test.subject".to_string()).unwrap();
514 let str_ref: &str = subject.as_ref();
515 assert_eq!(str_ref, "test.subject");
516 }
517
518 #[test]
519 fn test_new_unchecked() {
520 let subject = SchemaSubject::new_unchecked("INVALID Subject!".to_string());
522 assert_eq!(subject.as_str(), "INVALID Subject!");
523 }
524}