swift-mt-message 3.1.5

A fast, type-safe Rust implementation of SWIFT MT message parsing with comprehensive field support, derive macros, and validation.
Documentation
use super::field_utils::{parse_name_and_address, parse_party_identifier};
use super::swift_utils::{parse_bic, parse_max_length};
use crate::errors::ParseError;
use crate::traits::SwiftField;
use serde::{Deserialize, Serialize};

/// **Field 55A: Third Reimbursement Institution (BIC with Party Identifier)**
///
/// Specifies the receiver's branch in complex reimbursement chains.
/// Format: [/1!a][/34x] + BIC (8 or 11 chars)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Field55A {
    /// Optional party identifier (max 34 chars, specialized account)
    pub party_identifier: Option<String>,

    /// BIC code (8 or 11 chars)
    pub bic: String,
}

impl SwiftField for Field55A {
    fn parse(input: &str) -> crate::Result<Self>
    where
        Self: Sized,
    {
        let lines: Vec<&str> = input.split('\n').collect();

        if lines.is_empty() {
            return Err(ParseError::InvalidFormat {
                message: "Field 55A requires input".to_string(),
            });
        }

        let mut line_idx = 0;
        let mut party_identifier = None;

        // Check for optional party identifier on first line
        if let Some(party_id) = parse_party_identifier(lines[0])? {
            party_identifier = Some(format!("/{}", party_id));
            line_idx = 1;
        }

        // Ensure we have a BIC line
        if line_idx >= lines.len() {
            return Err(ParseError::InvalidFormat {
                message: "Field 55A requires BIC code after party identifier".to_string(),
            });
        }

        // Parse BIC code
        let bic = parse_bic(lines[line_idx])?;

        Ok(Field55A {
            party_identifier,
            bic,
        })
    }

    fn to_swift_string(&self) -> String {
        let mut result = String::from(":55A:");
        if let Some(ref party_id) = self.party_identifier {
            result.push_str(party_id);
            result.push('\n');
        }
        result.push_str(&self.bic);
        result
    }
}

/// **Field 55B: Third Reimbursement Institution (Party Identifier with Location)**
///
/// Domestic third institution routing with party identifier and location.
/// Format: [/1!a][/34x] + [35x]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Field55B {
    /// Optional party identifier (max 34 chars)
    pub party_identifier: Option<String>,

    /// Location (max 35 chars)
    pub location: Option<String>,
}

impl SwiftField for Field55B {
    fn parse(input: &str) -> crate::Result<Self>
    where
        Self: Sized,
    {
        if input.is_empty() {
            return Ok(Field55B {
                party_identifier: None,
                location: None,
            });
        }

        let lines: Vec<&str> = input.split('\n').collect();
        let mut party_identifier = None;
        let mut location = None;
        let mut line_idx = 0;

        // Check for party identifier on first line
        if !lines.is_empty() && lines[0].starts_with('/') {
            party_identifier = Some(lines[0].to_string());
            line_idx = 1;
        }

        // Remaining line is location
        if line_idx < lines.len() && !lines[line_idx].is_empty() {
            location = Some(parse_max_length(lines[line_idx], 35, "Field55B location")?);
        }

        Ok(Field55B {
            party_identifier,
            location,
        })
    }

    fn to_swift_string(&self) -> String {
        let mut result = String::from(":55B:");
        if let Some(ref party_id) = self.party_identifier {
            result.push_str(party_id);
            if self.location.is_some() {
                result.push('\n');
            }
        }
        if let Some(ref loc) = self.location {
            result.push_str(loc);
        }
        result
    }
}

/// **Field 55D: Third Reimbursement Institution (Party Identifier with Name and Address)**
///
/// Detailed third institution identification with name and address.
/// Format: [/1!a][/34x] + 4*35x
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Field55D {
    /// Optional party identifier (max 34 chars)
    pub party_identifier: Option<String>,

    /// Name and address (max 4 lines, 35 chars each)
    pub name_and_address: Vec<String>,
}

impl SwiftField for Field55D {
    fn parse(input: &str) -> crate::Result<Self>
    where
        Self: Sized,
    {
        let lines: Vec<&str> = input.split('\n').collect();

        if lines.is_empty() {
            return Err(ParseError::InvalidFormat {
                message: "Field 55D requires at least one line".to_string(),
            });
        }

        let mut party_identifier = None;
        let mut start_idx = 0;

        // Check for party identifier on first line
        if let Some(party_id) = parse_party_identifier(lines[0])? {
            party_identifier = Some(format!("/{}", party_id));
            start_idx = 1;
        }

        // Parse remaining lines as name and address
        let name_and_address = parse_name_and_address(&lines, start_idx, "Field55D")?;

        Ok(Field55D {
            party_identifier,
            name_and_address,
        })
    }

