use crate::error::{Severity, ValidationError};
use crate::rules::Rule;
pub struct IbanRule;
impl Rule for IbanRule {
fn id(&self) -> &'static str {
"IBAN_CHECK"
}
fn validate(&self, value: &str, path: &str) -> Vec<ValidationError> {
match validate_iban(value) {
Ok(()) => vec![],
Err(msg) => vec![ValidationError::new(
path,
Severity::Error,
"IBAN_CHECK",
msg,
)],
}
}
}
fn validate_iban(iban: &str) -> Result<(), String> {
let canonical: String = iban.chars().filter(|c| !c.is_whitespace()).collect();
let len = canonical.len();
if !(5..=34).contains(&len) {
return Err(format!(
"IBAN length {len} is out of range [5, 34]: `{iban}`"
));
}
let country = &canonical[..2];
if !country.chars().all(|c| c.is_ascii_uppercase()) {
return Err(format!(
"IBAN country code must be 2 uppercase letters, got `{country}`"
));
}
let check_str = &canonical[2..4];
if !check_str.chars().all(|c| c.is_ascii_digit()) {
return Err(format!(
"IBAN check digits must be 2 decimal digits, got `{check_str}`"
));
}
let bban = &canonical[4..];
if !bban.chars().all(|c| c.is_ascii_alphanumeric()) {
return Err(format!("IBAN BBAN must be alphanumeric, got `{bban}`"));
}
let rearranged = format!("{}{}", bban, &canonical[..4]);
let numeric = alpha_to_numeric(&rearranged);
let remainder = mod97(&numeric);
if remainder != 1 {
return Err(format!(
"IBAN check digit verification failed (mod-97 = {remainder}): `{iban}`"
));
}
Ok(())
}
use super::checkdigit::{alpha_to_numeric, mod97};
#[cfg(test)]
mod tests {
use super::*;
use crate::rules::Rule;
const VALID_IBANS: &[&str] = &[
"GB82WEST12345698765432",
"DE89370400440532013000",
"FR7630006000011234567890189",
"NL91ABNA0417164300",
"BE71096123456769",
"CH9300762011623852957",
"SE4550000000058398257466",
"NO9386011117947",
];
const INVALID_IBANS: &[&str] = &[
"GB82WEST1234569876543X", "GB82WEST123456987654321", "12WEST12345698765432", "GBXWEST12345698765432", "GB", "", "INVALID", ];
#[test]
fn valid_ibans_pass() {
let rule = IbanRule;
for iban in VALID_IBANS {
let errors = rule.validate(iban, "/test");
assert!(
errors.is_empty(),
"Expected no errors for valid IBAN `{iban}`, got: {errors:?}"
);
}
}
#[test]
fn invalid_ibans_fail() {
let rule = IbanRule;
for iban in INVALID_IBANS {
let errors = rule.validate(iban, "/test");
assert!(
!errors.is_empty(),
"Expected errors for invalid IBAN `{iban}`"
);
}
}
#[test]
fn error_has_correct_rule_id() {
let rule = IbanRule;
let errors = rule.validate("INVALID", "/some/path");
assert_eq!(errors[0].rule_id, "IBAN_CHECK");
assert_eq!(errors[0].path, "/some/path");
}
#[test]
fn rule_id_is_iban_check() {
assert_eq!(IbanRule.id(), "IBAN_CHECK");
}
#[test]
fn iban_with_spaces_is_normalised() {
let rule = IbanRule;
let errors = rule.validate("GB82 WEST 1234 5698 7654 32", "/test");
assert!(errors.is_empty(), "IBAN with spaces should be accepted");
}
}