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