use std::path::Path;
use super::MAX_FILE_SIZE;
use mx20022_parse::envelope::detect_message_type;
use mx20022_validate::schemes::{
cbpr::CbprPlusValidator,
fednow::FedNowValidator,
sepa::SepaValidator,
xml_scan::{extract_all_attributes, extract_all_elements, extract_element},
SchemeValidator,
};
use mx20022_validate::{RuleRegistry, Severity, ValidationResult};
#[derive(Debug)]
pub enum ValidateError {
Io(std::io::Error),
Parse(mx20022_parse::ParseError),
FileTooLarge { size: u64, max: u64 },
UnknownScheme(String),
ValidationFailed { error_count: usize },
}
impl std::fmt::Display for ValidateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidateError::Io(e) => write!(f, "I/O error: {e}"),
ValidateError::Parse(e) => write!(f, "parse error: {e}"),
ValidateError::FileTooLarge { size, max } => {
write!(
f,
"file is too large ({size} bytes); maximum allowed is {max} bytes"
)
}
ValidateError::UnknownScheme(s) => {
write!(
f,
"unknown scheme `{s}`; expected one of: fednow, sepa, cbpr"
)
}
ValidateError::ValidationFailed { error_count } => {
write!(f, "{error_count} validation error(s) found")
}
}
}
}
impl From<std::io::Error> for ValidateError {
fn from(e: std::io::Error) -> Self {
ValidateError::Io(e)
}
}
impl From<mx20022_parse::ParseError> for ValidateError {
fn from(e: mx20022_parse::ParseError) -> Self {
ValidateError::Parse(e)
}
}
impl std::error::Error for ValidateError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ValidateError::Io(e) => Some(e),
ValidateError::Parse(e) => Some(e),
ValidateError::FileTooLarge { .. }
| ValidateError::UnknownScheme(_)
| ValidateError::ValidationFailed { .. } => None,
}
}
}
fn resolve_scheme(scheme: Option<&str>) -> Result<Option<Box<dyn SchemeValidator>>, ValidateError> {
match scheme {
None => Ok(None),
Some("fednow") => Ok(Some(Box::new(FedNowValidator::new()))),
Some("sepa") => Ok(Some(Box::new(SepaValidator::new()))),
Some("cbpr") => Ok(Some(Box::new(CbprPlusValidator::new()))),
Some(other) => Err(ValidateError::UnknownScheme(other.to_owned())),
}
}
pub fn run(file: &Path, scheme: Option<&str>) -> Result<(), ValidateError> {
let meta = std::fs::metadata(file)?;
if meta.len() > MAX_FILE_SIZE {
return Err(ValidateError::FileTooLarge {
size: meta.len(),
max: MAX_FILE_SIZE,
});
}
let xml = std::fs::read_to_string(file)?;
let msg_id = detect_message_type(&xml)?;
println!("Validating: {} ({})", file.display(), msg_id.dotted());
if let Some(s) = scheme {
println!("Scheme: {s}");
}
println!();
let registry = RuleRegistry::with_defaults();
let mut result = ValidationResult::default();
let bic_tags = ["BICFI", "BIC"];
for tag in bic_tags {
for (idx, value) in extract_all_elements(&xml, tag).into_iter().enumerate() {
let path = format!("//{tag}[{}]", idx + 1);
let errors = registry.validate_field(value, &path, &["BIC_CHECK"]);
result.errors.extend(errors);
}
}
for (idx, value) in extract_all_elements(&xml, "IBAN").into_iter().enumerate() {
let path = format!("//IBAN[{}]", idx + 1);
let errors = registry.validate_field(value, &path, &["IBAN_CHECK"]);
result.errors.extend(errors);
}
for (idx, value) in extract_all_elements(&xml, "Ccy").into_iter().enumerate() {
let path = format!("//Ccy[{}]", idx + 1);
let errors = registry.validate_field(value, &path, &["CURRENCY_CHECK"]);
result.errors.extend(errors);
}
for (idx, value) in extract_all_attributes(&xml, "Ccy").into_iter().enumerate() {
let path = format!("//@Ccy[{}]", idx + 1);
let errors = registry.validate_field(value, &path, &["CURRENCY_CHECK"]);
result.errors.extend(errors);
}
for (idx, value) in extract_all_elements(&xml, "LEI").into_iter().enumerate() {
let path = format!("//LEI[{}]", idx + 1);
let errors = registry.validate_field(value, &path, &["LEI_CHECK"]);
result.errors.extend(errors);
}
for (idx, value) in extract_all_elements(&xml, "CreDtTm")
.into_iter()
.enumerate()
{
let path = format!("//CreDtTm[{}]", idx + 1);
let errors = registry.validate_field(value, &path, &["DATETIME_CHECK"]);
result.errors.extend(errors);
}
for (idx, value) in extract_all_elements(&xml, "IntrBkSttlmDt")
.into_iter()
.enumerate()
{
let path = format!("//IntrBkSttlmDt[{}]", idx + 1);
let errors = registry.validate_field(value, &path, &["DATE_CHECK"]);
result.errors.extend(errors);
}
let msg_id_present = extract_element(&xml, "BizMsgIdr")
.or_else(|| extract_element(&xml, "MsgId"))
.is_some();
if !msg_id_present {
use mx20022_validate::ValidationError;
result.errors.push(ValidationError::new(
"//GrpHdr/MsgId",
Severity::Warning,
"MSG_ID_MISSING",
"No message identifier (BizMsgIdr / MsgId) found in document",
));
}
let scheme_validator = resolve_scheme(scheme)?;
if let Some(validator) = scheme_validator {
let scheme_result = validator.validate(&xml, &msg_id.dotted());
result.merge(scheme_result);
}
let error_count = result.error_count();
let warning_count = result.warning_count();
if result.errors.is_empty() {
println!("Result: OK — no findings");
} else {
for finding in &result.errors {
let level = match finding.severity {
Severity::Error => "ERROR ",
Severity::Warning => "WARNING",
Severity::Info => "INFO ",
};
println!(
"[{level}] {} — {} ({})",
finding.path, finding.message, finding.rule_id
);
}
println!();
println!("Result: {error_count} error(s), {warning_count} warning(s)");
}
if !result.is_valid() {
return Err(ValidateError::ValidationFailed { error_count });
}
Ok(())
}