use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EntityId(String);
impl EntityId {
pub fn new(value: String) -> Result<Self> {
Self::validate(&value)?;
Ok(Self(value))
}
pub(crate) fn new_unchecked(value: String) -> Self {
Self(value)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
pub fn starts_with(&self, prefix: &str) -> bool {
self.0.starts_with(prefix)
}
pub fn ends_with(&self, suffix: &str) -> bool {
self.0.ends_with(suffix)
}
pub fn prefix(&self, delimiter: char) -> Option<&str> {
self.0
.split(delimiter)
.next()
.filter(|_| self.0.contains(delimiter))
}
fn validate(value: &str) -> Result<()> {
if value.is_empty() {
return Err(crate::error::AllSourceError::InvalidInput(
"Entity ID cannot be empty".to_string(),
));
}
if value.len() > 128 {
return Err(crate::error::AllSourceError::InvalidInput(format!(
"Entity ID cannot exceed 128 characters, got {}",
value.len()
)));
}
if value.chars().any(char::is_control) {
return Err(crate::error::AllSourceError::InvalidInput(
"Entity ID cannot contain control characters".to_string(),
));
}
if value.trim().is_empty() {
return Err(crate::error::AllSourceError::InvalidInput(
"Entity ID cannot be only whitespace".to_string(),
));
}
if value != value.trim() {
return Err(crate::error::AllSourceError::InvalidInput(
"Entity ID cannot have leading or trailing whitespace".to_string(),
));
}
Ok(())
}
}
impl fmt::Display for EntityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<&str> for EntityId {
type Error = crate::error::AllSourceError;
fn try_from(value: &str) -> Result<Self> {
EntityId::new(value.to_string())
}
}
impl TryFrom<String> for EntityId {
type Error = crate::error::AllSourceError;
fn try_from(value: String) -> Result<Self> {
EntityId::new(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_valid_entity_ids() {
let entity_id = EntityId::new("user123".to_string());
assert!(entity_id.is_ok());
assert_eq!(entity_id.unwrap().as_str(), "user123");
let entity_id = EntityId::new("user-123".to_string());
assert!(entity_id.is_ok());
let entity_id = EntityId::new("user_123".to_string());
assert!(entity_id.is_ok());
let entity_id = EntityId::new("order_ABC-456-XYZ".to_string());
assert!(entity_id.is_ok());
let entity_id = EntityId::new("550e8400-e29b-41d4-a716-446655440000".to_string());
assert!(entity_id.is_ok());
let entity_id = EntityId::new("entity:123@domain".to_string());
assert!(entity_id.is_ok());
}
#[test]
fn test_reject_empty_entity_id() {
let result = EntityId::new(String::new());
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("cannot be empty"));
}
}
#[test]
fn test_reject_whitespace_only() {
let result = EntityId::new(" ".to_string());
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("cannot be only whitespace"));
}
}
#[test]
fn test_reject_leading_trailing_whitespace() {
let result = EntityId::new(" user-123".to_string());
assert!(result.is_err());
let result = EntityId::new("user-123 ".to_string());
assert!(result.is_err());
let result = EntityId::new(" user-123 ".to_string());
assert!(result.is_err());
if let Err(e) = EntityId::new(" test ".to_string()) {
assert!(e.to_string().contains("leading or trailing whitespace"));
}
}
#[test]
fn test_reject_too_long_entity_id() {
let long_id = "a".repeat(129);
let result = EntityId::new(long_id);
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("cannot exceed 128 characters"));
}
}
#[test]
fn test_accept_max_length_entity_id() {
let max_id = "a".repeat(128);
let result = EntityId::new(max_id);
assert!(result.is_ok());
}
#[test]
fn test_reject_control_characters() {
let result = EntityId::new("user\n123".to_string());
assert!(result.is_err());
let result = EntityId::new("user\t123".to_string());
assert!(result.is_err());
let result = EntityId::new("user\x00123".to_string());
assert!(result.is_err());
if let Err(e) = EntityId::new("test\n".to_string()) {
assert!(e.to_string().contains("control characters"));
}
}
#[test]
fn test_starts_with() {
let entity_id = EntityId::new("user-123".to_string()).unwrap();
assert!(entity_id.starts_with("user-"));
assert!(entity_id.starts_with("user"));
assert!(!entity_id.starts_with("order-"));
}
#[test]
fn test_ends_with() {
let entity_id = EntityId::new("user-123".to_string()).unwrap();
assert!(entity_id.ends_with("-123"));
assert!(entity_id.ends_with("123"));
assert!(!entity_id.ends_with("-456"));
}
#[test]
fn test_prefix_extraction() {
let entity_id = EntityId::new("user-123".to_string()).unwrap();
assert_eq!(entity_id.prefix('-'), Some("user"));
let entity_id = EntityId::new("order_ABC_456".to_string()).unwrap();
assert_eq!(entity_id.prefix('_'), Some("order"));
let entity_id = EntityId::new("simple".to_string()).unwrap();
assert_eq!(entity_id.prefix('-'), None);
}
#[test]
fn test_display_trait() {
let entity_id = EntityId::new("user-123".to_string()).unwrap();
assert_eq!(format!("{entity_id}"), "user-123");
}
#[test]
fn test_try_from_str() {
let entity_id: Result<EntityId> = "order-456".try_into();
assert!(entity_id.is_ok());
assert_eq!(entity_id.unwrap().as_str(), "order-456");
let invalid: Result<EntityId> = "".try_into();
assert!(invalid.is_err());
}
#[test]
fn test_try_from_string() {
let entity_id: Result<EntityId> = "product-789".to_string().try_into();
assert!(entity_id.is_ok());
let invalid: Result<EntityId> = String::new().try_into();
assert!(invalid.is_err());
}
#[test]
fn test_into_inner() {
let entity_id = EntityId::new("test-entity".to_string()).unwrap();
let inner = entity_id.into_inner();
assert_eq!(inner, "test-entity");
}
#[test]
fn test_equality() {
let id1 = EntityId::new("entity-a".to_string()).unwrap();
let id2 = EntityId::new("entity-a".to_string()).unwrap();
let id3 = EntityId::new("entity-b".to_string()).unwrap();
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_cloning() {
let id1 = EntityId::new("entity".to_string()).unwrap();
let id2 = id1.clone();
assert_eq!(id1, id2);
}
#[test]
fn test_hash_consistency() {
use std::collections::HashSet;
let id1 = EntityId::new("entity-123".to_string()).unwrap();
let id2 = EntityId::new("entity-123".to_string()).unwrap();
let mut set = HashSet::new();
set.insert(id1);
assert!(set.contains(&id2));
}
#[test]
fn test_serde_serialization() {
let entity_id = EntityId::new("user-123".to_string()).unwrap();
let json = serde_json::to_string(&entity_id).unwrap();
assert_eq!(json, "\"user-123\"");
let deserialized: EntityId = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, entity_id);
}
#[test]
fn test_new_unchecked() {
let entity_id = EntityId::new_unchecked("invalid\nid".to_string());
assert_eq!(entity_id.as_str(), "invalid\nid");
}
}