use super::value_object_trait::{CompositeValidator, ValueObject};
use super::{EmailAddress, Name, ResourceId, UserName};
use crate::error::{ValidationError, ValidationResult};
use std::collections::HashSet;
#[derive(Debug)]
pub struct UniquePrimaryValidator;
impl UniquePrimaryValidator {
pub fn new() -> Self {
Self
}
fn has_primary_values(&self, obj: &dyn ValueObject) -> bool {
let attr_name = obj.attribute_name();
matches!(
attr_name,
"emails" | "phoneNumbers" | "addresses" | "members"
)
}
fn validate_primary_uniqueness(&self, obj: &dyn ValueObject) -> ValidationResult<()> {
if self.has_primary_values(obj) {
Ok(())
} else {
Ok(())
}
}
}
impl CompositeValidator for UniquePrimaryValidator {
fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
for obj in objects {
self.validate_primary_uniqueness(obj.as_ref())?;
}
Ok(())
}
fn dependent_attributes(&self) -> Vec<String> {
vec![
"emails".to_string(),
"phoneNumbers".to_string(),
"addresses".to_string(),
"members".to_string(),
]
}
fn applies_to(&self, attribute_names: &[String]) -> bool {
let dependent = self.dependent_attributes();
attribute_names.iter().any(|name| dependent.contains(name))
}
}
#[derive(Debug)]
pub struct UserNameUniquenessValidator {
case_insensitive: bool,
reserved_names: HashSet<String>,
}
impl UserNameUniquenessValidator {
pub fn new(case_insensitive: bool) -> Self {
let mut reserved_names = HashSet::new();
reserved_names.insert("admin".to_string());
reserved_names.insert("root".to_string());
reserved_names.insert("system".to_string());
reserved_names.insert("api".to_string());
reserved_names.insert("null".to_string());
reserved_names.insert("undefined".to_string());
Self {
case_insensitive,
reserved_names,
}
}
pub fn with_reserved_names(mut self, names: Vec<String>) -> Self {
for name in names {
self.reserved_names.insert(if self.case_insensitive {
name.to_lowercase()
} else {
name
});
}
self
}
fn validate_username(&self, username: &UserName) -> ValidationResult<()> {
let username_str = username.as_str();
let check_name = if self.case_insensitive {
username_str.to_lowercase()
} else {
username_str.to_string()
};
if self.reserved_names.contains(&check_name) {
return Err(ValidationError::ReservedUsername(username_str.to_string()));
}
if username_str.len() < 3 {
return Err(ValidationError::UsernameTooShort(username_str.to_string()));
}
if username_str.len() > 64 {
return Err(ValidationError::UsernameTooLong(username_str.to_string()));
}
if username_str.contains("..")
|| username_str.starts_with('.')
|| username_str.ends_with('.')
{
return Err(ValidationError::InvalidUsernameFormat(
username_str.to_string(),
));
}
Ok(())
}
}
impl CompositeValidator for UserNameUniquenessValidator {
fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
for obj in objects {
if obj.attribute_name() == "userName" {
if let Some(username) = obj.as_any().downcast_ref::<UserName>() {
self.validate_username(username)?;
}
}
}
Ok(())
}
fn dependent_attributes(&self) -> Vec<String> {
vec!["userName".to_string()]
}
fn applies_to(&self, attribute_names: &[String]) -> bool {
attribute_names.contains(&"userName".to_string())
}
}
#[derive(Debug)]
pub struct EmailConsistencyValidator {
allowed_domains: Option<Vec<String>>,
require_work_email: bool,
}
impl EmailConsistencyValidator {
pub fn new() -> Self {
Self {
allowed_domains: None,
require_work_email: false,
}
}
pub fn with_allowed_domains(mut self, domains: Vec<String>) -> Self {
self.allowed_domains = Some(domains);
self
}
pub fn with_work_email_requirement(mut self, required: bool) -> Self {
self.require_work_email = required;
self
}
fn validate_email_domain(&self, email: &EmailAddress) -> ValidationResult<()> {
if let Some(ref allowed_domains) = self.allowed_domains {
let email_str = email.value();
if let Some(domain) = email_str.split('@').nth(1) {
if !allowed_domains.iter().any(|d| domain.ends_with(d)) {
return Err(ValidationError::InvalidEmailDomain {
email: email_str.to_string(),
allowed_domains: allowed_domains.clone(),
});
}
}
}
Ok(())
}
fn has_work_email(&self, objects: &[Box<dyn ValueObject>]) -> bool {
for obj in objects {
if obj.attribute_name() == "emails" {
return true; }
}
false
}
}
impl CompositeValidator for EmailConsistencyValidator {
fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
for obj in objects {
if let Some(email) = obj.as_any().downcast_ref::<EmailAddress>() {
self.validate_email_domain(email)?;
}
}
if self.require_work_email && !self.has_work_email(objects) {
return Err(ValidationError::WorkEmailRequired);
}
Ok(())
}
fn dependent_attributes(&self) -> Vec<String> {
vec!["emails".to_string()]
}
fn applies_to(&self, attribute_names: &[String]) -> bool {
attribute_names.contains(&"emails".to_string())
}
}
#[derive(Debug)]
pub struct IdentityConsistencyValidator {
require_external_id: bool,
validate_id_format: bool,
}
impl IdentityConsistencyValidator {
pub fn new() -> Self {
Self {
require_external_id: false,
validate_id_format: true,
}
}
pub fn with_external_id_requirement(mut self, required: bool) -> Self {
self.require_external_id = required;
self
}
pub fn with_id_format_validation(mut self, enabled: bool) -> Self {
self.validate_id_format = enabled;
self
}
fn find_attribute<'a, T: 'static>(&self, objects: &'a [Box<dyn ValueObject>]) -> Option<&'a T> {
for obj in objects {
if let Some(typed_obj) = obj.as_any().downcast_ref::<T>() {
return Some(typed_obj);
}
}
None
}
fn validate_id_format_consistency(
&self,
objects: &[Box<dyn ValueObject>],
) -> ValidationResult<()> {
if !self.validate_id_format {
return Ok(());
}
if let Some(resource_id) = self.find_attribute::<ResourceId>(objects) {
let id_str = resource_id.as_str();
if id_str.contains('-') && id_str.len() == 36 {
if id_str.chars().filter(|&c| c == '-').count() != 4 {
return Err(ValidationError::InvalidIdFormat {
id: id_str.to_string(),
});
}
}
}
Ok(())
}
}
impl CompositeValidator for IdentityConsistencyValidator {
fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
if self.require_external_id {
let has_external_id = objects
.iter()
.any(|obj| obj.attribute_name() == "externalId");
if !has_external_id {
return Err(ValidationError::ExternalIdRequired);
}
}
self.validate_id_format_consistency(objects)?;
Ok(())
}
fn dependent_attributes(&self) -> Vec<String> {
vec![
"id".to_string(),
"userName".to_string(),
"externalId".to_string(),
]
}
fn applies_to(&self, attribute_names: &[String]) -> bool {
let dependent = self.dependent_attributes();
attribute_names.iter().any(|name| dependent.contains(name))
}
}
#[derive(Debug)]
pub struct NameConsistencyValidator {
validate_formatted_name: bool,
require_name_component: bool,
}
impl NameConsistencyValidator {
pub fn new() -> Self {
Self {
validate_formatted_name: true,
require_name_component: true,
}
}
pub fn with_formatted_name_validation(mut self, enabled: bool) -> Self {
self.validate_formatted_name = enabled;
self
}
pub fn with_name_component_requirement(mut self, required: bool) -> Self {
self.require_name_component = required;
self
}
fn validate_name_object(&self, name: &Name) -> ValidationResult<()> {
if self.require_name_component {
if name.given_name().is_none()
&& name.family_name().is_none()
&& name.formatted().is_none()
{
return Err(ValidationError::NameComponentRequired);
}
}
if self.validate_formatted_name {
if let Some(formatted) = name.formatted() {
if formatted.trim().is_empty() {
return Err(ValidationError::EmptyFormattedName);
}
}
}
Ok(())
}
}
impl CompositeValidator for NameConsistencyValidator {
fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
for obj in objects {
if obj.attribute_name() == "name" {
if let Some(name) = obj.as_any().downcast_ref::<Name>() {
self.validate_name_object(name)?;
}
}
}
Ok(())
}
fn dependent_attributes(&self) -> Vec<String> {
vec!["name".to_string()]
}
fn applies_to(&self, attribute_names: &[String]) -> bool {
attribute_names.contains(&"name".to_string())
}
}
pub struct CompositeValidatorChain {
validators: Vec<Box<dyn CompositeValidator>>,
}
impl CompositeValidatorChain {
pub fn new() -> Self {
Self {
validators: Vec::new(),
}
}
pub fn add_validator(mut self, validator: Box<dyn CompositeValidator>) -> Self {
self.validators.push(validator);
self
}
pub fn with_default_validators() -> Self {
Self::new()
.add_validator(Box::new(UniquePrimaryValidator::new()))
.add_validator(Box::new(UserNameUniquenessValidator::new(true)))
.add_validator(Box::new(EmailConsistencyValidator::new()))
.add_validator(Box::new(IdentityConsistencyValidator::new()))
.add_validator(Box::new(NameConsistencyValidator::new()))
}
}
impl CompositeValidator for CompositeValidatorChain {
fn validate_composite(&self, objects: &[Box<dyn ValueObject>]) -> ValidationResult<()> {
for validator in &self.validators {
validator.validate_composite(objects)?;
}
Ok(())
}
fn dependent_attributes(&self) -> Vec<String> {
let mut all_deps = Vec::new();
for validator in &self.validators {
all_deps.extend(validator.dependent_attributes());
}
all_deps.sort();
all_deps.dedup();
all_deps
}
fn applies_to(&self, attribute_names: &[String]) -> bool {
self.validators
.iter()
.any(|v| v.applies_to(attribute_names))
}
}
impl Default for CompositeValidatorChain {
fn default() -> Self {
Self::with_default_validators()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resource::value_objects::UserName;
fn create_test_objects() -> Vec<Box<dyn ValueObject>> {
vec![
Box::new(ResourceId::new("test-id".to_string()).unwrap()),
Box::new(UserName::new("testuser".to_string()).unwrap()),
Box::new(EmailAddress::new("test@example.com".to_string(), None, None, None).unwrap()),
]
}
#[test]
fn test_unique_primary_validator() {
let validator = UniquePrimaryValidator::new();
let objects = create_test_objects();
assert!(validator.validate_composite(&objects).is_ok());
assert!(validator.applies_to(&["emails".to_string()]));
assert!(!validator.applies_to(&["id".to_string()]));
}
#[test]
fn test_username_uniqueness_validator() {
let validator = UserNameUniquenessValidator::new(true);
let objects = create_test_objects();
assert!(validator.validate_composite(&objects).is_ok());
let reserved_objects =
vec![Box::new(UserName::new("admin".to_string()).unwrap()) as Box<dyn ValueObject>];
assert!(validator.validate_composite(&reserved_objects).is_err());
}
#[test]
fn test_email_consistency_validator() {
let validator =
EmailConsistencyValidator::new().with_allowed_domains(vec!["example.com".to_string()]);
let objects = create_test_objects();
assert!(validator.validate_composite(&objects).is_ok());
let invalid_objects = vec![Box::new(
EmailAddress::new("test@invalid.com".to_string(), None, None, None).unwrap(),
) as Box<dyn ValueObject>];
assert!(validator.validate_composite(&invalid_objects).is_err());
}
#[test]
fn test_identity_consistency_validator() {
let validator = IdentityConsistencyValidator::new().with_external_id_requirement(true);
let objects = create_test_objects();
assert!(validator.validate_composite(&objects).is_err());
}
#[test]
fn test_composite_validator_chain() {
let chain = CompositeValidatorChain::with_default_validators();
let objects = create_test_objects();
let _result = chain.validate_composite(&objects);
assert!(!chain.dependent_attributes().is_empty());
assert!(chain.applies_to(&["userName".to_string()]));
}
#[test]
fn test_username_length_validation() {
let validator = UserNameUniquenessValidator::new(false);
let short_objects =
vec![Box::new(UserName::new("ab".to_string()).unwrap()) as Box<dyn ValueObject>];
assert!(validator.validate_composite(&short_objects).is_err());
let valid_objects =
vec![Box::new(UserName::new("validuser".to_string()).unwrap()) as Box<dyn ValueObject>];
assert!(validator.validate_composite(&valid_objects).is_ok());
}
#[test]
fn test_name_consistency_validator() {
let validator = NameConsistencyValidator::new();
let objects = vec![];
assert!(validator.validate_composite(&objects).is_ok());
assert!(validator.applies_to(&["name".to_string()]));
}
}