    fn to_swift_string(&self) -> String {
        let mut result = String::from(":55D:");
        if let Some(ref party_id) = self.party_identifier {
            result.push_str(party_id);
            result.push('\n');
        }
        for (i, line) in self.name_and_address.iter().enumerate() {
            if i > 0 {
                result.push('\n');
            }
            result.push_str(line);
        }
        result
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub enum Field55ThirdReimbursementInstitution {
    #[serde(rename = "55A")]
    A(Field55A),
    #[serde(rename = "55B")]
    B(Field55B),
    #[serde(rename = "55D")]
    D(Field55D),
}

impl SwiftField for Field55ThirdReimbursementInstitution {
    fn parse(input: &str) -> crate::Result<Self>
    where
        Self: Sized,
    {
        // Try parsing as each variant
        // A: Has BIC code (8 or 11 uppercase letters/digits)
        // B: Has optional party identifier and/or location
        // D: Has party identifier and/or multiple lines of name/address

        let lines: Vec<&str> = input.split('\n').collect();
        let last_line = lines.last().unwrap_or(&"");

        // Check if last line looks like a BIC code
        if (8..=11).contains(&last_line.len())
            && last_line
                .chars()
                .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
        {
            // Try parsing as Field55A
            if let Ok(field) = Field55A::parse(input) {
                return Ok(Field55ThirdReimbursementInstitution::A(field));
            }
        }

        // Check for multiple lines suggesting D format
        if lines.len() > 2 || (lines.len() == 2 && !lines[0].starts_with('/')) {
            // Try parsing as Field55D (multiple lines of name/address)
            if let Ok(field) = Field55D::parse(input) {
                return Ok(Field55ThirdReimbursementInstitution::D(field));
            }
        }

        // Try parsing as Field55B (simpler format)
        if let Ok(field) = Field55B::parse(input) {
            return Ok(Field55ThirdReimbursementInstitution::B(field));
        }

        // If all fail, try in order
        if let Ok(field) = Field55A::parse(input) {
            return Ok(Field55ThirdReimbursementInstitution::A(field));
        }
        if let Ok(field) = Field55D::parse(input) {
            return Ok(Field55ThirdReimbursementInstitution::D(field));
        }

        Err(ParseError::InvalidFormat {
            message: "Field 55 could not be parsed as any valid 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 = Field55A::parse(value)?;
                Ok(Field55ThirdReimbursementInstitution::A(field))
            }
            Some("B") => {
                let field = Field55B::parse(value)?;
                Ok(Field55ThirdReimbursementInstitution::B(field))
            }
            Some("D") => {
                let field = Field55D::parse(value)?;
                Ok(Field55ThirdReimbursementInstitution::D(field))
            }
            _ => {
                // No variant specified, fall back to default parse behavior
                Self::parse(value)
            }
        }
    }

    fn to_swift_string(&self) -> String {
        match self {
            Field55ThirdReimbursementInstitution::A(field) => field.to_swift_string(),
            Field55ThirdReimbursementInstitution::B(field) => field.to_swift_string(),
            Field55ThirdReimbursementInstitution::D(field) => field.to_swift_string(),
        }
    }

    fn get_variant_tag(&self) -> Option<&'static str> {
        match self {
            Field55ThirdReimbursementInstitution::A(_) => Some("A"),
            Field55ThirdReimbursementInstitution::B(_) => Some("B"),
            Field55ThirdReimbursementInstitution::D(_) => Some("D"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_field55a_valid() {
        // Without party identifier
        let field = Field55A::parse("BNPAFRPPXXX").unwrap();
        assert_eq!(field.bic, "BNPAFRPPXXX");
        assert_eq!(field.party_identifier, None);
        assert_eq!(field.to_swift_string(), ":55A:BNPAFRPPXXX");

        // With party identifier
        let field = Field55A::parse("/E/55566677\nBNPAFRPP").unwrap();
        assert_eq!(field.bic, "BNPAFRPP");
        assert_eq!(field.party_identifier, Some("/E/55566677".to_string()));
        assert_eq!(field.to_swift_string(), ":55A:/E/55566677\nBNPAFRPP");
    }

    #[test]
    fn test_field55b_valid() {
        // Only location
        let field = Field55B::parse("PARIS BRANCH").unwrap();
        assert_eq!(field.location, Some("PARIS BRANCH".to_string()));
        assert_eq!(field.party_identifier, None);

        // With party identifier
        let field = Field55B::parse("/F/99887766\nPARIS").unwrap();
        assert_eq!(field.party_identifier, Some("/F/99887766".to_string()));
        assert_eq!(field.location, Some("PARIS".to_string()));

        // Empty
        let field = Field55B::parse("").unwrap();
        assert_eq!(field.party_identifier, None);
        assert_eq!(field.location, None);
    }

    #[test]
    fn test_field55d_valid() {
        // With party identifier and name/address
        let field =
            Field55D::parse("/E/55566677\nTHIRD BANK\n789 THIRD ST\nPARIS\nFRANCE").unwrap();
        assert_eq!(field.party_identifier, Some("/E/55566677".to_string()));
        assert_eq!(field.name_and_address.len(), 4);
        assert_eq!(field.name_and_address[0], "THIRD BANK");
        assert_eq!(field.name_and_address[3], "FRANCE");

        // Without party identifier
        let field = Field55D::parse("THIRD BANK\nPARIS").unwrap();
        assert_eq!(field.party_identifier, None);
        assert_eq!(field.name_and_address.len(), 2);
    }

    #[test]
    fn test_field55_enum() {
        // Parse as A
        let field = Field55ThirdReimbursementInstitution::parse("BNPAFRPPXXX").unwrap();
        assert!(matches!(field, Field55ThirdReimbursementInstitution::A(_)));

        // Parse as B
        let field = Field55ThirdReimbursementInstitution::parse("PARIS BRANCH").unwrap();
        assert!(matches!(field, Field55ThirdReimbursementInstitution::B(_)));

        // Parse as D
        let field =
            Field55ThirdReimbursementInstitution::parse("BANK NAME\nADDRESS LINE 1\nCITY").unwrap();
        assert!(matches!(field, Field55ThirdReimbursementInstitution::D(_)));
    }
}