skp_validator_rules/string/
alphanumeric.rs

1//! Alphanumeric validation rule.
2
3use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4
5/// Alphanumeric validation mode.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum AlphanumericMode {
8    /// Allow letters and digits (default)
9    #[default]
10    LettersAndDigits,
11    /// Allow only letters
12    LettersOnly,
13    /// Allow only digits
14    DigitsOnly,
15}
16
17/// Alphanumeric validation rule.
18///
19/// # Example
20///
21/// ```rust
22/// use skp_validator_rules::string::alphanumeric::AlphanumericRule;
23/// use skp_validator_core::{Rule, ValidationContext};
24///
25/// let rule = AlphanumericRule::new();
26/// let ctx = ValidationContext::default();
27///
28/// assert!(rule.validate("abc123", &ctx).is_ok());
29/// assert!(rule.validate("abc-123", &ctx).is_err()); // Contains dash
30/// ```
31#[derive(Debug, Clone, Default)]
32pub struct AlphanumericRule {
33    /// Validation mode
34    pub mode: AlphanumericMode,
35    /// Allow underscores
36    pub allow_underscore: bool,
37    /// Allow dashes
38    pub allow_dash: bool,
39    /// Allow spaces
40    pub allow_space: bool,
41    /// Custom error message
42    pub message: Option<String>,
43}
44
45impl AlphanumericRule {
46    /// Create a new alphanumeric rule.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Only allow letters (no digits).
52    pub fn letters_only(mut self) -> Self {
53        self.mode = AlphanumericMode::LettersOnly;
54        self
55    }
56
57    /// Only allow digits (no letters).
58    pub fn digits_only(mut self) -> Self {
59        self.mode = AlphanumericMode::DigitsOnly;
60        self
61    }
62
63    /// Allow underscores.
64    pub fn allow_underscore(mut self) -> Self {
65        self.allow_underscore = true;
66        self
67    }
68
69    /// Allow dashes.
70    pub fn allow_dash(mut self) -> Self {
71        self.allow_dash = true;
72        self
73    }
74
75    /// Allow spaces.
76    pub fn allow_space(mut self) -> Self {
77        self.allow_space = true;
78        self
79    }
80
81    /// Set custom error message.
82    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}