1use serde::{Deserialize, Serialize};
59use std::collections::HashMap;
60use std::fmt::Debug;
61
62pub mod errors;
63pub mod fields;
64pub mod headers;
65pub mod messages;
66pub mod parser;
67
68pub use errors::{ParseError, Result, ValidationError};
70pub use headers::{ApplicationHeader, BasicHeader, Trailer, UserHeader};
71pub use parser::SwiftParser;
72
73pub use swift_mt_message_macros::{SwiftField, SwiftMessage, swift_serde};
75
76pub type SwiftResult<T> = std::result::Result<T, crate::errors::ParseError>;
78
79pub trait SwiftField: Serialize + for<'de> Deserialize<'de> + Clone + std::fmt::Debug {
81 fn parse(value: &str) -> Result<Self>
83 where
84 Self: Sized;
85
86 fn to_swift_string(&self) -> String;
88
89 fn validate(&self) -> ValidationResult;
91
92 fn format_spec() -> &'static str;
94}
95
96pub trait SwiftMessageBody: Debug + Clone + Send + Sync + Serialize {
98 fn message_type() -> &'static str;
100
101 fn from_fields(fields: HashMap<String, Vec<String>>) -> SwiftResult<Self>
103 where
104 Self: Sized;
105
106 fn to_fields(&self) -> HashMap<String, Vec<String>>;
108
109 fn required_fields() -> Vec<&'static str>;
111
112 fn optional_fields() -> Vec<&'static str>;
114}
115
116#[derive(Debug, Clone, Serialize)]
118pub struct SwiftMessage<T: SwiftMessageBody> {
119 pub basic_header: BasicHeader,
121
122 pub application_header: ApplicationHeader,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub user_header: Option<UserHeader>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub trailer: Option<Trailer>,
132
133 pub blocks: RawBlocks,
135
136 pub message_type: String,
138
139 pub field_order: Vec<String>,
141
142 pub fields: T,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, Default)]
148pub struct RawBlocks {
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub block1: Option<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub block2: Option<String>,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub block3: Option<String>,
155 pub block4: String,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub block5: Option<String>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ValidationResult {
163 pub is_valid: bool,
164 pub errors: Vec<ValidationError>,
165 pub warnings: Vec<String>,
166}
167
168impl ValidationResult {
169 pub fn valid() -> Self {
170 Self {
171 is_valid: true,
172 errors: Vec::new(),
173 warnings: Vec::new(),
174 }
175 }
176
177 pub fn with_error(error: ValidationError) -> Self {
178 Self {
179 is_valid: false,
180 errors: vec![error],
181 warnings: Vec::new(),
182 }
183 }
184
185 pub fn with_errors(errors: Vec<ValidationError>) -> Self {
186 Self {
187 is_valid: errors.is_empty(),
188 errors,
189 warnings: Vec::new(),
190 }
191 }
192}
193
194pub mod common {
196 use crate::ValidationResult;
197 use crate::errors::{ParseError, ValidationError};
198 use serde::{Deserialize, Serialize};
199
200 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
213 pub struct BIC {
214 #[serde(rename = "bic")]
216 pub value: String,
217 }
218
219 impl BIC {
220 pub fn new(value: impl Into<String>) -> Result<Self, ParseError> {
238 let value = value.into().to_uppercase();
239 let bic = Self { value };
240 bic.validate_strict()?;
241 Ok(bic)
242 }
243
244 pub fn new_unchecked(value: impl Into<String>) -> Self {
246 Self {
247 value: value.into().to_uppercase(),
248 }
249 }
250
251 pub fn parse(value: &str, field_tag: Option<&str>) -> Result<Self, ParseError> {
253 let value = value.trim().to_uppercase();
254
255 if value.is_empty() {
256 return Err(ParseError::InvalidFieldFormat {
257 field_tag: field_tag.unwrap_or("BIC").to_string(),
258 message: "BIC cannot be empty".to_string(),
259 });
260 }
261
262 let bic = Self::new_unchecked(value);
263 bic.validate_with_context(field_tag)?;
264 Ok(bic)
265 }
266
267 fn validate_strict(&self) -> Result<(), ParseError> {
269 self.validate_with_context(None)
270 }
271
272 fn validate_with_context(&self, field_tag: Option<&str>) -> Result<(), ParseError> {
274 let field_name = field_tag.unwrap_or("BIC");
275
276 if self.value.len() != 8 && self.value.len() != 11 {
277 return Err(ParseError::InvalidFieldFormat {
278 field_tag: field_name.to_string(),
279 message: "BIC must be 8 or 11 characters".to_string(),
280 });
281 }
282
283 let bank_code = &self.value[0..4];
284 let country_code = &self.value[4..6];
285 let location_code = &self.value[6..8];
286
287 if !bank_code.chars().all(|c| c.is_ascii_alphabetic()) {
289 return Err(ParseError::InvalidFieldFormat {
290 field_tag: field_name.to_string(),
291 message: "BIC bank code (first 4 characters) must be alphabetic".to_string(),
292 });
293 }
294
295 if !country_code.chars().all(|c| c.is_ascii_alphabetic()) {
297 return Err(ParseError::InvalidFieldFormat {
298 field_tag: field_name.to_string(),
299 message: "BIC country code (characters 5-6) must be alphabetic".to_string(),
300 });
301 }
302
303 if !location_code.chars().all(|c| c.is_ascii_alphanumeric()) {
305 return Err(ParseError::InvalidFieldFormat {
306 field_tag: field_name.to_string(),
307 message: "BIC location code (characters 7-8) must be alphanumeric".to_string(),
308 });
309 }
310
311 if self.value.len() == 11 {
313 let branch_code = &self.value[8..11];
314 if !branch_code.chars().all(|c| c.is_ascii_alphanumeric()) {
315 return Err(ParseError::InvalidFieldFormat {
316 field_tag: field_name.to_string(),
317 message: "BIC branch code (characters 9-11) must be alphanumeric"
318 .to_string(),
319 });
320 }
321 }
322
323 Ok(())
324 }
325
326 pub fn validate(&self) -> ValidationResult {
328 match self.validate_strict() {
329 Ok(()) => ValidationResult::valid(),
330 Err(ParseError::InvalidFieldFormat { message, .. }) => {
331 ValidationResult::with_error(ValidationError::FormatValidation {
332 field_tag: "BIC".to_string(),
333 message,
334 })
335 }
336 Err(e) => ValidationResult::with_error(ValidationError::FormatValidation {
337 field_tag: "BIC".to_string(),
338 message: e.to_string(),
339 }),
340 }
341 }
342
343 pub fn value(&self) -> &str {
345 &self.value
346 }
347
348 pub fn bank_code(&self) -> &str {
350 &self.value[0..4]
351 }
352
353 pub fn country_code(&self) -> &str {
355 &self.value[4..6]
356 }
357
358 pub fn location_code(&self) -> &str {
360 &self.value[6..8]
361 }
362
363 pub fn branch_code(&self) -> Option<&str> {
365 if self.value.len() == 11 {
366 Some(&self.value[8..11])
367 } else {
368 None
369 }
370 }
371
372 pub fn is_full_bic(&self) -> bool {
374 self.value.len() == 11
375 }
376
377 pub fn is_major_financial_center(&self) -> bool {
379 let country = self.country_code();
380 let location = self.location_code();
381
382 matches!(
383 (country, location),
384 ("US", "33") | ("GB", "22") | ("DE", "FF") | ("JP", "22") | ("HK", "HK") | ("SG", "SG") | ("FR", "PP") | ("CH", "ZZ") | ("CA", "TT") | ("AU", "MM") )
395 }
396
397 pub fn is_retail_bank(&self) -> bool {
399 let bank_code = self.bank_code();
400
401 matches!(
403 bank_code,
404 "CHAS" | "BOFA" | "WELL" | "CITI" | "HSBC" | "BARC" | "LLOY" | "NATS" | "DEUT" | "COMM" | "BNPA" | "CRED" | "UBSW" | "CRSU" | "ROYA" | "TDOM" | "ANZI" | "CTBA" | "WEST" | "MUFG" | "SMBC" | "MIZB" )
427 }
428
429 pub fn supports_real_time_payments(&self) -> bool {
431 let country = self.country_code();
432
433 matches!(
435 country,
436 "US" | "GB" | "DE" | "NL" | "SE" | "DK" | "AU" | "SG" | "IN" | "BR" | "MX" | "JP" | "KR" | "CN" )
451 }
452
453 pub fn regulatory_jurisdiction(&self) -> &'static str {
455 match self.country_code() {
456 "US" => "Federal Reserve / OCC / FDIC",
457 "GB" => "Bank of England / PRA / FCA",
458 "DE" => "BaFin / ECB",
459 "FR" => "ACPR / ECB",
460 "JP" => "JFSA / Bank of Japan",
461 "CH" => "FINMA / SNB",
462 "CA" => "OSFI / Bank of Canada",
463 "AU" => "APRA / RBA",
464 "SG" => "MAS",
465 "HK" => "HKMA",
466 "CN" => "PBOC / CBIRC",
467 "IN" => "RBI",
468 "BR" => "Central Bank of Brazil",
469 "MX" => "CNBV / Banxico",
470 _ => "Other National Authority",
471 }
472 }
473
474 pub fn description(&self) -> String {
476 format!(
477 "Bank: {} | Country: {} | Location: {} | Branch: {}",
478 self.bank_code(),
479 self.country_code(),
480 self.location_code(),
481 self.branch_code().unwrap_or("XXX (Head Office)")
482 )
483 }
484 }
485
486 impl std::fmt::Display for BIC {
487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488 write!(f, "{}", self.value)
489 }
490 }
491
492 impl std::str::FromStr for BIC {
493 type Err = ParseError;
494
495 fn from_str(s: &str) -> Result<Self, Self::Err> {
496 Self::parse(s, None)
497 }
498 }
499
500 impl From<BIC> for String {
501 fn from(bic: BIC) -> String {
502 bic.value
503 }
504 }
505
506 impl AsRef<str> for BIC {
507 fn as_ref(&self) -> &str {
508 &self.value
509 }
510 }
511
512 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
514 pub struct Currency {
515 pub code: String,
516 }
517
518 impl Currency {
519 pub fn new(code: String) -> Self {
520 Self {
521 code: code.to_uppercase(),
522 }
523 }
524 }
525
526 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
528 pub struct Amount {
529 pub value: String,
530 pub decimal_places: u8,
531 }
532
533 impl Amount {
534 pub fn new(value: String) -> Self {
535 let decimal_places = if value.contains(',') {
536 value.split(',').nth(1).map(|s| s.len() as u8).unwrap_or(0)
537 } else {
538 0
539 };
540
541 Self {
542 value,
543 decimal_places,
544 }
545 }
546
547 pub fn to_decimal(&self) -> Result<f64, std::num::ParseFloatError> {
548 self.value.replace(',', ".").parse()
549 }
550 }
551
552 #[cfg(test)]
553 mod tests {
554 use super::*;
555
556 #[test]
557 fn test_bic_creation() {
558 let bic = BIC::new("CHASUS33XXX").unwrap();
559 assert_eq!(bic.value(), "CHASUS33XXX");
560 assert_eq!(bic.bank_code(), "CHAS");
561 assert_eq!(bic.country_code(), "US");
562 assert_eq!(bic.location_code(), "33");
563 assert_eq!(bic.branch_code(), Some("XXX"));
564 assert!(bic.is_full_bic());
565 }
566
567 #[test]
568 fn test_bic_short_format() {
569 let bic = BIC::new("DEUTDEFF").unwrap();
570 assert_eq!(bic.value(), "DEUTDEFF");
571 assert_eq!(bic.bank_code(), "DEUT");
572 assert_eq!(bic.country_code(), "DE");
573 assert_eq!(bic.location_code(), "FF");
574 assert_eq!(bic.branch_code(), None);
575 assert!(!bic.is_full_bic());
576 }
577
578 #[test]
579 fn test_bic_validation_errors() {
580 assert!(BIC::new("CHAS").is_err());
582
583 assert!(BIC::new("CHASUS33XXXX").is_err());
585
586 assert!(BIC::new("1234US33").is_err());
588
589 assert!(BIC::new("CHAS22XX").is_err());
591
592 assert!(BIC::new("CHASUS@#").is_err());
594 }
595
596 #[test]
597 fn test_bic_case_normalization() {
598 let bic = BIC::new("chasus33xxx").unwrap();
599 assert_eq!(bic.value(), "CHASUS33XXX");
600 }
601
602 #[test]
603 fn test_bic_utilities() {
604 let bic = BIC::new("CHASUS33XXX").unwrap();
605 assert!(bic.is_major_financial_center());
606 assert!(bic.is_retail_bank());
607 assert!(bic.supports_real_time_payments());
608 assert_eq!(
609 bic.regulatory_jurisdiction(),
610 "Federal Reserve / OCC / FDIC"
611 );
612 }
613
614 #[test]
615 fn test_bic_display_and_description() {
616 let bic = BIC::new("CHASUS33XXX").unwrap();
617 assert_eq!(bic.to_string(), "CHASUS33XXX");
618 assert!(bic.description().contains("CHAS"));
619 assert!(bic.description().contains("US"));
620 }
621
622 #[test]
623 fn test_bic_from_str() {
624 let bic: BIC = "CHASUS33XXX".parse().unwrap();
625 assert_eq!(bic.value(), "CHASUS33XXX");
626 }
627 }
628}
629
630#[derive(Debug, Clone, Serialize)]
632#[serde(tag = "message_type")]
633pub enum ParsedSwiftMessage {
634 #[serde(rename = "103")]
635 MT103(Box<SwiftMessage<messages::MT103>>),
636 #[serde(rename = "202")]
637 MT202(Box<SwiftMessage<messages::MT202>>),
638}
639
640impl ParsedSwiftMessage {
641 pub fn message_type(&self) -> &'static str {
643 match self {
644 ParsedSwiftMessage::MT103(_) => "103",
645 ParsedSwiftMessage::MT202(_) => "202",
646 }
647 }
648
649 pub fn as_mt103(&self) -> Option<&SwiftMessage<messages::MT103>> {
651 match self {
652 ParsedSwiftMessage::MT103(msg) => Some(msg),
653 _ => None,
654 }
655 }
656
657 pub fn as_mt202(&self) -> Option<&SwiftMessage<messages::MT202>> {
658 match self {
659 ParsedSwiftMessage::MT202(msg) => Some(msg),
660 _ => None,
661 }
662 }
663
664 pub fn into_mt103(self) -> Option<SwiftMessage<messages::MT103>> {
666 match self {
667 ParsedSwiftMessage::MT103(msg) => Some(*msg),
668 _ => None,
669 }
670 }
671
672 pub fn into_mt202(self) -> Option<SwiftMessage<messages::MT202>> {
673 match self {
674 ParsedSwiftMessage::MT202(msg) => Some(*msg),
675 _ => None,
676 }
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683 use crate::messages::mt103::MT103;
684
685 #[test]
686 fn test_full_mt103_parsing() {
687 let raw_message = r#"{1:F01BNPAFRPPXXX0000000000}{2:O1031234240101DEUTDEFFXXXX12345678952401011234N}{3:{103:EBA}}{4:
688:20:FT21001234567890
689:23B:CRED
690:32A:240101USD1000,00
691:50K:/1234567890
692ACME CORPORATION
693123 MAIN STREET
694NEW YORK NY 10001
695:52A:BNPAFRPPXXX
696:57A:DEUTDEFFXXX
697:59:/DE89370400440532013000
698MUELLER GMBH
699HAUPTSTRASSE 1
70010115 BERLIN
701:70:PAYMENT FOR INVOICE 12345
702:71A:OUR
703-}"#;
704
705 let result = SwiftParser::parse::<MT103>(raw_message);
706 assert!(result.is_ok(), "Parsing should succeed: {:?}", result.err());
707
708 let parsed = result.unwrap();
709 assert_eq!(parsed.message_type, "103");
710
711 let json = serde_json::to_string_pretty(&parsed);
713 assert!(json.is_ok(), "JSON serialization should work");
714 println!("Parsed MT103 JSON:\n{}", json.unwrap());
715 }
716
717 #[test]
718 fn test_field_parsing() {
719 use crate::fields::field20::Field20;
720
721 let result = Field20::parse(":20:FT21001234567890");
722 assert!(result.is_ok());
723
724 let field = result.unwrap();
725 assert_eq!(field.to_swift_string(), ":20:FT21001234567890");
726 }
727}