karbon_framework/validation/constraints/security/
username.rs1use crate::validation::constraints::{Constraint, ConstraintResult, ConstraintViolation};
2
3pub 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 if !chars[0].is_alphanumeric() {
76 return false;
77 }
78
79 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 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()); assert!(constraint.validate("_john").is_err()); assert!(constraint.validate("john_").is_err()); assert!(constraint.validate("john__doe").is_err()); assert!(constraint.validate("john..doe").is_err()); assert!(constraint.validate("john doe").is_err()); assert!(constraint.validate("john@doe").is_err()); }
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}