rosace_forms/
validator.rs1pub trait Validator: Send + Sync + 'static {
3 fn validate(&self, value: &str) -> Option<String>;
5
6 fn name(&self) -> &'static str;
8}
9
10pub struct Required;
12impl Validator for Required {
13 fn validate(&self, value: &str) -> Option<String> {
14 if value.trim().is_empty() { Some("This field is required.".into()) } else { None }
15 }
16 fn name(&self) -> &'static str { "Required" }
17}
18
19pub struct MinLength(pub usize);
21impl Validator for MinLength {
22 fn validate(&self, value: &str) -> Option<String> {
23 if value.len() < self.0 { Some(format!("Must be at least {} characters.", self.0)) } else { None }
24 }
25 fn name(&self) -> &'static str { "MinLength" }
26}
27
28pub struct MaxLength(pub usize);
30impl Validator for MaxLength {
31 fn validate(&self, value: &str) -> Option<String> {
32 if value.len() > self.0 { Some(format!("Must be no more than {} characters.", self.0)) } else { None }
33 }
34 fn name(&self) -> &'static str { "MaxLength" }
35}
36
37pub struct Contains(pub &'static str);
39impl Validator for Contains {
40 fn validate(&self, value: &str) -> Option<String> {
41 if !value.contains(self.0) { Some(format!("Must contain '{}'.", self.0)) } else { None }
42 }
43 fn name(&self) -> &'static str { "Contains" }
44}
45
46pub struct Email;
48impl Validator for Email {
49 fn validate(&self, value: &str) -> Option<String> {
50 let valid = value.contains('@') && value.split('@').nth(1).map(|d| d.contains('.')).unwrap_or(false);
51 if valid { None } else { Some("Must be a valid email address.".into()) }
52 }
53 fn name(&self) -> &'static str { "Email" }
54}
55
56pub struct Range {
58 pub min: f64,
59 pub max: f64,
60}
61impl Range {
62 pub fn new(min: f64, max: f64) -> Self { Self { min, max } }
63}
64impl Validator for Range {
65 fn validate(&self, value: &str) -> Option<String> {
66 match value.trim().parse::<f64>() {
67 Err(_) => Some("Must be a number.".into()),
68 Ok(n) if n < self.min => Some(format!("Must be at least {}.", self.min)),
69 Ok(n) if n > self.max => Some(format!("Must be no more than {}.", self.max)),
70 Ok(_) => None,
71 }
72 }
73 fn name(&self) -> &'static str { "Range" }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn required_rejects_empty() {
82 assert!(Required.validate("").is_some());
83 }
84
85 #[test]
86 fn required_rejects_whitespace() {
87 assert!(Required.validate(" ").is_some());
88 }
89
90 #[test]
91 fn required_accepts_non_empty() {
92 assert!(Required.validate("hello").is_none());
93 }
94
95 #[test]
96 fn min_length_rejects_short() {
97 assert!(MinLength(5).validate("abc").is_some());
98 }
99
100 #[test]
101 fn min_length_accepts_exact() {
102 assert!(MinLength(3).validate("abc").is_none());
103 }
104
105 #[test]
106 fn max_length_rejects_long() {
107 assert!(MaxLength(3).validate("abcd").is_some());
108 }
109
110 #[test]
111 fn max_length_accepts_exact() {
112 assert!(MaxLength(3).validate("abc").is_none());
113 }
114
115 #[test]
116 fn contains_rejects_missing() {
117 assert!(Contains("@").validate("nodomain").is_some());
118 }
119
120 #[test]
121 fn contains_accepts_present() {
122 assert!(Contains("@").validate("user@example.com").is_none());
123 }
124
125 #[test]
126 fn email_rejects_no_at() {
127 assert!(Email.validate("nodomain.com").is_some());
128 }
129
130 #[test]
131 fn email_rejects_no_dot_after_at() {
132 assert!(Email.validate("user@nodot").is_some());
133 }
134
135 #[test]
136 fn email_accepts_valid() {
137 assert!(Email.validate("user@example.com").is_none());
138 }
139
140 #[test]
141 fn range_rejects_below() {
142 assert!(Range::new(1.0, 10.0).validate("0").is_some());
143 }
144
145 #[test]
146 fn range_rejects_above() {
147 assert!(Range::new(1.0, 10.0).validate("11").is_some());
148 }
149
150 #[test]
151 fn range_rejects_non_numeric() {
152 assert!(Range::new(1.0, 10.0).validate("abc").is_some());
153 }
154
155 #[test]
156 fn range_accepts_within() {
157 assert!(Range::new(1.0, 10.0).validate("5").is_none());
158 }
159}