elif_validation/validators/
email.rs1use crate::error::{ValidationError, ValidationResult};
4use crate::traits::ValidationRule;
5use async_trait::async_trait;
6use regex::Regex;
7use serde_json::Value;
8
9#[derive(Debug, Clone)]
11pub struct EmailValidator {
12 pub message: Option<String>,
14 pub allow_unicode: bool,
16 pub require_tld: bool,
18 pub custom_pattern: Option<Regex>,
20}
21
22impl EmailValidator {
23 pub fn new() -> Self {
25 Self {
26 message: None,
27 allow_unicode: false,
28 require_tld: true,
29 custom_pattern: None,
30 }
31 }
32
33 pub fn message(mut self, message: impl Into<String>) -> Self {
35 self.message = Some(message.into());
36 self
37 }
38
39 pub fn allow_unicode(mut self, allow: bool) -> Self {
41 self.allow_unicode = allow;
42 self
43 }
44
45 pub fn require_tld(mut self, require: bool) -> Self {
47 self.require_tld = require;
48 self
49 }
50
51 pub fn custom_pattern(mut self, pattern: Regex) -> Self {
53 self.custom_pattern = Some(pattern);
54 self
55 }
56
57 fn get_pattern(&self) -> Result<Regex, regex::Error> {
59 if let Some(ref pattern) = self.custom_pattern {
60 return Ok(pattern.clone());
61 }
62
63 let pattern = if self.allow_unicode {
67 if self.require_tld {
68 r"^[^\s@.]+[^\s@]*@[^\s@.]+[^\s@]*\.[^\s@]+$"
70 } else {
71 r"^[^\s@.]+[^\s@]*@[^\s@.]+[^\s@]*$"
73 }
74 } else if self.require_tld {
75 r"^[a-zA-Z0-9]([a-zA-Z0-9._%+-]*[a-zA-Z0-9])?@[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$"
77 } else {
78 r"^[a-zA-Z0-9]([a-zA-Z0-9._%+-]*[a-zA-Z0-9])?@[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$"
80 };
81
82 Regex::new(pattern)
83 }
84
85 fn validate_email_format(&self, email: &str) -> bool {
87 if email.is_empty() {
89 return false;
90 }
91
92 let at_count = email.matches('@').count();
94 if at_count != 1 {
95 return false;
96 }
97
98 let parts: Vec<&str> = email.split('@').collect();
100 if parts.len() != 2 {
101 return false;
102 }
103
104 let local_part = parts[0];
105 let domain_part = parts[1];
106
107 if local_part.is_empty() {
109 return false;
110 }
111
112 if domain_part.is_empty() {
114 return false;
115 }
116
117 if local_part.len() > 64 {
119 return false;
120 }
121
122 if domain_part.len() > 255 {
124 return false;
125 }
126
127 match self.get_pattern() {
129 Ok(regex) => regex.is_match(email),
130 Err(_) => false, }
132 }
133}
134
135impl Default for EmailValidator {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141#[async_trait]
142impl ValidationRule for EmailValidator {
143 async fn validate(&self, value: &Value, field: &str) -> ValidationResult<()> {
144 if value.is_null() {
146 return Ok(());
147 }
148
149 let email = match value.as_str() {
150 Some(email) => email,
151 None => {
152 return Err(ValidationError::with_code(
153 field,
154 format!("{} must be a string for email validation", field),
155 "invalid_type",
156 ).into());
157 }
158 };
159
160 if !self.validate_email_format(email) {
161 let message = self
162 .message.clone()
163 .unwrap_or_else(|| format!("{} must be a valid email address", field));
164
165 return Err(ValidationError::with_code(field, message, "invalid_email").into());
166 }
167
168 Ok(())
169 }
170
171 fn rule_name(&self) -> &'static str {
172 "email"
173 }
174
175 fn parameters(&self) -> Option<Value> {
176 let mut params = serde_json::Map::new();
177
178 if let Some(ref message) = self.message {
179 params.insert("message".to_string(), Value::String(message.clone()));
180 }
181 params.insert("allow_unicode".to_string(), Value::Bool(self.allow_unicode));
182 params.insert("require_tld".to_string(), Value::Bool(self.require_tld));
183
184 if !params.is_empty() {
185 Some(Value::Object(params))
186 } else {
187 None
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[tokio::test]
197 async fn test_email_validator_valid_emails() {
198 let validator = EmailValidator::new();
199
200 let valid_emails = vec![
201 "test@example.com",
202 "user.name@domain.co.uk",
203 "first+last@subdomain.example.org",
204 "user123@test-domain.com",
205 "a@b.co",
206 ];
207
208 for email in valid_emails {
209 let result = validator.validate(&Value::String(email.to_string()), "email").await;
210 assert!(result.is_ok(), "Email '{}' should be valid", email);
211 }
212 }
213
214 #[tokio::test]
215 async fn test_email_validator_invalid_emails() {
216 let validator = EmailValidator::new();
217
218 let toolong_email = format!("toolong{}@domain.com", "a".repeat(60));
219 let invalid_emails = vec![
220 "", "plainaddress", "@missingdomain.com", "missing@.com", "double@@domain.com", "spaces @domain.com", &toolong_email, "test@", "test@domain", ];
230
231 for email in invalid_emails {
232 let result = validator.validate(&Value::String(email.to_string()), "email").await;
233 assert!(result.is_err(), "Email '{}' should be invalid", email);
234 }
235 }
236
237 #[tokio::test]
238 async fn test_email_validator_without_tld_requirement() {
239 let validator = EmailValidator::new().require_tld(false);
240
241 let result = validator.validate(&Value::String("test@localhost".to_string()), "email").await;
243 assert!(result.is_ok());
244
245 let result = validator.validate(&Value::String("admin@intranet".to_string()), "email").await;
246 assert!(result.is_ok());
247 }
248
249 #[tokio::test]
250 async fn test_email_validator_unicode_domain() {
251 let validator = EmailValidator::new().allow_unicode(true);
252
253 let result = validator.validate(&Value::String("test@тест.рф".to_string()), "email").await;
255 assert!(result.is_ok());
256 }
257
258 #[tokio::test]
259 async fn test_email_validator_custom_pattern() {
260 let custom_regex = Regex::new(r"^[a-z]+@company\.com$").unwrap();
261 let validator = EmailValidator::new().custom_pattern(custom_regex);
262
263 let result = validator.validate(&Value::String("john@company.com".to_string()), "email").await;
265 assert!(result.is_ok());
266
267 let result = validator.validate(&Value::String("john@otherdomain.com".to_string()), "email").await;
269 assert!(result.is_err());
270
271 let result = validator.validate(&Value::String("John@company.com".to_string()), "email").await;
272 assert!(result.is_err()); }
274
275 #[tokio::test]
276 async fn test_email_validator_custom_message() {
277 let validator = EmailValidator::new().message("Please enter a valid email address");
278
279 let result = validator.validate(&Value::String("invalid-email".to_string()), "email").await;
280 assert!(result.is_err());
281
282 let errors = result.unwrap_err();
283 let field_errors = errors.get_field_errors("email").unwrap();
284 assert_eq!(field_errors[0].message, "Please enter a valid email address");
285 }
286
287 #[tokio::test]
288 async fn test_email_validator_with_null() {
289 let validator = EmailValidator::new();
290
291 let result = validator.validate(&Value::Null, "email").await;
293 assert!(result.is_ok());
294 }
295
296 #[tokio::test]
297 async fn test_email_validator_invalid_type() {
298 let validator = EmailValidator::new();
299
300 let result = validator.validate(&Value::Number(serde_json::Number::from(42)), "email").await;
302 assert!(result.is_err());
303
304 let errors = result.unwrap_err();
305 let field_errors = errors.get_field_errors("email").unwrap();
306 assert_eq!(field_errors[0].code, "invalid_type");
307 }
308
309 #[tokio::test]
310 async fn test_email_validator_edge_cases() {
311 let validator = EmailValidator::new();
312
313 let edge_cases = vec![
315 ("aa@bb.cc", true), ("test@test@test.com", false), ("test@domain.com", true), ];
319
320 for (email, should_be_valid) in edge_cases {
321 let result = validator.validate(&Value::String(email.to_string()), "email").await;
322 if should_be_valid {
323 assert!(result.is_ok(), "Email '{}' should be valid", email);
324 } else {
325 assert!(result.is_err(), "Email '{}' should be invalid", email);
326 }
327 }
328 }
329}