Skip to main content

karbon_framework/validation/constraints/security/
username.rs

1use crate::validation::constraints::{Constraint, ConstraintResult, ConstraintViolation};
2
3/// Validates that a value is a valid username.
4///
5/// Rules:
6/// - Only alphanumeric, underscores, hyphens, and dots
7/// - Must start with a letter or digit
8/// - Cannot end with a special character
9/// - No consecutive special characters
10/// - Length between min and max
11pub struct Username {
12    pub message: String,
13    pub min_length: usize,
14    pub max_length: usize,
15    pub allow_dots: bool,
16    pub allow_hyphens: bool,
17}
18
19impl Default for Username {
20    fn default() -> Self {
21        Self {
22            message: "This value is not a valid username.".to_string(),
23            min_length: 3,
24            max_length: 32,
25            allow_dots: true,
26            allow_hyphens: true,
27        }
28    }
29}
30
31impl Username {
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    pub fn with_message(mut self, message: impl Into<String>) -> Self {
37        self.message = message.into();
38        self
39    }
40
41    pub fn min_length(mut self, min: usize) -> Self {
42        self.min_length = min;
43        self
44    }
45
46    pub fn max_length(mut self, max: usize) -> Self {
47        self.max_length = max;
48        self
49    }
50
51    pub fn allow_dots(mut self, allow: bool) -> Self {
52        self.allow_dots = allow;
53        self
54    }
55
56    pub fn allow_hyphens(mut self, allow: bool) -> Self {
57        self.allow_hyphens = allow;
58        self
59    }
60
61    fn is_special(c: char) -> bool {
62        c == '_' || c == '.' || c == '-'
63    }
64
65    fn is_valid_username(&self, value: &str) -> bool {
66        let len = value.len();
67
68        if len < self.min_length || len > self.max_length {
69            return false;
70        }
71
72        let chars: Vec<char> = value.chars().collect();
73
74        // Must start with alphanumeric
75        if !chars[0].is_alphanumeric() {
76            return false;
77        }
78
79        // Must end with alphanumeric
80        if !chars[len - 1].is_alphanumeric() {
81            return false;
82        }
83
84        for (i, &c) in chars.iter().enumerate() {
85            if c.is_alphanumeric() {
86                continue;
87            }
88
89            if c == '.' && !self.allow_dots {
90                return false;
91            }
92
93            if c == '-' && !self.allow_hyphens {
94                return false;
95            }
96
97            if !c.is_alphanumeric() && c != '_' && c != '.' && c != '-' {
98                return false;
99            }
100
101            // No consecutive special characters
102            if i > 0 && Self::is_special(chars[i - 1]) {
103                return false;
104            }
105        }
106
107        true
108    }
109}
110
111impl Constraint for Username {
112    fn validate(&self, value: &str) -> ConstraintResult {
113        if !self.is_valid_username(value) {
114            return Err(ConstraintViolation::new(
115                self.name(),
116                &self.message,
117                value,
118            ));
119        }
120        Ok(())
121    }
122
123    fn name(&self) -> &'static str {
124        "Username"
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_valid_usernames() {
134        let constraint = Username::new();
135        assert!(constraint.validate("john").is_ok());
136        assert!(constraint.validate("john_doe").is_ok());
137        assert!(constraint.validate("john.doe").is_ok());
138        assert!(constraint.validate("john-doe").is_ok());
139        assert!(constraint.validate("user123").is_ok());
140        assert!(constraint.validate("a1b").is_ok());
141    }
142
143    #[test]
144    fn test_invalid_usernames() {
145        let constraint = Username::new();
146        assert!(constraint.validate("").is_err());
147        assert!(constraint.validate("ab").is_err()); // too short
148        assert!(constraint.validate("_john").is_err()); // starts with special
149        assert!(constraint.validate("john_").is_err()); // ends with special
150        assert!(constraint.validate("john__doe").is_err()); // consecutive specials
151        assert!(constraint.validate("john..doe").is_err()); // consecutive specials
152        assert!(constraint.validate("john doe").is_err()); // space
153        assert!(constraint.validate("john@doe").is_err()); // invalid char
154    }
155
156    #[test]
157    fn test_no_dots_allowed() {
158        let constraint = Username::new().allow_dots(false);
159        assert!(constraint.validate("john_doe").is_ok());
160        assert!(constraint.validate("john.doe").is_err());
161    }
162
163    #[test]
164    fn test_no_hyphens_allowed() {
165        let constraint = Username::new().allow_hyphens(false);
166        assert!(constraint.validate("john_doe").is_ok());
167        assert!(constraint.validate("john-doe").is_err());
168    }
169
170    #[test]
171    fn test_length_constraints() {
172        let constraint = Username::new().min_length(5).max_length(10);
173        assert!(constraint.validate("johnd").is_ok());
174        assert!(constraint.validate("john").is_err());
175        assert!(constraint.validate("johndoe1234").is_err());
176    }
177}