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 '{value}' must be lowercase with dots, underscores, or hyphens"
195 )));
196 }
197
198 if value.starts_with('.')
200 || value.starts_with('-')
201 || value.starts_with('_')
202 || value.ends_with('.')
203 || value.ends_with('-')
204 || value.ends_with('_')
205 {
206 return Err(crate::error::AllSourceError::InvalidInput(format!(
207 "Schema subject '{value}' cannot start or end with special characters"
208 )));
209 }
210
211 if value.contains("..") {
213 return Err(crate::error::AllSourceError::InvalidInput(format!(
214 "Schema subject '{value}' cannot have consecutive dots"
215 )));
216 }
217
218 Ok(())
219 }
220}
221
222impl fmt::Display for SchemaSubject {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 write!(f, "{}", self.0)
225 }
226}
227
228impl TryFrom<&str> for SchemaSubject {
229 type Error = crate::error::AllSourceError;
230
231 fn try_from(value: &str) -> Result<Self> {
232 SchemaSubject::new(value.to_string())
233 }
234}
235
236impl TryFrom<String> for SchemaSubject {
237 type Error = crate::error::AllSourceError;
238
239 fn try_from(value: String) -> Result<Self> {
240 SchemaSubject::new(value)
241 }
242}
243
244impl AsRef<str> for SchemaSubject {
245 fn as_ref(&self) -> &str {
246 &self.0
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_create_valid_subjects() {
256 let subject = SchemaSubject::new("user.created".to_string());
258 assert!(subject.is_ok());
259 assert_eq!(subject.unwrap().as_str(), "user.created");
260
261 let subject = SchemaSubject::new("user.profile.updated".to_string());
263 assert!(subject.is_ok());
264
265 let subject = SchemaSubject::new("order_item.created".to_string());
267 assert!(subject.is_ok());
268
269 let subject = SchemaSubject::new("payment-processed".to_string());
271 assert!(subject.is_ok());
272
273 let subject = SchemaSubject::new("event.v2.updated".to_string());
275 assert!(subject.is_ok());
276
277 let subject = SchemaSubject::new("created".to_string());
279 assert!(subject.is_ok());
280 }
281
282 #[test]
283 fn test_reject_empty_subject() {
284 let result = SchemaSubject::new(String::new());
285 assert!(result.is_err());
286
287 if let Err(e) = result {
288 assert!(e.to_string().contains("cannot be empty"));
289 }
290 }
291
292 #[test]
293 fn test_reject_too_long_subject() {
294 let long_subject = "a".repeat(257);
295 let result = SchemaSubject::new(long_subject);
296 assert!(result.is_err());
297
298 if let Err(e) = result {
299 assert!(e.to_string().contains("cannot exceed 256 characters"));
300 }
301 }
302
303 #[test]
304 fn test_accept_max_length_subject() {
305 let max_subject = "a".repeat(256);
306 let result = SchemaSubject::new(max_subject);
307 assert!(result.is_ok());
308 }
309
310 #[test]
311 fn test_reject_uppercase() {
312 let result = SchemaSubject::new("User.Created".to_string());
313 assert!(result.is_err());
314
315 let result = SchemaSubject::new("user.CREATED".to_string());
316 assert!(result.is_err());
317 }
318
319 #[test]
320 fn test_reject_invalid_characters() {
321 let result = SchemaSubject::new("user created".to_string());
323 assert!(result.is_err());
324
325 let result = SchemaSubject::new("user:created".to_string());
327 assert!(result.is_err());
328
329 let result = SchemaSubject::new("user@created".to_string());
331 assert!(result.is_err());
332 }
333
334 #[test]
335 fn test_reject_starting_with_special_char() {
336 let result = SchemaSubject::new(".user.created".to_string());
337 assert!(result.is_err());
338
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
346 #[test]
347 fn test_reject_ending_with_special_char() {
348 let result = SchemaSubject::new("user.created.".to_string());
349 assert!(result.is_err());
350
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
358 #[test]
359 fn test_reject_consecutive_dots() {
360 let result = SchemaSubject::new("user..created".to_string());
361 assert!(result.is_err());
362
363 if let Err(e) = result {
364 assert!(e.to_string().contains("consecutive dots"));
365 }
366 }
367
368 #[test]
369 fn test_namespace_extraction() {
370 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
371 assert_eq!(subject.namespace(), Some("user"));
372
373 let subject = SchemaSubject::new("user.profile.updated".to_string()).unwrap();
374 assert_eq!(subject.namespace(), Some("user"));
375
376 let subject = SchemaSubject::new("created".to_string()).unwrap();
378 assert_eq!(subject.namespace(), None);
379 }
380
381 #[test]
382 fn test_action_extraction() {
383 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
384 assert_eq!(subject.action(), Some("created"));
385
386 let subject = SchemaSubject::new("user.profile.updated".to_string()).unwrap();
387 assert_eq!(subject.action(), Some("updated"));
388
389 let subject = SchemaSubject::new("created".to_string()).unwrap();
391 assert_eq!(subject.action(), None);
392 }
393
394 #[test]
395 fn test_is_in_namespace() {
396 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
397 assert!(subject.is_in_namespace("user"));
398 assert!(!subject.is_in_namespace("order"));
399 }
400
401 #[test]
402 fn test_starts_with() {
403 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
404 assert!(subject.starts_with("user"));
405 assert!(subject.starts_with("user."));
406 assert!(!subject.starts_with("order"));
407 }
408
409 #[test]
410 fn test_matches_pattern_exact() {
411 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
412 assert!(subject.matches_pattern("user.created"));
413 assert!(!subject.matches_pattern("user.updated"));
414 }
415
416 #[test]
417 fn test_matches_pattern_wildcard() {
418 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
419 assert!(subject.matches_pattern("user.*"));
420 assert!(subject.matches_pattern("*.created"));
421 assert!(subject.matches_pattern("*.*"));
422 assert!(!subject.matches_pattern("order.*"));
423 }
424
425 #[test]
426 fn test_matches_pattern_double_wildcard() {
427 let subject = SchemaSubject::new("user.profile.updated".to_string()).unwrap();
428 assert!(subject.matches_pattern("**"));
429 assert!(subject.matches_pattern("user.**"));
430 assert!(subject.matches_pattern("user.profile.**"));
431 }
432
433 #[test]
434 fn test_display_trait() {
435 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
436 assert_eq!(format!("{subject}"), "user.created");
437 }
438
439 #[test]
440 fn test_try_from_str() {
441 let subject: Result<SchemaSubject> = "user.created".try_into();
442 assert!(subject.is_ok());
443 assert_eq!(subject.unwrap().as_str(), "user.created");
444
445 let invalid: Result<SchemaSubject> = "".try_into();
446 assert!(invalid.is_err());
447 }
448
449 #[test]
450 fn test_try_from_string() {
451 let subject: Result<SchemaSubject> = "order.placed".to_string().try_into();
452 assert!(subject.is_ok());
453
454 let invalid: Result<SchemaSubject> = String::new().try_into();
455 assert!(invalid.is_err());
456 }
457
458 #[test]
459 fn test_into_inner() {
460 let subject = SchemaSubject::new("test.subject".to_string()).unwrap();
461 let inner = subject.into_inner();
462 assert_eq!(inner, "test.subject");
463 }
464
465 #[test]
466 fn test_equality() {
467 let subject1 = SchemaSubject::new("user.created".to_string()).unwrap();
468 let subject2 = SchemaSubject::new("user.created".to_string()).unwrap();
469 let subject3 = SchemaSubject::new("order.placed".to_string()).unwrap();
470
471 assert_eq!(subject1, subject2);
472 assert_ne!(subject1, subject3);
473 }
474
475 #[test]
476 fn test_cloning() {
477 let subject1 = SchemaSubject::new("test.subject".to_string()).unwrap();
478 let subject2 = subject1.clone();
479 assert_eq!(subject1, subject2);
480 }
481
482 #[test]
483 fn test_hash_consistency() {
484 use std::collections::HashSet;
485
486 let subject1 = SchemaSubject::new("user.created".to_string()).unwrap();
487 let subject2 = SchemaSubject::new("user.created".to_string()).unwrap();
488
489 let mut set = HashSet::new();
490 set.insert(subject1);
491
492 assert!(set.contains(&subject2));
493 }
494
495 #[test]
496 fn test_serde_serialization() {
497 let subject = SchemaSubject::new("user.created".to_string()).unwrap();
498
499 let json = serde_json::to_string(&subject).unwrap();
501 assert_eq!(json, "\"user.created\"");
502
503 let deserialized: SchemaSubject = serde_json::from_str(&json).unwrap();
505 assert_eq!(deserialized, subject);
506 }
507
508 #[test]
509 fn test_as_ref() {
510 let subject = SchemaSubject::new("test.subject".to_string()).unwrap();
511 let str_ref: &str = subject.as_ref();
512 assert_eq!(str_ref, "test.subject");
513 }
514
515 #[test]
516 fn test_new_unchecked() {
517 let subject = SchemaSubject::new_unchecked("INVALID Subject!".to_string());
519 assert_eq!(subject.as_str(), "INVALID Subject!");
520 }
521}