Skip to main content

axum_admin/
validator.rs

1use async_trait::async_trait;
2use regex::Regex;
3use serde_json::Value;
4
5use crate::adapter::DataAdapter;
6
7/// Synchronous field validator. Implement this for custom validation logic.
8pub trait Validator: Send + Sync {
9    fn validate(&self, value: &str) -> Result<(), String>;
10}
11
12/// Asynchronous field validator. Used for validators that need DB access (e.g. Unique).
13/// `record_id` is the current record's PK — pass `Some` on edit, `None` on create.
14#[async_trait]
15pub trait AsyncValidator: Send + Sync {
16    async fn validate(&self, value: &str, record_id: Option<&Value>) -> Result<(), String>;
17}
18
19// --- Built-in sync validators ---
20
21/// Fails if the value is an empty string. Auto-wired when `.required()` is called on a Field.
22pub 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
34/// Fails if the value's character count is less than `n`.
35pub 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
47/// Fails if the value's character count is greater than `n`.
48pub 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
60/// Fails if the value cannot be parsed as f64 or is less than `n`.
61pub 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(());  // let Required handle empty
67        }
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
76/// Fails if the value cannot be parsed as f64 or is greater than `n`.
77pub 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(());  // let Required handle empty
83        }
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
92/// Fails if the value does not match `pattern`. The pattern is compiled once at construction.
93pub struct RegexValidator {
94    pattern: String,
95    regex: Regex,
96}
97
98impl RegexValidator {
99    /// Panics if `pattern` is not a valid regex. Use `.regex()` on `Field` which calls this.
100    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(());  // let Required handle empty
112        }
113        if self.regex.is_match(value) {
114            Ok(())
115        } else {
116            Err(format!("Must match pattern: {}", self.pattern))
117        }
118    }
119}
120
121/// Basic email format validator. Auto-wired for `Field::email()`.
122pub struct EmailFormat;
123
124impl Validator for EmailFormat {
125    fn validate(&self, value: &str) -> Result<(), String> {
126        if value.trim().is_empty() {
127            return Ok(());  // let Required handle empty
128        }
129        // Check for exactly one '@', non-empty local part, non-empty domain with a '.'
130        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
139// --- Built-in async validators ---
140
141/// Fails if another row in the DB already has `col = value`.
142/// On edit, excludes the current record (matched by PK `id`).
143pub 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(());  // let Required handle empty
162        }
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        // Filter out the current record (on edit)
179        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}