allsource_core/domain/value_objects/
entity_id.rs1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct EntityId(String);
24
25impl EntityId {
26 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 as_str(&self) -> &str {
61 &self.0
62 }
63
64 pub fn into_inner(self) -> String {
66 self.0
67 }
68
69 pub fn starts_with(&self, prefix: &str) -> bool {
80 self.0.starts_with(prefix)
81 }
82
83 pub fn ends_with(&self, suffix: &str) -> bool {
94 self.0.ends_with(suffix)
95 }
96
97 pub fn prefix(&self, delimiter: char) -> Option<&str> {
110 self.0.split(delimiter).next().filter(|_| self.0.contains(delimiter))
111 }
112
113 fn validate(value: &str) -> Result<()> {
115 if value.is_empty() {
117 return Err(crate::error::AllSourceError::InvalidInput(
118 "Entity ID cannot be empty".to_string(),
119 ));
120 }
121
122 if value.len() > 128 {
124 return Err(crate::error::AllSourceError::InvalidInput(
125 format!("Entity ID cannot exceed 128 characters, got {}", value.len()),
126 ));
127 }
128
129 if value.chars().any(|c| c.is_control()) {
131 return Err(crate::error::AllSourceError::InvalidInput(
132 "Entity ID cannot contain control characters".to_string(),
133 ));
134 }
135
136 if value.trim().is_empty() {
138 return Err(crate::error::AllSourceError::InvalidInput(
139 "Entity ID cannot be only whitespace".to_string(),
140 ));
141 }
142
143 if value != value.trim() {
145 return Err(crate::error::AllSourceError::InvalidInput(
146 "Entity ID cannot have leading or trailing whitespace".to_string(),
147 ));
148 }
149
150 Ok(())
151 }
152}
153
154impl fmt::Display for EntityId {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 write!(f, "{}", self.0)
158 }
159}
160
161impl TryFrom<&str> for EntityId {
163 type Error = crate::error::AllSourceError;
164
165 fn try_from(value: &str) -> Result<Self> {
166 EntityId::new(value.to_string())
167 }
168}
169
170impl TryFrom<String> for EntityId {
172 type Error = crate::error::AllSourceError;
173
174 fn try_from(value: String) -> Result<Self> {
175 EntityId::new(value)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_create_valid_entity_ids() {
185 let entity_id = EntityId::new("user123".to_string());
187 assert!(entity_id.is_ok());
188 assert_eq!(entity_id.unwrap().as_str(), "user123");
189
190 let entity_id = EntityId::new("user-123".to_string());
192 assert!(entity_id.is_ok());
193
194 let entity_id = EntityId::new("user_123".to_string());
196 assert!(entity_id.is_ok());
197
198 let entity_id = EntityId::new("order_ABC-456-XYZ".to_string());
200 assert!(entity_id.is_ok());
201
202 let entity_id = EntityId::new("550e8400-e29b-41d4-a716-446655440000".to_string());
204 assert!(entity_id.is_ok());
205
206 let entity_id = EntityId::new("entity:123@domain".to_string());
208 assert!(entity_id.is_ok());
209 }
210
211 #[test]
212 fn test_reject_empty_entity_id() {
213 let result = EntityId::new("".to_string());
214 assert!(result.is_err());
215
216 if let Err(e) = result {
217 assert!(e.to_string().contains("cannot be empty"));
218 }
219 }
220
221 #[test]
222 fn test_reject_whitespace_only() {
223 let result = EntityId::new(" ".to_string());
224 assert!(result.is_err());
225
226 if let Err(e) = result {
227 assert!(e.to_string().contains("cannot be only whitespace"));
228 }
229 }
230
231 #[test]
232 fn test_reject_leading_trailing_whitespace() {
233 let result = EntityId::new(" user-123".to_string());
235 assert!(result.is_err());
236
237 let result = EntityId::new("user-123 ".to_string());
239 assert!(result.is_err());
240
241 let result = EntityId::new(" user-123 ".to_string());
243 assert!(result.is_err());
244
245 if let Err(e) = EntityId::new(" test ".to_string()) {
246 assert!(e.to_string().contains("leading or trailing whitespace"));
247 }
248 }
249
250 #[test]
251 fn test_reject_too_long_entity_id() {
252 let long_id = "a".repeat(129);
254 let result = EntityId::new(long_id);
255 assert!(result.is_err());
256
257 if let Err(e) = result {
258 assert!(e.to_string().contains("cannot exceed 128 characters"));
259 }
260 }
261
262 #[test]
263 fn test_accept_max_length_entity_id() {
264 let max_id = "a".repeat(128);
266 let result = EntityId::new(max_id);
267 assert!(result.is_ok());
268 }
269
270 #[test]
271 fn test_reject_control_characters() {
272 let result = EntityId::new("user\n123".to_string());
274 assert!(result.is_err());
275
276 let result = EntityId::new("user\t123".to_string());
278 assert!(result.is_err());
279
280 let result = EntityId::new("user\0123".to_string());
282 assert!(result.is_err());
283
284 if let Err(e) = EntityId::new("test\n".to_string()) {
285 assert!(e.to_string().contains("control characters"));
286 }
287 }
288
289 #[test]
290 fn test_starts_with() {
291 let entity_id = EntityId::new("user-123".to_string()).unwrap();
292 assert!(entity_id.starts_with("user-"));
293 assert!(entity_id.starts_with("user"));
294 assert!(!entity_id.starts_with("order-"));
295 }
296
297 #[test]
298 fn test_ends_with() {
299 let entity_id = EntityId::new("user-123".to_string()).unwrap();
300 assert!(entity_id.ends_with("-123"));
301 assert!(entity_id.ends_with("123"));
302 assert!(!entity_id.ends_with("-456"));
303 }
304
305 #[test]
306 fn test_prefix_extraction() {
307 let entity_id = EntityId::new("user-123".to_string()).unwrap();
308 assert_eq!(entity_id.prefix('-'), Some("user"));
309
310 let entity_id = EntityId::new("order_ABC_456".to_string()).unwrap();
311 assert_eq!(entity_id.prefix('_'), Some("order"));
312
313 let entity_id = EntityId::new("simple".to_string()).unwrap();
315 assert_eq!(entity_id.prefix('-'), None);
316 }
317
318 #[test]
319 fn test_display_trait() {
320 let entity_id = EntityId::new("user-123".to_string()).unwrap();
321 assert_eq!(format!("{}", entity_id), "user-123");
322 }
323
324 #[test]
325 fn test_try_from_str() {
326 let entity_id: Result<EntityId> = "order-456".try_into();
327 assert!(entity_id.is_ok());
328 assert_eq!(entity_id.unwrap().as_str(), "order-456");
329
330 let invalid: Result<EntityId> = "".try_into();
331 assert!(invalid.is_err());
332 }
333
334 #[test]
335 fn test_try_from_string() {
336 let entity_id: Result<EntityId> = "product-789".to_string().try_into();
337 assert!(entity_id.is_ok());
338
339 let invalid: Result<EntityId> = String::new().try_into();
340 assert!(invalid.is_err());
341 }
342
343 #[test]
344 fn test_into_inner() {
345 let entity_id = EntityId::new("test-entity".to_string()).unwrap();
346 let inner = entity_id.into_inner();
347 assert_eq!(inner, "test-entity");
348 }
349
350 #[test]
351 fn test_equality() {
352 let id1 = EntityId::new("entity-a".to_string()).unwrap();
353 let id2 = EntityId::new("entity-a".to_string()).unwrap();
354 let id3 = EntityId::new("entity-b".to_string()).unwrap();
355
356 assert_eq!(id1, id2);
358 assert_ne!(id1, id3);
359 }
360
361 #[test]
362 fn test_cloning() {
363 let id1 = EntityId::new("entity".to_string()).unwrap();
364 let id2 = id1.clone();
365 assert_eq!(id1, id2);
366 }
367
368 #[test]
369 fn test_hash_consistency() {
370 use std::collections::HashSet;
371
372 let id1 = EntityId::new("entity-123".to_string()).unwrap();
373 let id2 = EntityId::new("entity-123".to_string()).unwrap();
374
375 let mut set = HashSet::new();
376 set.insert(id1);
377
378 assert!(set.contains(&id2));
380 }
381
382 #[test]
383 fn test_serde_serialization() {
384 let entity_id = EntityId::new("user-123".to_string()).unwrap();
385
386 let json = serde_json::to_string(&entity_id).unwrap();
388 assert_eq!(json, "\"user-123\"");
389
390 let deserialized: EntityId = serde_json::from_str(&json).unwrap();
392 assert_eq!(deserialized, entity_id);
393 }
394
395 #[test]
396 fn test_new_unchecked() {
397 let entity_id = EntityId::new_unchecked("invalid\nid".to_string());
399 assert_eq!(entity_id.as_str(), "invalid\nid");
400 }
401}