skp_validator_rules/string/
alphanumeric.rs1use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum AlphanumericMode {
8 #[default]
10 LettersAndDigits,
11 LettersOnly,
13 DigitsOnly,
15}
16
17#[derive(Debug, Clone, Default)]
32pub struct AlphanumericRule {
33 pub mode: AlphanumericMode,
35 pub allow_underscore: bool,
37 pub allow_dash: bool,
39 pub allow_space: bool,
41 pub message: Option<String>,
43}
44
45impl AlphanumericRule {
46 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn letters_only(mut self) -> Self {
53 self.mode = AlphanumericMode::LettersOnly;
54 self
55 }
56
57 pub fn digits_only(mut self) -> Self {
59 self.mode = AlphanumericMode::DigitsOnly;
60 self
61 }
62
63 pub fn allow_underscore(mut self) -> Self {
65 self.allow_underscore = true;
66 self
67 }
68
69 pub fn allow_dash(mut self) -> Self {
71 self.allow_dash = true;
72 self
73 }
74
75 pub fn allow_space(mut self) -> Self {
77 self.allow_space = true;
78 self
79 }
80
81 pub fn message(mut self, msg: impl Into<String>) -> Self {
83 self.message = Some(msg.into());
84 self
85 }
86
87 fn get_message(&self) -> String {
88 self.message.clone().unwrap_or_else(|| {
89 match self.mode {
90 AlphanumericMode::LettersAndDigits => "Must contain only letters and digits".to_string(),
91 AlphanumericMode::LettersOnly => "Must contain only letters".to_string(),
92 AlphanumericMode::DigitsOnly => "Must contain only digits".to_string(),
93 }
94 })
95 }
96
97 fn is_allowed_char(&self, c: char) -> bool {
98 let base_check = match self.mode {
99 AlphanumericMode::LettersAndDigits => c.is_alphanumeric(),
100 AlphanumericMode::LettersOnly => c.is_alphabetic(),
101 AlphanumericMode::DigitsOnly => c.is_ascii_digit(),
102 };
103
104 if base_check {
105 return true;
106 }
107
108 if self.allow_underscore && c == '_' {
109 return true;
110 }
111
112 if self.allow_dash && c == '-' {
113 return true;
114 }
115
116 if self.allow_space && c == ' ' {
117 return true;
118 }
119
120 false
121 }
122}
123
124impl Rule<str> for AlphanumericRule {
125 fn validate(&self, value: &str, _ctx: &ValidationContext) -> ValidationResult<()> {
126 if value.is_empty() {
127 return Ok(());
128 }
129
130 if value.chars().all(|c| self.is_allowed_char(c)) {
131 Ok(())
132 } else {
133 Err(ValidationErrors::from_iter([
134 ValidationError::root("alphanumeric", self.get_message())
135 ]))
136 }
137 }
138
139 fn name(&self) -> &'static str {
140 "alphanumeric"
141 }
142
143 fn default_message(&self) -> String {
144 self.get_message()
145 }
146}
147
148impl Rule<String> for AlphanumericRule {
149 fn validate(&self, value: &String, ctx: &ValidationContext) -> ValidationResult<()> {
150 <Self as Rule<str>>::validate(self, value.as_str(), ctx)
151 }
152
153 fn name(&self) -> &'static str {
154 "alphanumeric"
155 }
156
157 fn default_message(&self) -> String {
158 self.get_message()
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_alphanumeric() {
168 let rule = AlphanumericRule::new();
169 let ctx = ValidationContext::default();
170
171 assert!(rule.validate("abc123", &ctx).is_ok());
172 assert!(rule.validate("ABC", &ctx).is_ok());
173 assert!(rule.validate("123", &ctx).is_ok());
174 assert!(rule.validate("abc-123", &ctx).is_err());
175 assert!(rule.validate("abc_123", &ctx).is_err());
176 }
177
178 #[test]
179 fn test_letters_only() {
180 let rule = AlphanumericRule::new().letters_only();
181 let ctx = ValidationContext::default();
182
183 assert!(rule.validate("abcXYZ", &ctx).is_ok());
184 assert!(rule.validate("abc123", &ctx).is_err());
185 }
186
187 #[test]
188 fn test_digits_only() {
189 let rule = AlphanumericRule::new().digits_only();
190 let ctx = ValidationContext::default();
191
192 assert!(rule.validate("12345", &ctx).is_ok());
193 assert!(rule.validate("abc123", &ctx).is_err());
194 }
195
196 #[test]
197 fn test_with_underscore() {
198 let rule = AlphanumericRule::new().allow_underscore();
199 let ctx = ValidationContext::default();
200
201 assert!(rule.validate("abc_123", &ctx).is_ok());
202 assert!(rule.validate("abc-123", &ctx).is_err());
203 }
204
205 #[test]
206 fn test_with_all_extras() {
207 let rule = AlphanumericRule::new()
208 .allow_underscore()
209 .allow_dash()
210 .allow_space();
211 let ctx = ValidationContext::default();
212
213 assert!(rule.validate("abc_123-def xyz", &ctx).is_ok());
214 }
215
216 #[test]
217 fn test_empty_is_valid() {
218 let rule = AlphanumericRule::new();
219 let ctx = ValidationContext::default();
220
221 assert!(rule.validate("", &ctx).is_ok());
222 }
223}