use super::{ValidationError, ValidationResult, Validator};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CardType {
Visa,
MasterCard,
AmericanExpress,
Discover,
JCB,
DinersClub,
}
impl CardType {
pub fn as_str(&self) -> &'static str {
match self {
CardType::Visa => "Visa",
CardType::MasterCard => "MasterCard",
CardType::AmericanExpress => "American Express",
CardType::Discover => "Discover",
CardType::JCB => "JCB",
CardType::DinersClub => "Diners Club",
}
}
}
impl std::fmt::Display for CardType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
pub struct CreditCardValidator {
allowed_types: Option<Vec<CardType>>,
message: Option<String>,
}
impl CreditCardValidator {
pub fn new() -> Self {
Self {
allowed_types: None,
message: None,
}
}
pub fn with_card_types(types: Vec<CardType>) -> Self {
Self {
allowed_types: Some(types),
message: None,
}
}
pub fn allow_types(mut self, types: Vec<CardType>) -> Self {
self.allowed_types = Some(types);
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
pub fn validate(&self, value: &str) -> Result<CardType, ValidationError> {
let cleaned: String = value
.chars()
.filter(|c| !c.is_whitespace() && *c != '-')
.collect();
if !cleaned.chars().all(|c| c.is_ascii_digit()) {
return Err(self.error_for_value(value));
}
if !Self::luhn_check(&cleaned) {
return Err(self.error_for_value(value));
}
let card_type = Self::detect_card_type(&cleaned)
.ok_or_else(|| ValidationError::InvalidCreditCard("Unknown card type".to_string()))?;
if let Some(ref allowed) = self.allowed_types
&& !allowed.contains(&card_type)
{
let allowed_str = allowed
.iter()
.map(|t| t.as_str())
.collect::<Vec<_>>()
.join(", ");
return Err(ValidationError::CardTypeNotAllowed {
card_type: card_type.to_string(),
allowed_types: allowed_str,
});
}
Ok(card_type)
}
fn luhn_check(card_number: &str) -> bool {
let digits: Vec<u32> = card_number.chars().filter_map(|c| c.to_digit(10)).collect();
if digits.is_empty() {
return false;
}
let mut sum = 0;
let parity = digits.len() % 2;
for (i, &digit) in digits.iter().enumerate() {
let mut d = digit;
if i % 2 == parity {
d *= 2;
if d > 9 {
d -= 9;
}
}
sum += d;
}
sum % 10 == 0
}
fn detect_card_type(card_number: &str) -> Option<CardType> {
if card_number.is_empty() {
return None;
}
if card_number.starts_with('4') {
return Some(CardType::Visa);
}
if card_number.len() >= 2 {
let prefix2: u32 = card_number[0..2].parse().unwrap_or(0);
if prefix2 == 34 || prefix2 == 37 {
return Some(CardType::AmericanExpress);
}
if (51..=55).contains(&prefix2) {
return Some(CardType::MasterCard);
}
if prefix2 == 36 || prefix2 == 38 {
return Some(CardType::DinersClub);
}
if prefix2 == 65 {
return Some(CardType::Discover);
}
}
if card_number.len() >= 3 {
let prefix3: u32 = card_number[0..3].parse().unwrap_or(0);
if (300..=305).contains(&prefix3) {
return Some(CardType::DinersClub);
}
if (644..=649).contains(&prefix3) {
return Some(CardType::Discover);
}
}
if card_number.len() >= 4 {
let prefix4: u32 = card_number[0..4].parse().unwrap_or(0);
if prefix4 == 6011 {
return Some(CardType::Discover);
}
if (2221..=2720).contains(&prefix4) {
return Some(CardType::MasterCard);
}
if (3528..=3589).contains(&prefix4) {
return Some(CardType::JCB);
}
}
if card_number.len() >= 6 {
let prefix6: u32 = card_number[0..6].parse().unwrap_or(0);
if (622126..=622925).contains(&prefix6) {
return Some(CardType::Discover);
}
}
None
}
fn error_for_value(&self, value: &str) -> ValidationError {
if let Some(ref msg) = self.message {
ValidationError::Custom(msg.clone())
} else {
ValidationError::InvalidCreditCard(value.to_string())
}
}
}
impl Default for CreditCardValidator {
fn default() -> Self {
Self::new()
}
}
impl Validator<String> for CreditCardValidator {
fn validate(&self, value: &String) -> ValidationResult<()> {
self.validate(value.as_str()).map(|_| ())
}
}
impl Validator<str> for CreditCardValidator {
fn validate(&self, value: &str) -> ValidationResult<()> {
self.validate(value).map(|_| ())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_luhn_algorithm_valid() {
assert!(CreditCardValidator::luhn_check("4532015112830366")); assert!(CreditCardValidator::luhn_check("5425233430109903")); assert!(CreditCardValidator::luhn_check("374245455400126")); assert!(CreditCardValidator::luhn_check("6011111111111117")); assert!(CreditCardValidator::luhn_check("3530111333300000")); }
#[test]
fn test_luhn_algorithm_invalid() {
assert!(!CreditCardValidator::luhn_check("4532015112830367")); assert!(!CreditCardValidator::luhn_check("1234567890123456")); assert!(!CreditCardValidator::luhn_check("1111111111111111")); assert!(!CreditCardValidator::luhn_check("9999999999999999")); }
#[test]
fn test_card_type_detection_visa() {
let card_type = CreditCardValidator::detect_card_type("4532015112830366");
assert_eq!(card_type, Some(CardType::Visa));
}
#[test]
fn test_card_type_detection_mastercard() {
assert_eq!(
CreditCardValidator::detect_card_type("5425233430109903"),
Some(CardType::MasterCard)
);
assert_eq!(
CreditCardValidator::detect_card_type("2221000000000009"),
Some(CardType::MasterCard)
);
assert_eq!(
CreditCardValidator::detect_card_type("2720999999999996"),
Some(CardType::MasterCard)
);
}
#[test]
fn test_card_type_detection_amex() {
assert_eq!(
CreditCardValidator::detect_card_type("374245455400126"),
Some(CardType::AmericanExpress)
);
assert_eq!(
CreditCardValidator::detect_card_type("340000000000009"),
Some(CardType::AmericanExpress)
);
}
#[test]
fn test_card_type_detection_discover() {
assert_eq!(
CreditCardValidator::detect_card_type("6011111111111117"),
Some(CardType::Discover)
);
assert_eq!(
CreditCardValidator::detect_card_type("6500000000000002"),
Some(CardType::Discover)
);
assert_eq!(
CreditCardValidator::detect_card_type("6440000000000004"),
Some(CardType::Discover)
);
}
#[test]
fn test_card_type_detection_jcb() {
assert_eq!(
CreditCardValidator::detect_card_type("3530111333300000"),
Some(CardType::JCB)
);
assert_eq!(
CreditCardValidator::detect_card_type("3589111333300003"),
Some(CardType::JCB)
);
}
#[test]
fn test_card_type_detection_diners_club() {
assert_eq!(
CreditCardValidator::detect_card_type("30000000000004"),
Some(CardType::DinersClub)
);
assert_eq!(
CreditCardValidator::detect_card_type("30500000000003"),
Some(CardType::DinersClub)
);
assert_eq!(
CreditCardValidator::detect_card_type("36000000000008"),
Some(CardType::DinersClub)
);
assert_eq!(
CreditCardValidator::detect_card_type("38000000000006"),
Some(CardType::DinersClub)
);
}
#[test]
fn test_validator_with_valid_cards() {
let validator = CreditCardValidator::new();
assert!(validator.validate("4532015112830366").is_ok()); assert!(validator.validate("5425233430109903").is_ok()); assert!(validator.validate("374245455400126").is_ok()); assert!(validator.validate("6011111111111117").is_ok()); assert!(validator.validate("3530111333300000").is_ok()); }
#[test]
fn test_validator_with_formatted_cards() {
let validator = CreditCardValidator::new();
assert!(validator.validate("4532-0151-1283-0366").is_ok());
assert!(validator.validate("4532 0151 1283 0366").is_ok());
assert!(validator.validate("4532 0151-1283 0366").is_ok());
}
#[test]
fn test_validator_with_invalid_cards() {
let validator = CreditCardValidator::new();
assert!(validator.validate("4532015112830367").is_err());
assert!(validator.validate("4532015112830366a").is_err());
assert!(validator.validate("abc4532015112830366").is_err());
assert!(validator.validate("").is_err());
}
#[test]
fn test_validator_with_allowed_types() {
let validator =
CreditCardValidator::new().allow_types(vec![CardType::Visa, CardType::MasterCard]);
assert!(validator.validate("4532015112830366").is_ok());
assert!(validator.validate("5425233430109903").is_ok());
match validator.validate("374245455400126") {
Err(ValidationError::CardTypeNotAllowed { .. }) => {}
_ => panic!("Expected CardTypeNotAllowed error"),
}
}
#[test]
fn test_validator_returns_card_type() {
let validator = CreditCardValidator::new();
assert_eq!(
validator.validate("4532015112830366").unwrap(),
CardType::Visa
);
assert_eq!(
validator.validate("5425233430109903").unwrap(),
CardType::MasterCard
);
assert_eq!(
validator.validate("374245455400126").unwrap(),
CardType::AmericanExpress
);
}
#[test]
fn test_validator_with_custom_message() {
let validator = CreditCardValidator::new().with_message("Custom error message");
match validator.validate("invalid") {
Err(ValidationError::Custom(msg)) => {
assert_eq!(msg, "Custom error message");
}
_ => panic!("Expected Custom error with custom message"),
}
}
#[test]
fn test_validator_trait_implementation() {
let validator = CreditCardValidator::new();
assert!(Validator::<str>::validate(&validator, "4532015112830366").is_ok());
assert!(Validator::<str>::validate(&validator, "invalid").is_err());
let card_string = String::from("4532015112830366");
assert!(Validator::<String>::validate(&validator, &card_string).is_ok());
let invalid_string = String::from("invalid");
assert!(Validator::<String>::validate(&validator, &invalid_string).is_err());
}
#[test]
fn test_card_type_display() {
assert_eq!(CardType::Visa.to_string(), "Visa");
assert_eq!(CardType::MasterCard.to_string(), "MasterCard");
assert_eq!(CardType::AmericanExpress.to_string(), "American Express");
assert_eq!(CardType::Discover.to_string(), "Discover");
assert_eq!(CardType::JCB.to_string(), "JCB");
assert_eq!(CardType::DinersClub.to_string(), "Diners Club");
}
#[test]
fn test_card_type_as_str() {
assert_eq!(CardType::Visa.as_str(), "Visa");
assert_eq!(CardType::MasterCard.as_str(), "MasterCard");
assert_eq!(CardType::AmericanExpress.as_str(), "American Express");
}
#[test]
fn test_default_validator() {
let validator = CreditCardValidator::default();
assert!(validator.validate("4532015112830366").is_ok());
}
#[test]
fn test_card_type_equality() {
assert_eq!(CardType::Visa, CardType::Visa);
assert_ne!(CardType::Visa, CardType::MasterCard);
}
#[test]
fn test_error_messages() {
let validator = CreditCardValidator::new();
match validator.validate("1234567890123456") {
Err(ValidationError::InvalidCreditCard(card)) => {
assert_eq!(card, "1234567890123456");
}
_ => panic!("Expected InvalidCreditCard error"),
}
let restricted_validator = CreditCardValidator::new().allow_types(vec![CardType::Visa]);
match restricted_validator.validate("5425233430109903") {
Err(ValidationError::CardTypeNotAllowed {
card_type,
allowed_types,
}) => {
assert_eq!(card_type, "MasterCard");
assert_eq!(allowed_types, "Visa");
}
_ => panic!("Expected CardTypeNotAllowed error"),
}
}
#[test]
fn test_edge_cases() {
let validator = CreditCardValidator::new();
assert!(validator.validate("0000000000000000").is_err());
assert!(validator.validate("0").is_err());
assert!(validator.validate("--- ").is_err());
}
#[test]
fn test_builder_pattern() {
let validator = CreditCardValidator::new()
.allow_types(vec![CardType::Visa])
.with_message("Please enter a valid Visa card");
match validator.validate("5425233430109903") {
Err(ValidationError::CardTypeNotAllowed { .. }) => {}
_ => panic!("Expected CardTypeNotAllowed error"),
}
match validator.validate("invalid") {
Err(ValidationError::Custom(msg)) => {
assert_eq!(msg, "Please enter a valid Visa card");
}
_ => panic!("Expected Custom error"),
}
}
}