use std::{
fmt,
hash::{Hash, Hasher},
};
use crate::error::{FraiseQLError, Result};
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct EntityKey {
pub entity_type: String,
pub entity_id: String,
}
impl EntityKey {
pub fn new(entity_type: &str, entity_id: &str) -> Result<Self> {
if entity_type.is_empty() {
return Err(FraiseQLError::Validation {
message: "entity_type cannot be empty".to_string(),
path: None,
});
}
if entity_type.contains(':') {
return Err(FraiseQLError::Validation {
message: format!(
"entity_type {entity_type:?} must not contain a colon character; \
colons are used as the cache-key separator"
),
path: None,
});
}
if entity_id.is_empty() {
return Err(FraiseQLError::Validation {
message: "entity_id cannot be empty".to_string(),
path: None,
});
}
Ok(Self {
entity_type: entity_type.to_string(),
entity_id: entity_id.to_string(),
})
}
#[must_use]
pub fn to_cache_key(&self) -> String {
format!("{}:{}", self.entity_type, self.entity_id)
}
pub fn from_cache_key(cache_key: &str) -> Result<Self> {
let parts: Vec<&str> = cache_key.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(FraiseQLError::Validation {
message: format!("Invalid entity key format: {}. Expected 'Type:id'", cache_key),
path: None,
});
}
Self::new(parts[0], parts[1])
}
}
impl fmt::Display for EntityKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_cache_key())
}
}
impl Hash for EntityKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.entity_type.hash(state);
self.entity_id.hash(state);
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_create_valid_entity_key() {
let key = EntityKey::new("User", "550e8400-e29b-41d4-a716-446655440000").unwrap();
assert_eq!(key.entity_type, "User");
assert_eq!(key.entity_id, "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_reject_empty_entity_type() {
let result = EntityKey::new("", "550e8400-e29b-41d4-a716-446655440000");
assert!(
matches!(result, Err(FraiseQLError::Validation { .. })),
"expected Validation error for empty entity_type, got: {result:?}"
);
}
#[test]
fn test_reject_empty_entity_id() {
let result = EntityKey::new("User", "");
assert!(
matches!(result, Err(FraiseQLError::Validation { .. })),
"expected Validation error for empty entity_id, got: {result:?}"
);
}
#[test]
fn test_serialize_to_cache_key_format() {
let key = EntityKey::new("User", "550e8400-e29b-41d4-a716-446655440000").unwrap();
let cache_key = key.to_cache_key();
assert_eq!(cache_key, "User:550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_deserialize_from_cache_key_format() {
let cache_key = "User:550e8400-e29b-41d4-a716-446655440000";
let key = EntityKey::from_cache_key(cache_key).unwrap();
assert_eq!(key.entity_type, "User");
assert_eq!(key.entity_id, "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_reject_colon_in_entity_type() {
let result = EntityKey::new("User:Admin", "550e8400-e29b-41d4-a716-446655440000");
assert!(result.is_err(), "colon in entity_type must be rejected");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("colon") || msg.contains("separator"),
"error should mention the separator: {msg}"
);
}
#[test]
fn test_reject_colon_only_in_entity_type() {
let result = EntityKey::new(":", "some-id");
assert!(
matches!(result, Err(FraiseQLError::Validation { .. })),
"expected Validation error for colon-only entity_type, got: {result:?}"
);
}
#[test]
fn test_entity_id_may_contain_colon() {
let result = EntityKey::new("User", "urn:uuid:550e8400-e29b-41d4-a716-446655440000");
assert!(result.is_ok(), "colon in entity_id must be accepted");
let key = result.unwrap();
let cache_key = key.to_cache_key();
let parsed = EntityKey::from_cache_key(&cache_key).unwrap();
assert_eq!(parsed.entity_type, "User");
assert_eq!(parsed.entity_id, "urn:uuid:550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_hash_consistency_for_hashmap() {
use std::collections::HashMap;
let key1 = EntityKey::new("User", "550e8400-e29b-41d4-a716-446655440000").unwrap();
let key2 = EntityKey::new("User", "550e8400-e29b-41d4-a716-446655440000").unwrap();
let mut map = HashMap::new();
map.insert(key1, "value1");
assert_eq!(map.get(&key2), Some(&"value1"));
let key3 = EntityKey::new("User", "550e8400-e29b-41d4-a716-446655440001").unwrap();
assert_eq!(map.get(&key3), None);
}
}