use crate::error::{Severity, ValidationError};
use crate::rules::Rule;
pub struct LeiRule;
impl Rule for LeiRule {
fn id(&self) -> &'static str {
"LEI_CHECK"
}
fn validate(&self, value: &str, path: &str) -> Vec<ValidationError> {
match validate_lei(value) {
Ok(()) => vec![],
Err(msg) => vec![ValidationError::new(
path,
Severity::Error,
"LEI_CHECK",
msg,
)],
}
}
}
fn validate_lei(lei: &str) -> Result<(), String> {
if lei.len() != 20 {
return Err(format!(
"LEI must be exactly 20 characters, got {}: `{lei}`",
lei.len()
));
}
for (i, c) in lei.chars().enumerate() {
if !c.is_ascii_uppercase() && !c.is_ascii_digit() {
return Err(format!(
"LEI must contain only uppercase alphanumeric characters; \
character {} (`{c}`) is invalid in `{lei}`",
i + 1
));
}
}
let check_str = &lei[18..20];
if !check_str.chars().all(|c| c.is_ascii_digit()) {
return Err(format!(
"LEI check digits (characters 19-20) must be decimal digits, \
got `{check_str}` in `{lei}`"
));
}
let numeric = alpha_to_numeric(lei);
let remainder = mod97(&numeric);
if remainder != 1 {
return Err(format!(
"LEI check digit verification failed (mod-97 = {remainder}): `{lei}`"
));
}
Ok(())
}
use super::checkdigit::{alpha_to_numeric, mod97};
#[cfg(test)]
mod tests {
use super::*;
use crate::rules::Rule;
const VALID_LEIS: &[&str] = &[
"7ZW8QJWVPR4P1S5PX088",
"5493001KJTIIGC8Y1R12",
"213800WSGIIZCXF1P572",
];
const INVALID_LEIS: &[&str] = &[
"TOOSHORT", "7ZW8QJWVPR4P1S5PX08800", "7ZW8QJWVPR4P1S5PX0!8", "7zw8QJWVPR4P1S5PX088", "7ZW8QJWVPR4P1S5PX0AA", "7ZW8QJWVPR4P1S5PX099", "", ];
#[test]
fn valid_leis_pass() {
let rule = LeiRule;
for lei in VALID_LEIS {
let errors = rule.validate(lei, "/test");
assert!(
errors.is_empty(),
"Expected no errors for valid LEI `{lei}`, got: {errors:?}"
);
}
}
#[test]
fn invalid_leis_fail() {
let rule = LeiRule;
for lei in INVALID_LEIS {
let errors = rule.validate(lei, "/test");
assert!(
!errors.is_empty(),
"Expected errors for invalid LEI `{lei}`"
);
}
}
#[test]
fn error_has_correct_rule_id_and_path() {
let rule = LeiRule;
let errors = rule.validate("TOOSHORT", "/Document/LEI");
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].rule_id, "LEI_CHECK");
assert_eq!(errors[0].path, "/Document/LEI");
assert_eq!(errors[0].severity, Severity::Error);
}
#[test]
fn rule_id_is_lei_check() {
assert_eq!(LeiRule.id(), "LEI_CHECK");
}
#[test]
fn wrong_length_produces_length_message() {
let rule = LeiRule;
let errors = rule.validate("TOOSHORT", "/test");
assert!(!errors.is_empty());
assert!(
errors[0].message.contains("20 characters"),
"Expected length message, got: {}",
errors[0].message
);
}
#[test]
fn bad_check_digits_produces_mod97_message() {
let rule = LeiRule;
let errors = rule.validate("7ZW8QJWVPR4P1S5PX099", "/test");
assert!(!errors.is_empty());
assert!(
errors[0].message.contains("mod-97") || errors[0].message.contains("check digit"),
"Expected mod-97 message, got: {}",
errors[0].message
);
}
#[test]
fn lowercase_characters_rejected() {
let rule = LeiRule;
let errors = rule.validate("7zw8QJWVPR4P1S5PX085", "/test");
assert!(!errors.is_empty());
}
}