use super::errors::{ValidationError, ValidationResult};
use std::future::Future;
use std::pin::Pin;
pub type UniquenessCheckFn =
Box<dyn Fn(String, Option<i32>) -> Pin<Box<dyn Future<Output = bool> + Send>> + Send + Sync>;
pub struct UniqueValidator {
field_name: String,
check_fn: UniquenessCheckFn,
}
impl UniqueValidator {
pub fn new(field_name: impl Into<String>, check_fn: UniquenessCheckFn) -> Self {
Self {
field_name: field_name.into(),
check_fn,
}
}
pub async fn validate_async(
&self,
value: impl Into<String>,
exclude_id: Option<i32>,
) -> ValidationResult<()> {
let value_str = value.into();
let exists = (self.check_fn)(value_str.clone(), exclude_id).await;
if exists {
Err(ValidationError::NotUnique {
field: self.field_name.clone(),
value: value_str,
})
} else {
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
#[tokio::test]
async fn test_unique_validator_new_value() {
let existing_values = Arc::new(Mutex::new(HashSet::from(["existinguser".to_string()])));
let validator = UniqueValidator::new(
"username",
Box::new(move |value, exclude_id| {
let existing = existing_values.clone();
Box::pin(async move {
let values = existing.lock().unwrap();
if let Some(_id) = exclude_id {
values.contains(&value)
} else {
values.contains(&value)
}
})
}),
);
let result = validator.validate_async("newuser", None).await;
assert!(result.is_ok());
let result = validator.validate_async("existinguser", None).await;
assert!(result.is_err());
if let Err(ValidationError::NotUnique { field, value }) = result {
assert_eq!(field, "username");
assert_eq!(value, "existinguser");
} else {
panic!("Expected NotUnique error");
}
}
#[tokio::test]
async fn test_unique_validator_excludes_instance() {
let users = Arc::new(Mutex::new(vec![
(1, "user1".to_string()),
(2, "user2".to_string()),
]));
let validator = UniqueValidator::new(
"username",
Box::new(move |value, exclude_id| {
let users_clone = users.clone();
Box::pin(async move {
let users = users_clone.lock().unwrap();
users
.iter()
.any(|(id, username)| username == &value && Some(*id) != exclude_id)
})
}),
);
let result = validator.validate_async("user1", Some(1)).await;
assert!(result.is_ok());
let result = validator.validate_async("user2", Some(1)).await;
assert!(result.is_err());
let result = validator.validate_async("user1", None).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_unique_validator_empty_database() {
let validator = UniqueValidator::new(
"email",
Box::new(|_value, _exclude_id| Box::pin(async move { false })),
);
let result = validator.validate_async("test@example.com", None).await;
assert!(result.is_ok());
}
}