use crate::v2::context::ValidationContext;
use crate::v2::error::RuleError;
use crate::v2::traits::AsyncValidationRule;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AsyncUniqueRule {
pub table: String,
pub column: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl AsyncUniqueRule {
pub fn new(table: impl Into<String>, column: impl Into<String>) -> Self {
Self {
table: table.into(),
column: column.into(),
message: None,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
}
#[async_trait]
impl AsyncValidationRule<str> for AsyncUniqueRule {
async fn validate_async(&self, value: &str, ctx: &ValidationContext) -> Result<(), RuleError> {
let db = ctx.database().ok_or_else(|| {
RuleError::new(
"async_unique",
"Database validator not configured in context",
)
})?;
let is_unique = if let Some(exclude_id) = ctx.exclude_id() {
db.is_unique_except(&self.table, &self.column, value, exclude_id)
.await
.map_err(|e| RuleError::new("async_unique", format!("Database error: {}", e)))?
} else {
db.is_unique(&self.table, &self.column, value)
.await
.map_err(|e| RuleError::new("async_unique", format!("Database error: {}", e)))?
};
if is_unique {
Ok(())
} else {
let message = self
.message
.clone()
.unwrap_or_else(|| "validation.unique.taken".to_string());
Err(RuleError::new("async_unique", message)
.param("table", self.table.clone())
.param("column", self.column.clone()))
}
}
fn rule_name(&self) -> &'static str {
"async_unique"
}
}
#[async_trait]
impl AsyncValidationRule<String> for AsyncUniqueRule {
async fn validate_async(
&self,
value: &String,
ctx: &ValidationContext,
) -> Result<(), RuleError> {
<Self as AsyncValidationRule<str>>::validate_async(self, value.as_str(), ctx).await
}
fn rule_name(&self) -> &'static str {
"async_unique"
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AsyncExistsRule {
pub table: String,
pub column: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl AsyncExistsRule {
pub fn new(table: impl Into<String>, column: impl Into<String>) -> Self {
Self {
table: table.into(),
column: column.into(),
message: None,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
}
#[async_trait]
impl AsyncValidationRule<str> for AsyncExistsRule {
async fn validate_async(&self, value: &str, ctx: &ValidationContext) -> Result<(), RuleError> {
let db = ctx.database().ok_or_else(|| {
RuleError::new(
"async_exists",
"Database validator not configured in context",
)
})?;
let exists = db
.exists(&self.table, &self.column, value)
.await
.map_err(|e| RuleError::new("async_exists", format!("Database error: {}", e)))?;
if exists {
Ok(())
} else {
let message = self
.message
.clone()
.unwrap_or_else(|| "validation.exists.not_found".to_string());
Err(RuleError::new("async_exists", message)
.param("table", self.table.clone())
.param("column", self.column.clone()))
}
}
fn rule_name(&self) -> &'static str {
"async_exists"
}
}
#[async_trait]
impl AsyncValidationRule<String> for AsyncExistsRule {
async fn validate_async(
&self,
value: &String,
ctx: &ValidationContext,
) -> Result<(), RuleError> {
<Self as AsyncValidationRule<str>>::validate_async(self, value.as_str(), ctx).await
}
fn rule_name(&self) -> &'static str {
"async_exists"
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AsyncApiRule {
pub endpoint: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl AsyncApiRule {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
message: None,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
}
#[async_trait]
impl AsyncValidationRule<str> for AsyncApiRule {
async fn validate_async(&self, value: &str, ctx: &ValidationContext) -> Result<(), RuleError> {
let http = ctx.http().ok_or_else(|| {
RuleError::new("async_api", "HTTP validator not configured in context")
})?;
let is_valid = http
.validate(&self.endpoint, value)
.await
.map_err(|e| RuleError::new("async_api", format!("API error: {}", e)))?;
if is_valid {
Ok(())
} else {
let message = self
.message
.clone()
.unwrap_or_else(|| "validation.api.invalid".to_string());
Err(RuleError::new("async_api", message).param("endpoint", self.endpoint.clone()))
}
}
fn rule_name(&self) -> &'static str {
"async_api"
}
}
#[async_trait]
impl AsyncValidationRule<String> for AsyncApiRule {
async fn validate_async(
&self,
value: &String,
ctx: &ValidationContext,
) -> Result<(), RuleError> {
<Self as AsyncValidationRule<str>>::validate_async(self, value.as_str(), ctx).await
}
fn rule_name(&self) -> &'static str {
"async_api"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::v2::context::{DatabaseValidator, ValidationContextBuilder};
struct MockDbValidator {
unique_values: Vec<String>,
existing_values: Vec<String>,
}
#[async_trait]
impl DatabaseValidator for MockDbValidator {
async fn exists(&self, _table: &str, _column: &str, value: &str) -> Result<bool, String> {
Ok(self.existing_values.contains(&value.to_string()))
}
async fn is_unique(
&self,
_table: &str,
_column: &str,
value: &str,
) -> Result<bool, String> {
Ok(!self.unique_values.contains(&value.to_string()))
}
async fn is_unique_except(
&self,
_table: &str,
_column: &str,
value: &str,
_except_id: &str,
) -> Result<bool, String> {
Ok(!self.unique_values.contains(&value.to_string()))
}
}
#[tokio::test]
async fn async_unique_rule_valid() {
let db = MockDbValidator {
unique_values: vec!["taken@example.com".to_string()],
existing_values: vec![],
};
let ctx = ValidationContextBuilder::new().database(db).build();
let rule = AsyncUniqueRule::new("users", "email");
assert!(rule.validate_async("new@example.com", &ctx).await.is_ok());
}
#[tokio::test]
async fn async_unique_rule_invalid() {
let db = MockDbValidator {
unique_values: vec!["taken@example.com".to_string()],
existing_values: vec![],
};
let ctx = ValidationContextBuilder::new().database(db).build();
let rule = AsyncUniqueRule::new("users", "email");
let err = rule
.validate_async("taken@example.com", &ctx)
.await
.unwrap_err();
assert_eq!(err.code, "async_unique");
}
#[tokio::test]
async fn async_exists_rule_valid() {
let db = MockDbValidator {
unique_values: vec![],
existing_values: vec!["existing_id".to_string()],
};
let ctx = ValidationContextBuilder::new().database(db).build();
let rule = AsyncExistsRule::new("users", "id");
assert!(rule.validate_async("existing_id", &ctx).await.is_ok());
}
#[tokio::test]
async fn async_exists_rule_invalid() {
let db = MockDbValidator {
unique_values: vec![],
existing_values: vec!["existing_id".to_string()],
};
let ctx = ValidationContextBuilder::new().database(db).build();
let rule = AsyncExistsRule::new("users", "id");
let err = rule
.validate_async("nonexistent_id", &ctx)
.await
.unwrap_err();
assert_eq!(err.code, "async_exists");
}
#[tokio::test]
async fn async_rule_without_context() {
let ctx = ValidationContext::new();
let rule = AsyncUniqueRule::new("users", "email");
let err = rule
.validate_async("test@example.com", &ctx)
.await
.unwrap_err();
assert!(err.message.contains("not configured"));
}
#[test]
fn async_rule_serialization() {
let rule = AsyncUniqueRule::new("users", "email").with_message("Email already taken");
let json = serde_json::to_string(&rule).unwrap();
let parsed: AsyncUniqueRule = serde_json::from_str(&json).unwrap();
assert_eq!(rule, parsed);
}
}