rust_iso20022 0.1.2

ISO 20022 financial message parser and generated Rust types for SWIFT MX XML/JSON.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
//! Explainable ISO semantic rules for validated financial scalar fields.

use super::{
    FieldPath, Rule, RuleDescriptor, RuleId, Severity, ValidationContext, ValidationIssue,
    ValidationLayer, ValidationTarget,
};
use crate::helpers::{
    AccountIdentifier, Bic, ClearingIdentifier, CountryCode, Currency, Iban, IsoDate, IsoDateTime,
    Lei, Money, ValueError,
};

type Validator = fn(&ValidationTarget<'_>) -> Vec<(&'static str, ValueError)>;

struct FinancialRule {
    descriptor: &'static RuleDescriptor,
    validator: Validator,
    field_paths: &'static [&'static str],
}

impl Rule for FinancialRule {
    fn descriptor(&self) -> &'static RuleDescriptor {
        self.descriptor
    }

    fn field_paths(&self) -> Vec<&'static str> {
        self.field_paths.to_vec()
    }

    fn evaluate(
        &self,
        target: &ValidationTarget<'_>,
        _context: &ValidationContext<'_>,
    ) -> Vec<ValidationIssue> {
        (self.validator)(target)
            .into_iter()
            .filter(|(_, error)| error.rule_id() == self.descriptor.id)
            .map(|(path, _)| {
                ValidationIssue::new(
                    self.descriptor,
                    Severity::Error,
                    FieldPath::new(path),
                    self.descriptor.reason,
                )
            })
            .collect()
    }
}

macro_rules! financial_rule {
    ($descriptor:ident, $rule:ident, $id:literal, $title:literal, $reason:literal, $source:path, $validator:ident, $paths:expr) => {
        static $descriptor: RuleDescriptor = RuleDescriptor {
            id: RuleId::new($id),
            layer: ValidationLayer::IsoSemantic,
            title: $title,
            reason: $reason,
            source: Some($source),
            profile: None,
            profile_version: None,
            affected_messages: &[],
        };
        static $rule: FinancialRule = FinancialRule {
            descriptor: &$descriptor,
            validator: $validator,
            field_paths: $paths,
        };
    };
}

const ISO_SOURCE: &str = "ISO 20022 schema-derived scalar constraint";
const IBAN_SOURCE: &str = "ISO 13616 and SWIFT IBAN Registry release 102 (June 2026)";
const BIC_SOURCE: &str = "ISO 9362 syntax; no SWIFT BIC Directory existence lookup";
const LEI_SOURCE: &str = "ISO 17442 checksum syntax; no GLEIF directory status lookup";
const CURRENCY_SOURCE: &str = "ISO 4217 SIX List One published 2026-09-17";
const COUNTRY_SOURCE: &str = "ISO 3166-1 alpha-2 snapshot from rust_iso3166 0.2.0";

financial_rule!(
    IBAN_CHARSET_D,
    IBAN_CHARSET_R,
    "ISO20022-L2-IBAN-CHARSET",
    "IBAN character set",
    "IBAN must contain only canonical uppercase ASCII letters and digits.",
    IBAN_SOURCE,
    validate_iban,
    &["$financial.iban"]
);
financial_rule!(
    IBAN_LENGTH_D,
    IBAN_LENGTH_R,
    "ISO20022-L2-IBAN-LENGTH",
    "IBAN length",
    "IBAN length must match the registered national format.",
    IBAN_SOURCE,
    validate_iban,
    &["$financial.iban"]
);
financial_rule!(
    IBAN_FORMAT_D,
    IBAN_FORMAT_R,
    "ISO20022-L2-IBAN-FORMAT",
    "IBAN header",
    "IBAN must start with an uppercase country code and two check digits.",
    IBAN_SOURCE,
    validate_iban,
    &["$financial.iban"]
);
financial_rule!(
    IBAN_COUNTRY_D,
    IBAN_COUNTRY_R,
    "ISO20022-L2-IBAN-COUNTRY",
    "IBAN registry country",
    "IBAN country must exist in the pinned SWIFT IBAN Registry snapshot.",
    IBAN_SOURCE,
    validate_iban,
    &["$financial.iban"]
);
financial_rule!(
    IBAN_BBAN_D,
    IBAN_BBAN_R,
    "ISO20022-L2-IBAN-BBAN-FORMAT",
    "IBAN national structure",
    "IBAN BBAN characters must match the registered national structure.",
    IBAN_SOURCE,
    validate_iban,
    &["$financial.iban"]
);
financial_rule!(
    IBAN_CHECKSUM_D,
    IBAN_CHECKSUM_R,
    "ISO20022-L2-IBAN-CHECKSUM",
    "IBAN checksum",
    "IBAN must satisfy the ISO 13616 MOD-97 checksum.",
    IBAN_SOURCE,
    validate_iban,
    &["$financial.iban"]
);

