use super::field_utils::{parse_name_and_address, parse_party_identifier};
use super::swift_utils::{parse_bic, parse_swift_chars};
use crate::errors::ParseError;
use crate::traits::SwiftField;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Field52A {
#[serde(skip_serializing_if = "Option::is_none")]
pub party_identifier: Option<String>,
pub bic: String,
}
impl SwiftField for Field52A {
fn parse(input: &str) -> crate::Result<Self>
where
Self: Sized,
{
let lines: Vec<&str> = input.lines().collect();
if lines.is_empty() {
return Err(ParseError::InvalidFormat {
message: "Field 52A cannot be empty".to_string(),
});
}
let mut party_identifier = None;
let mut bic_line_idx = 0;
if let Some(party_id) = parse_party_identifier(lines[0])? {
party_identifier = Some(party_id);
bic_line_idx = 1;
}
if bic_line_idx >= lines.len() {
return Err(ParseError::InvalidFormat {
message: "Field 52A missing BIC code".to_string(),
});
}
let bic = parse_bic(lines[bic_line_idx])?;
Ok(Field52A {
party_identifier,
bic,
})
}
fn to_swift_string(&self) -> String {
let mut lines = Vec::new();
if let Some(ref id) = self.party_identifier {
lines.push(format!("/{}", id));
}
lines.push(self.bic.clone());
format!(":52A:{}", lines.join("\n"))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Field52B {
pub party_identifier: Option<String>,
pub location: Option<String>,
}
impl SwiftField for Field52B {
fn parse(input: &str) -> crate::Result<Self>
where
Self: Sized,
{
if input.is_empty() {
return Ok(Field52B {
party_identifier: None,
location: None,
});
}
let lines: Vec<&str> = input.lines().collect();
let mut party_identifier = None;
let mut location = None;
let mut current_idx = 0;
if !lines.is_empty() && lines[0].starts_with('/') {
let line = &lines[0][1..];
if let Some(slash_pos) = line.find('/') {
let code = &line[..slash_pos];
let id = &line[slash_pos + 1..];
if code.len() == 1
&& code.chars().all(|c| c.is_ascii_alphabetic())
&& id.len() <= 34
{
parse_swift_chars(id, "Field 52B party identifier")?;
party_identifier = Some(format!("{}/{}", code, id));
current_idx = 1;
}
} else if line.len() <= 34 {
parse_swift_chars(line, "Field 52B party identifier")?;
party_identifier = Some(line.to_string());
current_idx = 1;
}
}
if current_idx < lines.len() {
let loc = lines[current_idx];
if !loc.is_empty() && loc.len() <= 35 {
parse_swift_chars(loc, "Field 52B location")?;
location = Some(loc.to_string());
}
}
Ok(Field52B {
party_identifier,
location,
})
}
fn to_swift_string(&self) -> String {
let mut result = Vec::new();
if let Some(ref id) = self.party_identifier {
result.push(format!("/{}", id));
}
if let Some(ref loc) = self.location {
result.push(loc.clone());
}
format!(":52B:{}", result.join("\n"))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Field52C {
pub party_identifier: String,
}
impl SwiftField for Field52C {
fn parse(input: &str) -> crate::Result<Self>
where
Self: Sized,
{
if !input.starts_with('/') {
return Err(ParseError::InvalidFormat {
message: "Field 52C must start with '/'".to_string(),
});
}
let identifier = &input[1..];
if identifier.is_empty() || identifier.len() > 34 {
return Err(ParseError::InvalidFormat {
message: "Field 52C party identifier must be 1-34 characters".to_string(),
});
}
parse_swift_chars(identifier, "Field 52C party identifier")?;
Ok(Field52C {
party_identifier: identifier.to_string(),
})
}
fn to_swift_string(&self) -> String {
format!(":52C:/{}", self.party_identifier)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Field52D {
pub party_identifier: Option<String>,
pub name_and_address: Vec<String>,
}
impl SwiftField for Field52D {
fn parse(input: &str) -> crate::Result<Self>
where
Self: Sized,
{
let lines: Vec<&str> = input.lines().collect();
if lines.is_empty() {
return Err(ParseError::InvalidFormat {
message: "Field 52D must have at least one line".to_string(),
});
}
let mut party_identifier = None;
let mut start_idx = 0;
if lines[0].starts_with('/') {
let line = &lines[0][1..];
if let Some(slash_pos) = line.find('/') {
let code = &line[..slash_pos];
let id = &line[slash_pos + 1..];
if code.len() == 1
&& code.chars().all(|c| c.is_ascii_alphabetic())
&& id.len() <= 34
{
parse_swift_chars(id, "Field 52D party identifier")?;
party_identifier = Some(format!("{}/{}", code, id));
start_idx = 1;
}
} else if line.len() <= 34 {
parse_swift_chars(line, "Field 52D party identifier")?;
party_identifier = Some(line.to_string());
start_idx = 1;
}
}
let name_and_address = parse_name_and_address(&lines, start_idx, "Field 52D")?;
Ok(Field52D {
party_identifier,
name_and_address,
})
}
fn to_swift_string(&self) -> String {
let mut lines = Vec::new();
if let Some(ref id) = self.party_identifier {
lines.push(format!("/{}", id));
}
for line in &self.name_and_address {
lines.push(line.clone());
}
format!(":52D:{}", lines.join("\n"))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Field52AccountServicingInstitution {
#[serde(rename = "52A")]
A(Field52A),
#[serde(rename = "52C")]
C(Field52C),
}
impl SwiftField for Field52AccountServicingInstitution {
fn parse(input: &str) -> crate::Result<Self>
where
Self: Sized,
{
if let Ok(field) = Field52A::parse(input) {
return Ok(Field52AccountServicingInstitution::A(field));
}
if let Ok(field) = Field52C::parse(input) {
return Ok(Field52AccountServicingInstitution::C(field));
}
Err(ParseError::InvalidFormat {
message: "Field 52 Account Servicing Institution could not be parsed as option A or C"
.to_string(),
})
}
fn parse_with_variant(
value: &str,
variant: Option<&str>,
_field_tag: Option<&str>,
) -> crate::Result<Self>
where
Self: Sized,
{
match variant {
Some("A") => {
let field = Field52A::parse(value)?;
Ok(Field52AccountServicingInstitution::A(field))
}
Some("C") => {
let field = Field52C::parse(value)?;
Ok(Field52AccountServicingInstitution::C(field))
}
_ => {
Self::parse(value)
}
}
}
fn to_swift_string(&self) -> String {
match self {
Field52AccountServicingInstitution::A(field) => field.to_swift_string(),
Field52AccountServicingInstitution::C(field) => field.to_swift_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Field52OrderingInstitution {
#[serde(rename = "52A")]
A(Field52A),
#[serde(rename = "52D")]
D(Field52D),
}
impl SwiftField for Field52OrderingInstitution {
fn parse(input: &str) -> crate::Result<Self>
where
Self: Sized,
{
if let Ok(field) = Field52A::parse(input) {
return Ok(Field52OrderingInstitution::A(field));
}
if let Ok(field) = Field52D::parse(input) {
return Ok(Field52OrderingInstitution::D(field));
}
Err(ParseError::InvalidFormat {
message: "Field 52 Ordering Institution could not be parsed as option A or D"
.to_string(),
})
}
fn parse_with_variant(
value: &str,
variant: Option<&str>,
_field_tag: Option<&str>,
) -> crate::Result<Self>
where
Self: Sized,
{
match variant {
Some("A") => {
let field = Field52A::parse(value)?;
Ok(Field52OrderingInstitution::A(field))
}
Some("D") => {
let field = Field52D::parse(value)?;
Ok(Field52OrderingInstitution::D(field))
}
_ => {
Self::parse(value)
}
}
}
fn to_swift_string(&self) -> String {
match self {
Field52OrderingInstitution::A(field) => field.to_swift_string(),
Field52OrderingInstitution::D(field) => field.to_swift_string(),
}
}
fn get_variant_tag(&self) -> Option<&'static str> {
match self {
Field52OrderingInstitution::A(_) => Some("A"),
Field52OrderingInstitution::D(_) => Some("D"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Field52CreditorBank {
#[serde(rename = "52A")]
A(Field52A),
#[serde(rename = "52C")]
C(Field52C),
#[serde(rename = "52D")]
D(Field52D),
}
impl SwiftField for Field52CreditorBank {
fn parse(input: &str) -> crate::Result<Self>
where
Self: Sized,
{
if let Ok(field) = Field52A::parse(input) {
return Ok(Field52CreditorBank::A(field));
}
if input.starts_with('/')
&& !input.contains('\n')
&& let Ok(field) = Field52C::parse(input)
{
return Ok(Field52CreditorBank::C(field));
}
if let Ok(field) = Field52D::parse(input) {
return Ok(Field52CreditorBank::D(field));
}
Err(ParseError::InvalidFormat {
message: "Field 52 Creditor Bank could not be parsed as option A, C or D".to_string(),
})
}
fn parse_with_variant(
value: &str,
variant: Option<&str>,
_field_tag: Option<&str>,
) -> crate::Result<Self>
where
Self: Sized,
{
match variant {
Some("A") => {
let field = Field52A::parse(value)?;
Ok(Field52CreditorBank::A(field))
}
Some("C") => {
let field = Field52C::parse(value)?;
Ok(Field52CreditorBank::C(field))
}
Some("D") => {
let field = Field52D::parse(value)?;
Ok(Field52CreditorBank::D(field))
}
_ => {
Self::parse(value)
}
}
}
fn to_swift_string(&self) -> String {
match self {
Field52CreditorBank::A(field) => field.to_swift_string(),
Field52CreditorBank::C(field) => field.to_swift_string(),
Field52CreditorBank::D(field) => field.to_swift_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Field52DrawerBank {
#[serde(rename = "52A")]
A(Field52A),
#[serde(rename = "52B")]
B(Field52B),
#[serde(rename = "52D")]
D(Field52D),
}
impl SwiftField for Field52DrawerBank {
fn parse(input: &str) -> crate::Result<Self>
where
Self: Sized,
{
if let Ok(field) = Field52A::parse(input) {
return Ok(Field52DrawerBank::A(field));
}
if let Ok(field) = Field52B::parse(input) {
return Ok(Field52DrawerBank::B(field));
}
if let Ok(field) = Field52D::parse(input) {
return Ok(Field52DrawerBank::D(field));
}
Err(ParseError::InvalidFormat {
message: "Field 52 Drawer Bank could not be parsed as option A, B or D".to_string(),
})
}
fn parse_with_variant(
value: &str,
variant: Option<&str>,
_field_tag: Option<&str>,
) -> crate::Result<Self>
where
Self: Sized,
{
match variant {
Some("A") => {
let field = Field52A::parse(value)?;
Ok(Field52DrawerBank::A(field))
}
Some("B") => {
let field = Field52B::parse(value)?;
Ok(Field52DrawerBank::B(field))
}
Some("D") => {
let field = Field52D::parse(value)?;
Ok(Field52DrawerBank::D(field))
}
_ => {
Self::parse(value)
}
}
}
fn to_swift_string(&self) -> String {
match self {
Field52DrawerBank::A(field) => field.to_swift_string(),
Field52DrawerBank::B(field) => field.to_swift_string(),
Field52DrawerBank::D(field) => field.to_swift_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field52a() {
let field = Field52A::parse("/C/US123456\nDEUTDEFF").unwrap();
assert_eq!(field.party_identifier, Some("C/US123456".to_string()));
assert_eq!(field.bic, "DEUTDEFF");
let field = Field52A::parse("CHASUS33XXX").unwrap();
assert_eq!(field.party_identifier, None);
assert_eq!(field.bic, "CHASUS33XXX");
}
#[test]
fn test_field52b() {
let field = Field52B::parse("/A/12345\nNEW YORK").unwrap();
assert_eq!(field.party_identifier, Some("A/12345".to_string()));
assert_eq!(field.location, Some("NEW YORK".to_string()));
let field = Field52B::parse("").unwrap();
assert_eq!(field.party_identifier, None);
assert_eq!(field.location, None);
}
#[test]
fn test_field52c() {
let field = Field52C::parse("/UKCLEARING123").unwrap();
assert_eq!(field.party_identifier, "UKCLEARING123");
assert_eq!(field.to_swift_string(), ":52C:/UKCLEARING123");
}
#[test]
fn test_field52d() {
let field = Field52D::parse("/D/DE123456\nDEUTSCHE BANK\nFRANKFURT").unwrap();
assert_eq!(field.party_identifier, Some("D/DE123456".to_string()));
assert_eq!(field.name_and_address.len(), 2);
assert_eq!(field.name_and_address[0], "DEUTSCHE BANK");
let field = Field52D::parse("ACME BANK\nLONDON").unwrap();
assert_eq!(field.party_identifier, None);
assert_eq!(field.name_and_address.len(), 2);
}
#[test]
fn test_field52_invalid() {
assert!(Field52A::parse("INVALID").is_err());
assert!(Field52C::parse("NOSLASH").is_err());
assert!(Field52D::parse("LINE1\nLINE2\nLINE3\nLINE4\nLINE5").is_err());
}
}