skp_validator_rules/financial/
credit_card.rs1use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4
5#[derive(Debug, Clone, Default)]
20pub struct CreditCardRule {
21 pub message: Option<String>,
23}
24
25impl CreditCardRule {
26 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub fn message(mut self, msg: impl Into<String>) -> Self {
33 self.message = Some(msg.into());
34 self
35 }
36
37 fn get_message(&self) -> String {
38 self.message.clone().unwrap_or_else(|| "Must be a valid credit card number".to_string())
39 }
40
41 fn luhn_check(&self, number: &str) -> bool {
43 let digits: String = number.chars().filter(|c| c.is_ascii_digit()).collect();
45
46 if digits.len() < 13 || digits.len() > 19 {
48 return false;
49 }
50
51 let mut sum = 0;
52 let mut double = false;
53
54 for c in digits.chars().rev() {
56 if let Some(digit) = c.to_digit(10) {
57 let mut value = digit;
58 if double {
59 value *= 2;
60 if value > 9 {
61 value -= 9;
62 }
63 }
64 sum += value;
65 double = !double;
66 } else {
67 return false;
68 }
69 }
70
71 sum % 10 == 0
72 }
73}
74
75impl Rule<str> for CreditCardRule {
76 fn validate(&self, value: &str, _ctx: &ValidationContext) -> ValidationResult<()> {
77 if value.is_empty() {
78 return Ok(());
79 }
80
81 if self.luhn_check(value) {
82 Ok(())
83 } else {
84 Err(ValidationErrors::from_iter([
85 ValidationError::root("credit_card", self.get_message())
86 ]))
87 }
88 }
89
90 fn name(&self) -> &'static str {
91 "credit_card"
92 }
93
94 fn default_message(&self) -> String {
95 self.get_message()
96 }
97}
98
99impl Rule<String> for CreditCardRule {
100 fn validate(&self, value: &String, ctx: &ValidationContext) -> ValidationResult<()> {
101 <Self as Rule<str>>::validate(self, value.as_str(), ctx)
102 }
103
104 fn name(&self) -> &'static str {
105 "credit_card"
106 }
107
108 fn default_message(&self) -> String {
109 self.get_message()
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn test_valid_cards() {
119 let rule = CreditCardRule::new();
120 let ctx = ValidationContext::default();
121
122 assert!(rule.validate("4532015112830366", &ctx).is_ok()); assert!(rule.validate("5425233430109903", &ctx).is_ok()); assert!(rule.validate("378282246310005", &ctx).is_ok()); assert!(rule.validate("4111111111111111", &ctx).is_ok()); }
128
129 #[test]
130 fn test_invalid_cards() {
131 let rule = CreditCardRule::new();
132 let ctx = ValidationContext::default();
133
134 assert!(rule.validate("1234567890123456", &ctx).is_err());
135 assert!(rule.validate("4111111111111112", &ctx).is_err()); }
137
138 #[test]
139 fn test_with_spaces() {
140 let rule = CreditCardRule::new();
141 let ctx = ValidationContext::default();
142
143 assert!(rule.validate("4532 0151 1283 0366", &ctx).is_ok());
145 }
146
147 #[test]
148 fn test_with_dashes() {
149 let rule = CreditCardRule::new();
150 let ctx = ValidationContext::default();
151
152 assert!(rule.validate("4532-0151-1283-0366", &ctx).is_ok());
154 }
155
156 #[test]
157 fn test_empty_is_valid() {
158 let rule = CreditCardRule::new();
159 let ctx = ValidationContext::default();
160
161 assert!(rule.validate("", &ctx).is_ok());
162 }
163
164 #[test]
165 fn test_too_short() {
166 let rule = CreditCardRule::new();
167 let ctx = ValidationContext::default();
168
169 assert!(rule.validate("123456789012", &ctx).is_err()); }
171}