allsource_core/domain/value_objects/
projection_name.rs1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct ProjectionName(String);
25
26impl ProjectionName {
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 starts_with(&self, prefix: &str) -> bool {
79 self.0.starts_with(prefix)
80 }
81
82 pub fn ends_with(&self, suffix: &str) -> bool {
95 self.0.ends_with(suffix)
96 }
97
98 pub fn contains(&self, pattern: &str) -> bool {
100 self.0.contains(pattern)
101 }
102
103 fn validate(value: &str) -> Result<()> {
105 if value.is_empty() {
107 return Err(crate::error::AllSourceError::InvalidInput(
108 "Projection name cannot be empty".to_string(),
109 ));
110 }
111
112 if value.len() > 100 {
114 return Err(crate::error::AllSourceError::InvalidInput(format!(
115 "Projection name cannot exceed 100 characters, got {}",
116 value.len()
117 )));
118 }
119
120 if !value
122 .chars()
123 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
124 {
125 return Err(crate::error::AllSourceError::InvalidInput(format!(
126 "Projection name '{}' must be alphanumeric with hyphens or underscores",
127 value
128 )));
129 }
130
131 if value.starts_with('-')
133 || value.starts_with('_')
134 || value.ends_with('-')
135 || value.ends_with('_')
136 {
137 return Err(crate::error::AllSourceError::InvalidInput(format!(
138 "Projection name '{}' cannot start or end with hyphen or underscore",
139 value
140 )));
141 }
142
143 Ok(())
144 }
145}
146
147impl fmt::Display for ProjectionName {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 write!(f, "{}", self.0)
150 }
151}
152
153impl TryFrom<&str> for ProjectionName {
154 type Error = crate::error::AllSourceError;
155
156 fn try_from(value: &str) -> Result<Self> {
157 ProjectionName::new(value.to_string())
158 }
159}
160
161impl TryFrom<String> for ProjectionName {
162 type Error = crate::error::AllSourceError;
163
164 fn try_from(value: String) -> Result<Self> {
165 ProjectionName::new(value)
166 }
167}
168
169impl AsRef<str> for ProjectionName {
170 fn as_ref(&self) -> &str {
171 &self.0
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn test_create_valid_names() {
181 let name = ProjectionName::new("user_snapshot".to_string());
183 assert!(name.is_ok());
184 assert_eq!(name.unwrap().as_str(), "user_snapshot");
185
186 let name = ProjectionName::new("event-counter".to_string());
188 assert!(name.is_ok());
189
190 let name = ProjectionName::new("projection123".to_string());
192 assert!(name.is_ok());
193
194 let name = ProjectionName::new("user-account_snapshot123".to_string());
196 assert!(name.is_ok());
197 }
198
199 #[test]
200 fn test_reject_empty_name() {
201 let result = ProjectionName::new("".to_string());
202 assert!(result.is_err());
203
204 if let Err(e) = result {
205 assert!(e.to_string().contains("cannot be empty"));
206 }
207 }
208
209 #[test]
210 fn test_reject_too_long_name() {
211 let long_name = "a".repeat(101);
212 let result = ProjectionName::new(long_name);
213 assert!(result.is_err());
214
215 if let Err(e) = result {
216 assert!(e.to_string().contains("cannot exceed 100 characters"));
217 }
218 }
219
220 #[test]
221 fn test_accept_max_length_name() {
222 let max_name = "a".repeat(100);
223 let result = ProjectionName::new(max_name);
224 assert!(result.is_ok());
225 }
226
227 #[test]
228 fn test_reject_invalid_characters() {
229 let result = ProjectionName::new("user snapshot".to_string());
231 assert!(result.is_err());
232
233 let result = ProjectionName::new("user.snapshot".to_string());
235 assert!(result.is_err());
236
237 let result = ProjectionName::new("user:snapshot".to_string());
239 assert!(result.is_err());
240
241 let result = ProjectionName::new("user@snapshot".to_string());
243 assert!(result.is_err());
244 }
245
246 #[test]
247 fn test_reject_starting_with_special_char() {
248 let result = ProjectionName::new("-projection".to_string());
249 assert!(result.is_err());
250
251 let result = ProjectionName::new("_projection".to_string());
252 assert!(result.is_err());
253 }
254
255 #[test]
256 fn test_reject_ending_with_special_char() {
257 let result = ProjectionName::new("projection-".to_string());
258 assert!(result.is_err());
259
260 let result = ProjectionName::new("projection_".to_string());
261 assert!(result.is_err());
262 }
263
264 #[test]
265 fn test_starts_with() {
266 let name = ProjectionName::new("user_snapshot".to_string()).unwrap();
267 assert!(name.starts_with("user"));
268 assert!(name.starts_with("user_"));
269 assert!(!name.starts_with("order"));
270 }
271
272 #[test]
273 fn test_ends_with() {
274 let name = ProjectionName::new("user_snapshot".to_string()).unwrap();
275 assert!(name.ends_with("snapshot"));
276 assert!(name.ends_with("_snapshot"));
277 assert!(!name.ends_with("counter"));
278 }
279
280 #[test]
281 fn test_contains() {
282 let name = ProjectionName::new("user_snapshot".to_string()).unwrap();
283 assert!(name.contains("user"));
284 assert!(name.contains("_"));
285 assert!(name.contains("snap"));
286 assert!(!name.contains("order"));
287 }
288
289 #[test]
290 fn test_display_trait() {
291 let name = ProjectionName::new("user_snapshot".to_string()).unwrap();
292 assert_eq!(format!("{}", name), "user_snapshot");
293 }
294
295 #[test]
296 fn test_try_from_str() {
297 let name: Result<ProjectionName> = "user_snapshot".try_into();
298 assert!(name.is_ok());
299 assert_eq!(name.unwrap().as_str(), "user_snapshot");
300
301 let invalid: Result<ProjectionName> = "".try_into();
302 assert!(invalid.is_err());
303 }
304
305 #[test]
306 fn test_try_from_string() {
307 let name: Result<ProjectionName> = "event_counter".to_string().try_into();
308 assert!(name.is_ok());
309
310 let invalid: Result<ProjectionName> = String::new().try_into();
311 assert!(invalid.is_err());
312 }
313
314 #[test]
315 fn test_into_inner() {
316 let name = ProjectionName::new("test_projection".to_string()).unwrap();
317 let inner = name.into_inner();
318 assert_eq!(inner, "test_projection");
319 }
320
321 #[test]
322 fn test_equality() {
323 let name1 = ProjectionName::new("user_snapshot".to_string()).unwrap();
324 let name2 = ProjectionName::new("user_snapshot".to_string()).unwrap();
325 let name3 = ProjectionName::new("order_snapshot".to_string()).unwrap();
326
327 assert_eq!(name1, name2);
328 assert_ne!(name1, name3);
329 }
330
331 #[test]
332 fn test_cloning() {
333 let name1 = ProjectionName::new("test_projection".to_string()).unwrap();
334 let name2 = name1.clone();
335 assert_eq!(name1, name2);
336 }
337
338 #[test]
339 fn test_hash_consistency() {
340 use std::collections::HashSet;
341
342 let name1 = ProjectionName::new("user_snapshot".to_string()).unwrap();
343 let name2 = ProjectionName::new("user_snapshot".to_string()).unwrap();
344
345 let mut set = HashSet::new();
346 set.insert(name1);
347
348 assert!(set.contains(&name2));
349 }
350
351 #[test]
352 fn test_serde_serialization() {
353 let name = ProjectionName::new("user_snapshot".to_string()).unwrap();
354
355 let json = serde_json::to_string(&name).unwrap();
357 assert_eq!(json, "\"user_snapshot\"");
358
359 let deserialized: ProjectionName = serde_json::from_str(&json).unwrap();
361 assert_eq!(deserialized, name);
362 }
363
364 #[test]
365 fn test_as_ref() {
366 let name = ProjectionName::new("test_projection".to_string()).unwrap();
367 let str_ref: &str = name.as_ref();
368 assert_eq!(str_ref, "test_projection");
369 }
370
371 #[test]
372 fn test_new_unchecked() {
373 let name = ProjectionName::new_unchecked("invalid name!".to_string());
375 assert_eq!(name.as_str(), "invalid name!");
376 }
377}