1use async_trait::async_trait;
2use regex::Regex;
3use serde_json::Value;
4
5use crate::adapter::DataAdapter;
6
7pub trait Validator: Send + Sync {
9 fn validate(&self, value: &str) -> Result<(), String>;
10}
11
12#[async_trait]
15pub trait AsyncValidator: Send + Sync {
16 async fn validate(&self, value: &str, record_id: Option<&Value>) -> Result<(), String>;
17}
18
19pub struct Required;
23
24impl Validator for Required {
25 fn validate(&self, value: &str) -> Result<(), String> {
26 if value.trim().is_empty() {
27 Err("This field is required.".to_string())
28 } else {
29 Ok(())
30 }
31 }
32}
33
34pub struct MinLength(pub usize);
36
37impl Validator for MinLength {
38 fn validate(&self, value: &str) -> Result<(), String> {
39 if value.len() < self.0 {
40 Err(format!("Must be at least {} characters.", self.0))
41 } else {
42 Ok(())
43 }
44 }
45}
46
47pub struct MaxLength(pub usize);
49
50impl Validator for MaxLength {
51 fn validate(&self, value: &str) -> Result<(), String> {
52 if value.len() > self.0 {
53 Err(format!("Must be at most {} characters.", self.0))
54 } else {
55 Ok(())
56 }
57 }
58}
59
60pub struct MinValue(pub f64);
62
63impl Validator for MinValue {
64 fn validate(&self, value: &str) -> Result<(), String> {
65 if value.trim().is_empty() {
66 return Ok(()); }
68 match value.parse::<f64>() {
69 Ok(n) if n >= self.0 => Ok(()),
70 Ok(_) => Err(format!("Must be at least {}.", self.0)),
71 Err(_) => Err("Must be a valid number.".to_string()),
72 }
73 }
74}
75
76pub struct MaxValue(pub f64);
78
79impl Validator for MaxValue {
80 fn validate(&self, value: &str) -> Result<(), String> {
81 if value.trim().is_empty() {
82 return Ok(()); }
84 match value.parse::<f64>() {
85 Ok(n) if n <= self.0 => Ok(()),
86 Ok(_) => Err(format!("Must be at most {}.", self.0)),
87 Err(_) => Err("Must be a valid number.".to_string()),
88 }
89 }
90}
91
92pub struct RegexValidator {
94 pattern: String,
95 regex: Regex,
96}
97
98impl RegexValidator {
99 pub fn new(pattern: &str) -> Self {
101 Self {
102 regex: Regex::new(pattern).expect("invalid regex pattern"),
103 pattern: pattern.to_string(),
104 }
105 }
106}
107
108impl Validator for RegexValidator {
109 fn validate(&self, value: &str) -> Result<(), String> {
110 if value.trim().is_empty() {
111 return Ok(()); }
113 if self.regex.is_match(value) {
114 Ok(())
115 } else {
116 Err(format!("Must match pattern: {}", self.pattern))
117 }
118 }
119}
120
121pub struct EmailFormat;
123
124impl Validator for EmailFormat {
125 fn validate(&self, value: &str) -> Result<(), String> {
126 if value.trim().is_empty() {
127 return Ok(()); }
129 let parts: Vec<&str> = value.splitn(2, '@').collect();
131 if parts.len() != 2 || parts[0].is_empty() || !parts[1].contains('.') || parts[1].starts_with('.') || parts[1].ends_with('.') {
132 Err("Enter a valid email address.".to_string())
133 } else {
134 Ok(())
135 }
136 }
137}
138
139pub struct Unique {
144 adapter: Box<dyn DataAdapter>,
145 col: String,
146}
147
148impl Unique {
149 pub fn new(adapter: Box<dyn DataAdapter>, col: &str) -> Self {
150 Self {
151 adapter,
152 col: col.to_string(),
153 }
154 }
155}
156
157#[async_trait]
158impl AsyncValidator for Unique {
159 async fn validate(&self, value: &str, record_id: Option<&Value>) -> Result<(), String> {
160 if value.trim().is_empty() {
161 return Ok(()); }
163 use crate::adapter::ListParams;
164 use std::collections::HashMap;
165
166 let mut filters = HashMap::new();
167 filters.insert(self.col.clone(), Value::String(value.to_string()));
168
169 let params = ListParams {
170 page: 1,
171 per_page: 10,
172 filters,
173 ..Default::default()
174 };
175
176 let rows = self.adapter.list(params).await.unwrap_or_default();
177
178 let conflicts: Vec<_> = rows.iter().filter(|row| {
180 if let Some(rid) = record_id {
181 let row_id = row.get("id").map(|v| match v {
182 Value::String(s) => s.clone(),
183 Value::Number(n) => n.to_string(),
184 other => other.to_string(),
185 }).unwrap_or_default();
186 let current_id = match rid {
187 Value::String(s) => s.clone(),
188 Value::Number(n) => n.to_string(),
189 other => other.to_string(),
190 };
191 row_id != current_id
192 } else {
193 true
194 }
195 }).collect();
196
197 if conflicts.is_empty() {
198 Ok(())
199 } else {
200 Err(format!("This {} is already taken.", self.col))
201 }
202 }
203}