use async_trait::async_trait;
use regex::Regex;
use serde_json::Value;
use crate::adapter::DataAdapter;
pub trait Validator: Send + Sync {
fn validate(&self, value: &str) -> Result<(), String>;
}
#[async_trait]
pub trait AsyncValidator: Send + Sync {
async fn validate(&self, value: &str, record_id: Option<&Value>) -> Result<(), String>;
}
pub struct Required;
impl Validator for Required {
fn validate(&self, value: &str) -> Result<(), String> {
if value.trim().is_empty() {
Err("This field is required.".to_string())
} else {
Ok(())
}
}
}
pub struct MinLength(pub usize);
impl Validator for MinLength {
fn validate(&self, value: &str) -> Result<(), String> {
if value.len() < self.0 {
Err(format!("Must be at least {} characters.", self.0))
} else {
Ok(())
}
}
}
pub struct MaxLength(pub usize);
impl Validator for MaxLength {
fn validate(&self, value: &str) -> Result<(), String> {
if value.len() > self.0 {
Err(format!("Must be at most {} characters.", self.0))
} else {
Ok(())
}
}
}
pub struct MinValue(pub f64);
impl Validator for MinValue {
fn validate(&self, value: &str) -> Result<(), String> {
if value.trim().is_empty() {
return Ok(()); }
match value.parse::<f64>() {
Ok(n) if n >= self.0 => Ok(()),
Ok(_) => Err(format!("Must be at least {}.", self.0)),
Err(_) => Err("Must be a valid number.".to_string()),
}
}
}
pub struct MaxValue(pub f64);
impl Validator for MaxValue {
fn validate(&self, value: &str) -> Result<(), String> {
if value.trim().is_empty() {
return Ok(()); }
match value.parse::<f64>() {
Ok(n) if n <= self.0 => Ok(()),
Ok(_) => Err(format!("Must be at most {}.", self.0)),
Err(_) => Err("Must be a valid number.".to_string()),
}
}
}
pub struct RegexValidator {
pattern: String,
regex: Regex,
}
impl RegexValidator {
pub fn new(pattern: &str) -> Self {
Self {
regex: Regex::new(pattern).expect("invalid regex pattern"),
pattern: pattern.to_string(),
}
}
}
impl Validator for RegexValidator {
fn validate(&self, value: &str) -> Result<(), String> {
if value.trim().is_empty() {
return Ok(()); }
if self.regex.is_match(value) {
Ok(())
} else {
Err(format!("Must match pattern: {}", self.pattern))
}
}
}
pub struct EmailFormat;
impl Validator for EmailFormat {
fn validate(&self, value: &str) -> Result<(), String> {
if value.trim().is_empty() {
return Ok(()); }
let parts: Vec<&str> = value.splitn(2, '@').collect();
if parts.len() != 2 || parts[0].is_empty() || !parts[1].contains('.') || parts[1].starts_with('.') || parts[1].ends_with('.') {
Err("Enter a valid email address.".to_string())
} else {
Ok(())
}
}
}
pub struct Unique {
adapter: Box<dyn DataAdapter>,
col: String,
}
impl Unique {
pub fn new(adapter: Box<dyn DataAdapter>, col: &str) -> Self {
Self {
adapter,
col: col.to_string(),
}
}
}
#[async_trait]
impl AsyncValidator for Unique {
async fn validate(&self, value: &str, record_id: Option<&Value>) -> Result<(), String> {
if value.trim().is_empty() {
return Ok(()); }
use crate::adapter::ListParams;
use std::collections::HashMap;
let mut filters = HashMap::new();
filters.insert(self.col.clone(), Value::String(value.to_string()));
let params = ListParams {
page: 1,
per_page: 10,
filters,
..Default::default()
};
let rows = self.adapter.list(params).await.unwrap_or_default();
let conflicts: Vec<_> = rows.iter().filter(|row| {
if let Some(rid) = record_id {
let row_id = row.get("id").map(|v| match v {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
other => other.to_string(),
}).unwrap_or_default();
let current_id = match rid {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
other => other.to_string(),
};
row_id != current_id
} else {
true
}
}).collect();
if conflicts.is_empty() {
Ok(())
} else {
Err(format!("This {} is already taken.", self.col))
}
}
}