Skip to main content

sz_orm_core/validation/
rules.rs

1//! # 校验规则函数(8 种)
2//!
3//! 每个函数返回 `Result<(), ValidationError>`,可组合使用 `aggregate` 聚合。
4
5use super::ValidationError;
6use regex::Regex;
7use std::sync::OnceLock;
8
9/// 邮箱正则缓存(RFC 5322 简化版)
10static EMAIL_REGEX: OnceLock<Regex> = OnceLock::new();
11
12fn email_regex() -> &'static Regex {
13    EMAIL_REGEX
14        .get_or_init(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap())
15}
16
17/// 验证邮箱格式
18pub fn validate_email(field: &str, value: &str) -> Result<(), ValidationError> {
19    if email_regex().is_match(value) {
20        Ok(())
21    } else {
22        Err(ValidationError::Email {
23            field: field.to_string(),
24            value: value.to_string(),
25        })
26    }
27}
28
29/// 验证字符串长度在 [min, max] 范围内(Unicode 安全,按字符计数)
30pub fn validate_length(
31    field: &str,
32    value: &str,
33    min: usize,
34    max: usize,
35) -> Result<(), ValidationError> {
36    let actual = value.chars().count();
37    if actual >= min && actual <= max {
38        Ok(())
39    } else {
40        Err(ValidationError::Length {
41            field: field.to_string(),
42            min,
43            max,
44            actual,
45        })
46    }
47}
48
49/// 验证数值在 [min, max] 范围内
50pub fn validate_range<T: PartialOrd + std::fmt::Display>(
51    field: &str,
52    value: T,
53    min: T,
54    max: T,
55) -> Result<(), ValidationError> {
56    if value >= min && value <= max {
57        Ok(())
58    } else {
59        Err(ValidationError::Range {
60            field: field.to_string(),
61            min: min.to_string(),
62            max: max.to_string(),
63            actual: value.to_string(),
64        })
65    }
66}
67
68/// 验证正则匹配
69pub fn validate_regex(field: &str, value: &str, pattern: &str) -> Result<(), ValidationError> {
70    match Regex::new(pattern) {
71        Ok(re) => {
72            if re.is_match(value) {
73                Ok(())
74            } else {
75                Err(ValidationError::Regex {
76                    field: field.to_string(),
77                    pattern: pattern.to_string(),
78                    value: value.to_string(),
79                })
80            }
81        }
82        Err(_) => Err(ValidationError::Regex {
83            field: field.to_string(),
84            pattern: pattern.to_string(),
85            value: value.to_string(),
86        }),
87    }
88}
89
90/// 验证非空
91pub fn validate_required(field: &str, value: &str) -> Result<(), ValidationError> {
92    if value.is_empty() {
93        Err(ValidationError::Required {
94            field: field.to_string(),
95        })
96    } else {
97        Ok(())
98    }
99}
100
101/// 验证包含子串
102pub fn validate_contains(field: &str, value: &str, substring: &str) -> Result<(), ValidationError> {
103    if value.contains(substring) {
104        Ok(())
105    } else {
106        Err(ValidationError::Contains {
107            field: field.to_string(),
108            substring: substring.to_string(),
109        })
110    }
111}
112
113/// 验证不包含子串
114pub fn validate_does_not_contain(
115    field: &str,
116    value: &str,
117    substring: &str,
118) -> Result<(), ValidationError> {
119    if !value.contains(substring) {
120        Ok(())
121    } else {
122        Err(ValidationError::DoesNotContain {
123            field: field.to_string(),
124            substring: substring.to_string(),
125        })
126    }
127}
128
129/// 自定义校验失败
130pub fn validate_custom(field: &str, reason: &str) -> Result<(), ValidationError> {
131    Err(ValidationError::Custom {
132        field: field.to_string(),
133        reason: reason.to_string(),
134    })
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_validate_email_valid() {
143        assert!(validate_email("email", "user@example.com").is_ok());
144        assert!(validate_email("email", "a.b+c@d.co").is_ok());
145    }
146
147    #[test]
148    fn test_validate_email_invalid() {
149        assert!(validate_email("email", "noatsign").is_err());
150        assert!(validate_email("email", "").is_err());
151        assert!(validate_email("email", "a@").is_err());
152        assert!(validate_email("email", "@b.com").is_err());
153    }
154
155    #[test]
156    fn test_validate_length_boundary() {
157        assert!(validate_length("name", "abc", 3, 5).is_ok());
158        assert!(validate_length("name", "abcde", 3, 5).is_ok());
159    }
160
161    #[test]
162    fn test_validate_length_too_long() {
163        assert!(validate_length("name", "abcdef", 3, 5).is_err());
164    }
165
166    #[test]
167    fn test_validate_length_empty() {
168        assert!(validate_length("name", "", 1, 5).is_err());
169    }
170
171    #[test]
172    fn test_validate_length_unicode() {
173        assert!(validate_length("name", "你好", 2, 2).is_ok());
174    }
175
176    #[test]
177    fn test_validate_range_boundary() {
178        assert!(validate_range("age", 18i64, 18, 65).is_ok());
179        assert!(validate_range("age", 65i64, 18, 65).is_ok());
180    }
181
182    #[test]
183    fn test_validate_range_out_of_range() {
184        assert!(validate_range("age", 17i64, 18, 65).is_err());
185        assert!(validate_range("age", 66i64, 18, 65).is_err());
186    }
187
188    #[test]
189    fn test_validate_range_negative() {
190        assert!(validate_range("temp", -10.5f64, -20.0, 50.0).is_ok());
191        assert!(validate_range("temp", -30.0f64, -20.0, 50.0).is_err());
192    }
193
194    #[test]
195    fn test_validate_regex_match() {
196        assert!(validate_regex("code", "ABC123", r"^[A-Z]{3}\d{3}$").is_ok());
197    }
198
199    #[test]
200    fn test_validate_regex_no_match() {
201        assert!(validate_regex("code", "abc123", r"^[A-Z]{3}\d{3}$").is_err());
202    }
203
204    #[test]
205    fn test_validate_regex_invalid_pattern() {
206        assert!(validate_regex("code", "abc", r"[invalid").is_err());
207    }
208
209    #[test]
210    fn test_validate_required_non_empty() {
211        assert!(validate_required("name", "value").is_ok());
212    }
213
214    #[test]
215    fn test_validate_required_empty() {
216        assert!(validate_required("name", "").is_err());
217    }
218
219    #[test]
220    fn test_validate_contains_found() {
221        assert!(validate_contains("url", "https://example.com", "https://").is_ok());
222    }
223
224    #[test]
225    fn test_validate_contains_not_found() {
226        assert!(validate_contains("url", "ftp://example.com", "https://").is_err());
227    }
228
229    #[test]
230    fn test_validate_contains_empty_substring() {
231        assert!(validate_contains("name", "anything", "").is_ok());
232    }
233
234    #[test]
235    fn test_validate_does_not_contain_clean() {
236        assert!(validate_does_not_contain("name", "hello", "sql").is_ok());
237    }
238
239    #[test]
240    fn test_validate_does_not_contain_dirty() {
241        assert!(validate_does_not_contain("name", "drop table", "drop").is_err());
242    }
243
244    #[test]
245    fn test_validate_does_not_contain_empty_substring() {
246        assert!(validate_does_not_contain("name", "anything", "").is_err());
247    }
248
249    #[test]
250    fn test_aggregate_empty() {
251        let results = vec![];
252        assert!(super::super::aggregate(results).is_ok());
253    }
254
255    #[test]
256    fn test_aggregate_all_ok() {
257        let results = vec![Ok(()), Ok(()), Ok(())];
258        assert!(super::super::aggregate(results).is_ok());
259    }
260
261    #[test]
262    fn test_aggregate_single_error() {
263        let results = vec![Ok(()), Err(ValidationError::Required { field: "x".into() })];
264        let result = super::super::aggregate(results);
265        assert!(matches!(result, Err(ValidationError::Required { .. })));
266    }
267
268    #[test]
269    fn test_aggregate_multiple_errors() {
270        let results = vec![
271            Err(ValidationError::Required { field: "a".into() }),
272            Ok(()),
273            Err(ValidationError::Required { field: "b".into() }),
274        ];
275        let result = super::super::aggregate(results);
276        match result {
277            Err(ValidationError::Aggregate { errors, count }) => {
278                assert_eq!(errors.len(), 2);
279                assert_eq!(count, 2);
280            }
281            _ => panic!("expected Aggregate with 2 errors"),
282        }
283    }
284}