financial_rule!(
    BIC_LENGTH_D,
    BIC_LENGTH_R,
    "ISO20022-L2-BIC-LENGTH",
    "BIC length",
    "BIC must contain 8 or 11 characters.",
    BIC_SOURCE,
    validate_bic,
    &["$financial.bic"]
);
financial_rule!(
    BIC_FORMAT_D,
    BIC_FORMAT_R,
    "ISO20022-L2-BIC-FORMAT",
    "BIC syntax",
    "BIC components must use the ISO 9362 uppercase ASCII syntax.",
    BIC_SOURCE,
    validate_bic,
    &["$financial.bic"]
);
financial_rule!(
    BIC_COUNTRY_D,
    BIC_COUNTRY_R,
    "ISO20022-L2-BIC-COUNTRY",
    "BIC country",
    "BIC country component must be an assigned ISO 3166-1 alpha-2 code.",
    BIC_SOURCE,
    validate_bic,
    &["$financial.bic"]
);

financial_rule!(
    LEI_LENGTH_D,
    LEI_LENGTH_R,
    "ISO20022-L2-LEI-LENGTH",
    "LEI length",
    "LEI must contain exactly 20 characters.",
    LEI_SOURCE,
    validate_lei,
    &["$financial.lei"]
);
financial_rule!(
    LEI_CHARSET_D,
    LEI_CHARSET_R,
    "ISO20022-L2-LEI-CHARSET",
    "LEI character set",
    "LEI must use uppercase ASCII letters and digits.",
    LEI_SOURCE,
    validate_lei,
    &["$financial.lei"]
);
financial_rule!(
    LEI_CHECKSUM_D,
    LEI_CHECKSUM_R,
    "ISO20022-L2-LEI-CHECKSUM",
    "LEI checksum",
    "LEI must satisfy the ISO 17442 MOD-97 checksum.",
    LEI_SOURCE,
    validate_lei,
    &["$financial.lei"]
);

financial_rule!(
    CURRENCY_D,
    CURRENCY_R,
    "ISO20022-L2-CURRENCY-CODE",
    "Active currency code",
    "Currency must be present in the pinned active ISO 4217 snapshot.",
    CURRENCY_SOURCE,
    validate_currency,
    &["$financial.currency"]
);
financial_rule!(
    COUNTRY_D,
    COUNTRY_R,
    "ISO20022-L2-COUNTRY-CODE",
    "Country code",
    "Country must be present in the pinned ISO 3166-1 alpha-2 snapshot.",
    COUNTRY_SOURCE,
    validate_country,
    &["$financial.country"]
);
financial_rule!(
    AMOUNT_FORMAT_D,
    AMOUNT_FORMAT_R,
    "ISO20022-L2-AMOUNT-FORMAT",
    "Amount syntax",
    "Amount must use non-negative plain decimal notation.",
    ISO_SOURCE,
    validate_money,
    &["$financial.amount_currency", "$financial.amount"]
);
financial_rule!(
    AMOUNT_PRECISION_D,
    AMOUNT_PRECISION_R,
    "ISO20022-L2-AMOUNT-PRECISION",
    "Currency amount precision",
    "Amount fractional precision must not exceed the currency minor units.",
    CURRENCY_SOURCE,
    validate_money,
    &["$financial.amount_currency", "$financial.amount"]
);
financial_rule!(
    AMOUNT_RANGE_D,
    AMOUNT_RANGE_R,
    "ISO20022-L2-AMOUNT-RANGE",
    "Amount digit bound",
    "Amount must not exceed the supported 18 total decimal digits.",
    ISO_SOURCE,
    validate_money,
    &["$financial.amount_currency", "$financial.amount"]
);
financial_rule!(
    DATE_FORMAT_D,
    DATE_FORMAT_R,
    "ISO20022-L2-DATE-TIME-FORMAT",
    "Date and time syntax",
    "Date and date-time values must use the supported ISO lexical form.",
    ISO_SOURCE,
    validate_temporal,
    &["$financial.date", "$financial.datetime"]
);
financial_rule!(
    DATE_RANGE_D,
    DATE_RANGE_R,
    "ISO20022-L2-DATE-TIME-RANGE",
    "Calendar range",
    "Date and date-time components must form a valid calendar value.",
    ISO_SOURCE,
    validate_temporal,
    &["$financial.date", "$financial.datetime"]
);
financial_rule!(
    ACCOUNT_D,
    ACCOUNT_R,
    "ISO20022-L2-ACCOUNT-ID",
    "Account identifier",
    "Account identifier must satisfy the bounded ISO 20022 text syntax.",
    ISO_SOURCE,
    validate_account,
    &["$financial.account"]
);
financial_rule!(
    CLEARING_SCHEME_D,
    CLEARING_SCHEME_R,
    "ISO20022-L2-CLEARING-SCHEME",
    "Clearing scheme",
    "Clearing scheme identifier must satisfy the bounded ISO 20022 text syntax.",
    ISO_SOURCE,
    validate_clearing,
    &["$financial.clearing.scheme"]
);
financial_rule!(
    CLEARING_MEMBER_D,
    CLEARING_MEMBER_R,
    "ISO20022-L2-CLEARING-MEMBER",
    "Clearing member",
    "Clearing member identifier must satisfy the bounded ISO 20022 text syntax.",
    ISO_SOURCE,
    validate_clearing,
    &["$financial.clearing.member"]
);

