use rand::seq::SliceRandom;
use rand::Rng;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CardBrand {
Visa,
Mastercard,
AmericanExpress,
Discover,
UnionPay,
Jcb,
DinersClubInternational,
Elo,
RuPay,
Mir,
CartesBancaires,
BcCard,
Verve,
}
impl CardBrand {
pub fn as_str(self) -> &'static str {
match self {
CardBrand::Visa => "Visa",
CardBrand::Mastercard => "Mastercard",
CardBrand::AmericanExpress => "American Express",
CardBrand::Discover => "Discover Financial Services",
CardBrand::UnionPay => "UnionPay",
CardBrand::Jcb => "JCB Co., Ltd.",
CardBrand::DinersClubInternational => "Diners Club International",
CardBrand::Elo => "Elo",
CardBrand::RuPay => "RuPay",
CardBrand::Mir => "Mir",
CardBrand::CartesBancaires => "Cartes Bancaires",
CardBrand::BcCard => "BC Card",
CardBrand::Verve => "Verve",
}
}
}
impl fmt::Display for CardBrand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreditCardData {
pub brand: CardBrand,
pub number: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CreditCardOutputFormat {
Plain,
Json,
JsonLd,
}
impl CardBrand {
fn random(rng: &mut impl Rng) -> Self {
const BRANDS: &[CardBrand] = &[
CardBrand::Visa,
CardBrand::Mastercard,
CardBrand::AmericanExpress,
CardBrand::Discover,
CardBrand::UnionPay,
CardBrand::Jcb,
CardBrand::DinersClubInternational,
CardBrand::Elo,
CardBrand::RuPay,
CardBrand::Mir,
CardBrand::CartesBancaires,
CardBrand::BcCard,
CardBrand::Verve,
];
*BRANDS.choose(rng).unwrap()
}
fn pattern(self, rng: &mut impl Rng) -> (String, usize) {
match self {
CardBrand::Visa => {
let lengths = [13, 16, 19];
("4".to_string(), *lengths.choose(rng).unwrap())
}
CardBrand::Mastercard => {
if rng.gen_bool(0.5) {
(rng.gen_range(51..=55).to_string(), 16)
} else {
(rng.gen_range(2221..=2720).to_string(), 16)
}
}
CardBrand::AmericanExpress => {
let prefixes = ["34", "37"];
(prefixes.choose(rng).unwrap().to_string(), 15)
}
CardBrand::Discover => {
let choice = rng.gen_range(0..4);
match choice {
0 => ("6011".to_string(), 16),
1 => ("65".to_string(), 16),
2 => (rng.gen_range(644..=649).to_string(), 16),
_ => (rng.gen_range(622126..=622925).to_string(), 16),
}
}
CardBrand::UnionPay => ("62".to_string(), rng.gen_range(16..=19)),
CardBrand::Jcb => (rng.gen_range(3528..=3589).to_string(), 16),
CardBrand::DinersClubInternational => {
let choice = rng.gen_range(0..3);
match choice {
0 => (rng.gen_range(300..=305).to_string(), 14),
1 => ("36".to_string(), 14),
_ => (rng.gen_range(38..=39).to_string(), 14),
}
}
CardBrand::Elo => {
let prefixes = [
"401178", "401179", "431274", "438935", "451416", "457393", "457631", "504175",
"627780", "636297", "636368", "650031", "650033", "650051", "650405", "650485",
"650538", "650541", "650598", "650700", "650718", "650720", "650727", "650901",
"650978", "651652", "651679", "655000", "655019",
];
(prefixes.choose(rng).unwrap().to_string(), 16)
}
CardBrand::RuPay => {
let prefixes = ["60", "65", "81", "82", "508"];
(prefixes.choose(rng).unwrap().to_string(), 16)
}
CardBrand::Mir => (rng.gen_range(2200..=2204).to_string(), 16),
CardBrand::CartesBancaires => {
if rng.gen_bool(0.5) {
("4".to_string(), 16)
} else {
(rng.gen_range(51..=55).to_string(), 16)
}
}
CardBrand::BcCard => {
let prefixes = ["94", "4579", "5150"];
(prefixes.choose(rng).unwrap().to_string(), 16)
}
CardBrand::Verve => {
let choice = rng.gen_range(0..3);
let prefix = match choice {
0 => rng.gen_range(506099..=506198).to_string(),
1 => rng.gen_range(650002..=650027).to_string(),
_ => rng.gen_range(507865..=507964).to_string(),
};
let lengths = [16, 18, 19];
(prefix, *lengths.choose(rng).unwrap())
}
}
}
}
pub fn generate_credit_card() -> String {
generate_credit_card_data().number
}
pub fn generate_credit_card_for_brand(brand: CardBrand) -> String {
generate_credit_card_data_for_brand(brand).number
}
pub fn generate_credit_card_data() -> CreditCardData {
let mut rng = rand::thread_rng();
let brand = CardBrand::random(&mut rng);
generate_credit_card_data_for_brand(brand)
}
pub fn generate_credit_card_data_for_brand(brand: CardBrand) -> CreditCardData {
let mut rng = rand::thread_rng();
let (prefix, total_length) = brand.pattern(&mut rng);
let number = generate_with_luhn(prefix, total_length, &mut rng);
CreditCardData { brand, number }
}
pub fn generate_credit_card_for_brand_name(brand_name: &str) -> Option<String> {
let brand = parse_brand_name(brand_name)?;
Some(generate_credit_card_for_brand(brand))
}
pub fn generate_credit_card_data_for_brand_name(brand_name: &str) -> Option<CreditCardData> {
let brand = parse_brand_name(brand_name)?;
Some(generate_credit_card_data_for_brand(brand))
}
pub fn format_credit_card_data(card: &CreditCardData, format: CreditCardOutputFormat) -> String {
match format {
CreditCardOutputFormat::Plain => {
format!("Brand: {}\nNumber: {}", card.brand.as_str(), card.number)
}
CreditCardOutputFormat::Json => {
format!(
"{{\"brand\":\"{}\",\"number\":\"{}\"}}",
card.brand.as_str(),
card.number
)
}
CreditCardOutputFormat::JsonLd => {
format!(
"{{\"@context\":\"https://schema.org\",\"@type\":\"PaymentCard\",\"brand\":\"{}\",\"identifier\":\"{}\"}}",
card.brand.as_str(),
card.number
)
}
}
}
pub fn generate_credit_card_formatted(format: CreditCardOutputFormat) -> String {
let card = generate_credit_card_data();
format_credit_card_data(&card, format)
}
pub fn generate_credit_card_formatted_for_brand(
brand: CardBrand,
format: CreditCardOutputFormat,
) -> String {
let card = generate_credit_card_data_for_brand(brand);
format_credit_card_data(&card, format)
}
pub fn generate_credit_card_formatted_for_brand_name(
brand_name: &str,
format: CreditCardOutputFormat,
) -> Option<String> {
let card = generate_credit_card_data_for_brand_name(brand_name)?;
Some(format_credit_card_data(&card, format))
}
fn parse_brand_name(brand_name: &str) -> Option<CardBrand> {
let normalized: String = brand_name
.to_lowercase()
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect();
match normalized.as_str() {
"visa" => Some(CardBrand::Visa),
"mastercard" => Some(CardBrand::Mastercard),
"americanexpress" | "amex" => Some(CardBrand::AmericanExpress),
"discover" | "discoverfinancialservices" => Some(CardBrand::Discover),
"unionpay" | "chinaunionpay" => Some(CardBrand::UnionPay),
"jcb" | "jcbcoltd" | "jcbco" => Some(CardBrand::Jcb),
"dinersclubinternational" | "dinersclub" | "diners" => {
Some(CardBrand::DinersClubInternational)
}
"elo" | "elocartaodepagamento" | "elocartodepagamento" => Some(CardBrand::Elo),
"rupay" => Some(CardBrand::RuPay),
"mir" | "mirpaymentsystem" => Some(CardBrand::Mir),
"cartesbancaires" | "cb" => Some(CardBrand::CartesBancaires),
"bccard" | "bc" => Some(CardBrand::BcCard),
"verve" | "vervepaymentcards" => Some(CardBrand::Verve),
_ => None,
}
}
fn generate_with_luhn(prefix: String, total_length: usize, rng: &mut impl Rng) -> String {
let mut digits: Vec<u8> = prefix
.chars()
.map(|c| c.to_digit(10).unwrap() as u8)
.collect();
while digits.len() + 1 < total_length {
digits.push(rng.gen_range(0..=9));
}
let check = luhn_check_digit(&digits);
digits.push(check);
digits.into_iter().map(|d| (b'0' + d) as char).collect()
}
fn luhn_check_digit(payload: &[u8]) -> u8 {
let mut sum = 0u32;
let mut double = true;
for digit in payload.iter().rev() {
let mut value = *digit as u32;
if double {
value *= 2;
if value > 9 {
value -= 9;
}
}
sum += value;
double = !double;
}
((10 - (sum % 10)) % 10) as u8
}
#[cfg(test)]
mod tests {
use super::*;
fn is_luhn_valid(number: &str) -> bool {
let mut sum = 0u32;
let mut double = false;
for ch in number.chars().rev() {
let mut digit = ch.to_digit(10).unwrap();
if double {
digit *= 2;
if digit > 9 {
digit -= 9;
}
}
sum += digit;
double = !double;
}
sum.is_multiple_of(10)
}
#[test]
fn test_generate_credit_card_default() {
let card = generate_credit_card();
assert!(card.chars().all(|c| c.is_ascii_digit()));
assert!(is_luhn_valid(&card));
}
#[test]
fn test_generate_all_brand_variants() {
let brands = [
CardBrand::Visa,
CardBrand::Mastercard,
CardBrand::AmericanExpress,
CardBrand::Discover,
CardBrand::UnionPay,
CardBrand::Jcb,
CardBrand::DinersClubInternational,
CardBrand::Elo,
CardBrand::RuPay,
CardBrand::Mir,
CardBrand::CartesBancaires,
CardBrand::BcCard,
CardBrand::Verve,
];
for brand in brands {
let card = generate_credit_card_for_brand(brand);
assert!(card.chars().all(|c| c.is_ascii_digit()));
assert!(is_luhn_valid(&card));
}
}
#[test]
fn test_brand_name_parser() {
let cases = [
"Visa",
"Mastercard",
"American Express",
"Discover Financial Services",
"UnionPay",
"JCB Co., Ltd.",
"Diners Club International",
"Elo",
"RuPay",
"Mir",
"Cartes Bancaires",
"BC Card",
"Verve",
"Amex",
];
for case in cases {
let card = generate_credit_card_for_brand_name(case);
assert!(card.is_some(), "brand should be accepted: {case}");
assert!(is_luhn_valid(&card.unwrap()));
}
}
#[test]
fn test_unknown_brand_name_returns_none() {
let card = generate_credit_card_for_brand_name("unknown-brand");
assert!(card.is_none());
}
#[test]
fn test_generate_credit_card_data_has_brand_and_number() {
let card = generate_credit_card_data();
assert!(!card.number.is_empty());
assert!(is_luhn_valid(&card.number));
assert!(!card.brand.as_str().is_empty());
}
#[test]
fn test_plain_format() {
let card = generate_credit_card_data_for_brand(CardBrand::Visa);
let output = format_credit_card_data(&card, CreditCardOutputFormat::Plain);
assert!(output.contains("Brand: Visa"));
assert!(output.contains("Number: "));
}
#[test]
fn test_json_format() {
let card = generate_credit_card_data_for_brand(CardBrand::Visa);
let output = format_credit_card_data(&card, CreditCardOutputFormat::Json);
assert!(output.contains("\"brand\":\"Visa\""));
assert!(output.contains("\"number\":\""));
}
#[test]
fn test_jsonld_format() {
let card = generate_credit_card_data_for_brand(CardBrand::Visa);
let output = format_credit_card_data(&card, CreditCardOutputFormat::JsonLd);
assert!(output.contains("\"@context\":\"https://schema.org\""));
assert!(output.contains("\"@type\":\"PaymentCard\""));
assert!(output.contains("\"brand\":\"Visa\""));
assert!(output.contains("\"identifier\":\""));
}
}