static FINANCIAL_RULES: &[&dyn Rule] = &[
    &IBAN_CHARSET_R,
    &IBAN_LENGTH_R,
    &IBAN_FORMAT_R,
    &IBAN_COUNTRY_R,
    &IBAN_BBAN_R,
    &IBAN_CHECKSUM_R,
    &BIC_LENGTH_R,
    &BIC_FORMAT_R,
    &BIC_COUNTRY_R,
    &LEI_LENGTH_R,
    &LEI_CHARSET_R,
    &LEI_CHECKSUM_R,
    &CURRENCY_R,
    &COUNTRY_R,
    &AMOUNT_FORMAT_R,
    &AMOUNT_PRECISION_R,
    &AMOUNT_RANGE_R,
    &DATE_FORMAT_R,
    &DATE_RANGE_R,
    &ACCOUNT_R,
    &CLEARING_SCHEME_R,
    &CLEARING_MEMBER_R,
];

/// The executable L2 scalar rules used for both validation and explanation.
pub fn financial_rules() -> &'static [&'static dyn Rule] {
    FINANCIAL_RULES
}

fn one(
    target: &ValidationTarget<'_>,
    path: &'static str,
    parse: impl FnOnce(&str) -> Result<(), ValueError>,
) -> Vec<(&'static str, ValueError)> {
    target
        .text(path)
        .and_then(|value| parse(value).err())
        .map(|error| vec![(path, error)])
        .unwrap_or_default()
}

fn validate_iban(target: &ValidationTarget<'_>) -> Vec<(&'static str, ValueError)> {
    one(target, "$financial.iban", |value| {
        Iban::parse(value).map(drop)
    })
}

fn validate_bic(target: &ValidationTarget<'_>) -> Vec<(&'static str, ValueError)> {
    one(target, "$financial.bic", |value| {
        Bic::parse(value).map(drop)
    })
}

fn validate_lei(target: &ValidationTarget<'_>) -> Vec<(&'static str, ValueError)> {
    one(target, "$financial.lei", |value| {
        Lei::parse(value).map(drop)
    })
}

fn validate_currency(target: &ValidationTarget<'_>) -> Vec<(&'static str, ValueError)> {
    one(target, "$financial.currency", |value| {
        Currency::parse(value).map(drop)
    })
}

fn validate_country(target: &ValidationTarget<'_>) -> Vec<(&'static str, ValueError)> {
    one(target, "$financial.country", |value| {
        CountryCode::parse(value).map(drop)
    })
}

fn validate_money(target: &ValidationTarget<'_>) -> Vec<(&'static str, ValueError)> {
    let Some(currency) = target
        .text("$financial.amount_currency")
        .or_else(|| target.text("$financial.currency"))
    else {
        return Vec::new();
    };
    let Some(amount) = target.text("$financial.amount") else {
        return Vec::new();
    };
    if Currency::parse(currency).is_err() {
        return Vec::new();
    }
    Money::parse(currency, amount)
        .err()
        .map(|error| vec![("$financial.amount", error)])
        .unwrap_or_default()
}

fn validate_temporal(target: &ValidationTarget<'_>) -> Vec<(&'static str, ValueError)> {
    let mut issues = one(target, "$financial.date", |value| {
        IsoDate::parse(value).map(drop)
    });
    issues.extend(one(target, "$financial.datetime", |value| {
        IsoDateTime::parse(value).map(drop)
    }));
    issues
}

fn validate_account(target: &ValidationTarget<'_>) -> Vec<(&'static str, ValueError)> {
    one(target, "$financial.account", |value| {
        AccountIdentifier::parse(value).map(drop)
    })
}

fn validate_clearing(target: &ValidationTarget<'_>) -> Vec<(&'static str, ValueError)> {
    let Some(scheme) = target.text("$financial.clearing.scheme") else {
        return Vec::new();
    };
    let Some(member) = target.text("$financial.clearing.member_id") else {
        return Vec::new();
    };
    ClearingIdentifier::parse(scheme, member)
        .err()
        .map(|error| {
            let path = if error.rule_id().as_str() == "ISO20022-L2-CLEARING-SCHEME" {
                "$financial.clearing.scheme"
            } else {
                "$financial.clearing.member_id"
            };
            vec![(path, error)]
        })
        .unwrap_or_default()
}