use crate::errors::{ParseError, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct BasicHeader {
pub application_id: String,
pub service_id: String,
pub logical_terminal: String,
pub sender_bic: String,
pub session_number: String,
pub sequence_number: String,
}
impl serde::Serialize for BasicHeader {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let normalized_logical_terminal = if self.logical_terminal.len() > 12 {
self.logical_terminal[..12].to_string()
} else if self.logical_terminal.len() < 12 {
format!("{:X<12}", self.logical_terminal)
} else {
self.logical_terminal.clone()
};
let mut state = serializer.serialize_struct("BasicHeader", 5)?;
state.serialize_field("application_id", &self.application_id)?;
state.serialize_field("service_id", &self.service_id)?;
state.serialize_field("logical_terminal", &normalized_logical_terminal)?;
state.serialize_field("sender_bic", &self.sender_bic)?;
state.serialize_field("session_number", &self.session_number)?;
state.serialize_field("sequence_number", &self.sequence_number)?;
state.end()
}
}
impl<'de> serde::Deserialize<'de> for BasicHeader {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct BasicHeaderHelper {
application_id: String,
service_id: String,
logical_terminal: String,
sender_bic: String,
session_number: String,
sequence_number: String,
}
let helper = BasicHeaderHelper::deserialize(deserializer)?;
let normalized_logical_terminal = if helper.logical_terminal.len() > 12 {
helper.logical_terminal[..12].to_string()
} else if helper.logical_terminal.len() < 12 {
format!("{:X<12}", helper.logical_terminal)
} else {
helper.logical_terminal.clone()
};
let sender_bic = helper.sender_bic;
Ok(BasicHeader {
application_id: helper.application_id,
service_id: helper.service_id,
logical_terminal: normalized_logical_terminal,
sender_bic,
session_number: helper.session_number,
sequence_number: helper.sequence_number,
})
}
}
impl BasicHeader {
pub fn parse(block1: &str) -> Result<Self> {
if block1.len() != 25 {
return Err(ParseError::InvalidBlockStructure {
block: "1".to_string(),
message: format!(
"Block 1 must be exactly 25 characters, got {}",
block1.len()
),
});
}
let application_id = block1[0..1].to_string();
let service_id = block1[1..3].to_string();
let raw_logical_terminal = block1[3..15].to_string();
let session_number = block1[15..19].to_string();
let sequence_number = block1[19..25].to_string();
let logical_terminal = raw_logical_terminal;
let sender_bic = if logical_terminal.len() == 12 {
let last_four = &logical_terminal[8..12];
if last_four == "XXXX" || (last_four.len() == 4 && &last_four[1..] == "XXX") {
logical_terminal[0..8].to_string()
} else {
let potential_branch = &logical_terminal[8..11];
if potential_branch.chars().all(|c| c.is_ascii_alphanumeric())
&& potential_branch != "XXX"
{
logical_terminal[0..11].to_string()
} else {
logical_terminal[0..8].to_string()
}
}
} else if logical_terminal.len() >= 11 {
logical_terminal[0..11].to_string()
} else if logical_terminal.len() >= 8 {
logical_terminal[0..8].to_string()
} else {
logical_terminal.clone()
};
Ok(BasicHeader {
application_id,
service_id,
logical_terminal,
sender_bic,
session_number,
sequence_number,
})
}
}
impl std::fmt::Display for BasicHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let logical_terminal = if self.logical_terminal.len() > 12 {
self.logical_terminal[..12].to_string()
} else if self.logical_terminal.len() < 12 {
format!("{:X<12}", self.logical_terminal)
} else {
self.logical_terminal.clone()
};
let session_number = format!(
"{:0>4}",
&self.session_number[..self.session_number.len().min(4)]
);
let sequence_number = format!(
"{:0>6}",
&self.sequence_number[..self.sequence_number.len().min(6)]
);
write!(
f,
"{}{}{}{}{}",
self.application_id, self.service_id, logical_terminal, session_number, sequence_number
)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct InputApplicationHeader {
pub message_type: String,
pub destination_address: String,
pub receiver_bic: String,
pub priority: String,
pub delivery_monitoring: Option<String>,
pub obsolescence_period: Option<String>,
}
impl serde::Serialize for InputApplicationHeader {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let normalized_destination_address = if self.destination_address.len() > 12 {
self.destination_address[..12].to_string()
} else if self.destination_address.len() < 12 {
format!("{:X<12}", self.destination_address)
} else {
self.destination_address.clone()
};
let field_count = 4
+ self.delivery_monitoring.is_some() as usize
+ self.obsolescence_period.is_some() as usize;
let mut state = serializer.serialize_struct("InputApplicationHeader", field_count)?;
state.serialize_field("message_type", &self.message_type)?;
state.serialize_field("destination_address", &normalized_destination_address)?;
state.serialize_field("receiver_bic", &self.receiver_bic)?;
state.serialize_field("priority", &self.priority)?;
if let Some(ref dm) = self.delivery_monitoring {
state.serialize_field("delivery_monitoring", dm)?;
}
if let Some(ref op) = self.obsolescence_period {
state.serialize_field("obsolescence_period", op)?;
}
state.end()
}
}
impl<'de> serde::Deserialize<'de> for InputApplicationHeader {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct InputApplicationHeaderHelper {
message_type: String,
destination_address: String,
receiver_bic: String,
priority: String,
delivery_monitoring: Option<String>,
obsolescence_period: Option<String>,
}
let helper = InputApplicationHeaderHelper::deserialize(deserializer)?;
let normalized_destination_address = if helper.destination_address.len() > 12 {
helper.destination_address[..12].to_string()
} else if helper.destination_address.len() < 12 {
format!("{:X<12}", helper.destination_address)
} else {
helper.destination_address.clone()
};
let receiver_bic = helper.receiver_bic;
Ok(InputApplicationHeader {
message_type: helper.message_type,
destination_address: normalized_destination_address,
receiver_bic,
priority: helper.priority,
delivery_monitoring: helper.delivery_monitoring,
obsolescence_period: helper.obsolescence_period,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct OutputApplicationHeader {
pub message_type: String,
pub input_time: String,
pub mir: MessageInputReference,
pub output_date: String,
pub output_time: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(tag = "direction")]
pub enum ApplicationHeader {
#[serde(rename = "I")]
Input(InputApplicationHeader),
#[serde(rename = "O")]
Output(OutputApplicationHeader),
}
impl ApplicationHeader {
pub fn parse(block2: &str) -> Result<Self> {
if block2.len() < 4 {
return Err(ParseError::InvalidBlockStructure {
block: "2".to_string(),
message: format!(
"Block 2 too short: expected at least 4 characters, got {}",
block2.len()
),
});
}
let direction = &block2[0..1];
let message_type = block2[1..4].to_string();
match direction {
"I" => {
if block2.len() < 17 {
return Err(ParseError::InvalidBlockStructure {
block: "2".to_string(),
message: format!(
"Input Block 2 too short: expected at least 17 characters, got {}",
block2.len()
),
});
}
let raw_destination_address = block2[4..16].to_string();
let priority = block2[16..17].to_string();
let destination_address = raw_destination_address;
let receiver_bic = if destination_address.len() == 12 {
let last_four = &destination_address[8..12];
if last_four == "XXXX" || (last_four.len() == 4 && &last_four[1..] == "XXX") {
destination_address[0..8].to_string()
} else {
let potential_branch = &destination_address[8..11];
if potential_branch.chars().all(|c| c.is_ascii_alphanumeric())
&& potential_branch != "XXX"
{
destination_address[0..11].to_string()
} else {
destination_address[0..8].to_string()
}
}
} else if destination_address.len() >= 11 {
destination_address[0..11].to_string()
} else if destination_address.len() >= 8 {
destination_address[0..8].to_string()
} else {
destination_address.clone()
};
let delivery_monitoring = if block2.len() >= 18 {
let monitoring = &block2[17..18];
if monitoring
.chars()
.all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit())
{
Some(monitoring.to_string())
} else {
None
}
} else {
None
};
let obsolescence_period = if delivery_monitoring.is_some() && block2.len() >= 21 {
Some(block2[18..21].to_string())
} else {
None
};
Ok(ApplicationHeader::Input(InputApplicationHeader {
message_type,
destination_address,
receiver_bic,
priority,
delivery_monitoring,
obsolescence_period,
}))
}
"O" => {
if block2.len() < 46 {
return Err(ParseError::InvalidBlockStructure {
block: "2".to_string(),
message: format!(
"Output Block 2 too short: expected at least 46 characters, got {}",
block2.len()
),
});
}
let input_time = block2[4..8].to_string();
let mir_date = block2[8..14].to_string(); let mir_lt_address = block2[14..26].to_string(); let mir_session = block2[26..30].to_string(); let mir_sequence = block2[30..36].to_string();
let output_date = block2[36..42].to_string(); let output_time = block2[42..46].to_string();
let priority = if block2.len() >= 47 {
Some(block2[46..47].to_string())
} else {
None
};
let mir = MessageInputReference {
date: mir_date,
lt_identifier: mir_lt_address.clone(),
branch_code: if mir_lt_address.len() >= 12 {
mir_lt_address[9..12].to_string()
} else {
"XXX".to_string()
},
session_number: mir_session,
sequence_number: mir_sequence,
};
Ok(ApplicationHeader::Output(OutputApplicationHeader {
message_type,
input_time,
mir,
output_date,
output_time,
priority,
}))
}
_ => Err(ParseError::InvalidBlockStructure {
block: "2".to_string(),
message: format!(
"Invalid direction indicator: expected 'I' or 'O', got '{}'",
direction
),
}),
}
}
pub fn message_type(&self) -> &str {
match self {
ApplicationHeader::Input(header) => &header.message_type,
ApplicationHeader::Output(header) => &header.message_type,
}
}
pub fn priority(&self) -> Option<&str> {
match self {
ApplicationHeader::Input(header) => Some(&header.priority),
ApplicationHeader::Output(header) => header.priority.as_deref(),
}
}
}
impl std::fmt::Display for ApplicationHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApplicationHeader::Input(header) => {
write!(f, "{}", header)
}
ApplicationHeader::Output(header) => {
let mut result = format!(
"O{}{}{}{}{}{}{}{}",
header.message_type,
header.input_time,
header.mir.date,
header.mir.lt_identifier,
header.mir.session_number,
header.mir.sequence_number,
header.output_date,
header.output_time,
);
if let Some(ref priority) = header.priority {
result.push_str(priority);
}
write!(f, "{result}")
}
}
}
}
impl std::fmt::Display for InputApplicationHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message_type = format!(
"{:0>3}",
&self.message_type[..self.message_type.len().min(3)]
);
let destination_address = if self.destination_address.len() > 12 {
self.destination_address[..12].to_string()
} else if self.destination_address.len() < 12 {
format!("{:X<12}", self.destination_address)
} else {
self.destination_address.clone()
};
let mut result = format!("I{}{}{}", message_type, destination_address, self.priority);
if let Some(ref delivery_monitoring) = self.delivery_monitoring {
result.push_str(delivery_monitoring);
}
if let Some(ref obsolescence_period) = self.obsolescence_period {
result.push_str(obsolescence_period);
}
write!(f, "{result}")
}
}
impl std::fmt::Display for OutputApplicationHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut result = format!(
"O{}{}{}{}{}{}{}{}",
self.message_type,
self.input_time,
self.mir.date,
self.mir.lt_identifier,
self.mir.session_number,
self.mir.sequence_number,
self.output_date,
self.output_time,
);
if let Some(ref priority) = self.priority {
result.push_str(priority);
}
write!(f, "{result}")
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct UserHeader {
#[serde(skip_serializing_if = "Option::is_none")]
pub service_identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub banking_priority: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_user_reference: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validation_flag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub balance_checkpoint: Option<BalanceCheckpoint>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_input_reference: Option<MessageInputReference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub related_reference: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "service_type_identifier")]
pub service_type_identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_end_to_end_reference: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub addressee_information: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_release_information: Option<PaymentReleaseInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sanctions_screening_info: Option<SanctionsScreeningInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_controls_info: Option<PaymentControlsInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct BalanceCheckpoint {
pub date: String,
pub time: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub hundredths_of_second: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct MessageInputReference {
pub date: String,
pub lt_identifier: String,
pub branch_code: String,
pub session_number: String,
pub sequence_number: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct PaymentReleaseInfo {
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_info: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct SanctionsScreeningInfo {
pub code_word: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_info: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct PaymentControlsInfo {
pub code_word: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_info: Option<String>,
}
impl UserHeader {
pub fn parse(block3: &str) -> Result<Self> {
let mut user_header = UserHeader::default();
if block3.contains("{103:")
&& let Some(start) = block3.find("{103:")
&& let Some(end) = block3[start..].find('}')
{
user_header.service_identifier = Some(block3[start + 5..start + end].to_string());
}
if block3.contains("{113:")
&& let Some(start) = block3.find("{113:")
&& let Some(end) = block3[start..].find('}')
{
user_header.banking_priority = Some(block3[start + 5..start + end].to_string());
}
if block3.contains("{108:")
&& let Some(start) = block3.find("{108:")
&& let Some(end) = block3[start..].find('}')
{
user_header.message_user_reference = Some(block3[start + 5..start + end].to_string());
}
if block3.contains("{119:")
&& let Some(start) = block3.find("{119:")
&& let Some(end) = block3[start..].find('}')
{
user_header.validation_flag = Some(block3[start + 5..start + end].to_string());
}
if block3.contains("{423:")
&& let Some(start) = block3.find("{423:")
&& let Some(end) = block3[start..].find('}')
{
let value = &block3[start + 5..start + end];
user_header.balance_checkpoint = Self::parse_balance_checkpoint(value);
}
if block3.contains("{106:")
&& let Some(start) = block3.find("{106:")
&& let Some(end) = block3[start..].find('}')
{
let value = &block3[start + 5..start + end];
user_header.message_input_reference = Self::parse_message_input_reference(value);
}
if block3.contains("{424:")
&& let Some(start) = block3.find("{424:")
&& let Some(end) = block3[start..].find('}')
{
user_header.related_reference = Some(block3[start + 5..start + end].to_string());
}
if block3.contains("{111:")
&& let Some(start) = block3.find("{111:")
&& let Some(end) = block3[start..].find('}')
{
user_header.service_type_identifier = Some(block3[start + 5..start + end].to_string());
}
if block3.contains("{121:")
&& let Some(start) = block3.find("{121:")
&& let Some(end) = block3[start..].find('}')
{
user_header.unique_end_to_end_reference =
Some(block3[start + 5..start + end].to_string());
}
if block3.contains("{115:")
&& let Some(start) = block3.find("{115:")
&& let Some(end) = block3[start..].find('}')
{
user_header.addressee_information = Some(block3[start + 5..start + end].to_string());
}
if block3.contains("{165:")
&& let Some(start) = block3.find("{165:")
&& let Some(end) = block3[start..].find('}')
{
let value = &block3[start + 5..start + end];
user_header.payment_release_information = Self::parse_payment_release_info(value);
}
if block3.contains("{433:")
&& let Some(start) = block3.find("{433:")
&& let Some(end) = block3[start..].find('}')
{
let value = &block3[start + 5..start + end];
user_header.sanctions_screening_info = Self::parse_sanctions_screening_info(value);
}
if block3.contains("{434:")
&& let Some(start) = block3.find("{434:")
&& let Some(end) = block3[start..].find('}')
{
let value = &block3[start + 5..start + end];
user_header.payment_controls_info = Self::parse_payment_controls_info(value);
}
Ok(user_header)
}
fn parse_balance_checkpoint(value: &str) -> Option<BalanceCheckpoint> {
if value.len() >= 12 {
Some(BalanceCheckpoint {
date: value[0..6].to_string(),
time: value[6..12].to_string(),
hundredths_of_second: if value.len() > 12 {
Some(value[12..].to_string())
} else {
None
},
})
} else {
None
}
}
fn parse_message_input_reference(value: &str) -> Option<MessageInputReference> {
if value.len() >= 28 {
Some(MessageInputReference {
date: value[0..6].to_string(),
lt_identifier: value[6..18].to_string(),
branch_code: value[18..21].to_string(),
session_number: value[21..25].to_string(),
sequence_number: value[25..].to_string(),
})
} else {
None
}
}
fn parse_payment_release_info(value: &str) -> Option<PaymentReleaseInfo> {
if value.len() >= 3 {
let code = value[0..3].to_string();
let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
Some(value[4..].to_string())
} else {
None
};
Some(PaymentReleaseInfo {
code,
additional_info,
})
} else {
None
}
}
fn parse_sanctions_screening_info(value: &str) -> Option<SanctionsScreeningInfo> {
if value.len() >= 3 {
let code_word = value[0..3].to_string();
let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
Some(value[4..].to_string())
} else {
None
};
Some(SanctionsScreeningInfo {
code_word,
additional_info,
})
} else {
None
}
}
fn parse_payment_controls_info(value: &str) -> Option<PaymentControlsInfo> {
if value.len() >= 3 {
let code_word = value[0..3].to_string();
let additional_info = if value.len() > 4 && value.chars().nth(3) == Some('/') {
Some(value[4..].to_string())
} else {
None
};
Some(PaymentControlsInfo {
code_word,
additional_info,
})
} else {
None
}
}
}
impl std::fmt::Display for UserHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut result = String::new();
if let Some(ref service_id) = self.service_identifier {
result.push_str(&format!("{{103:{service_id}}}"));
}
if let Some(ref banking_priority) = self.banking_priority {
result.push_str(&format!("{{113:{banking_priority}}}"));
}
if let Some(ref message_user_ref) = self.message_user_reference {
result.push_str(&format!("{{108:{message_user_ref}}}"));
}
if let Some(ref validation_flag) = self.validation_flag {
result.push_str(&format!("{{119:{validation_flag}}}"));
}
if let Some(ref unique_end_to_end_ref) = self.unique_end_to_end_reference {
result.push_str(&format!("{{121:{unique_end_to_end_ref}}}"));
}
if let Some(ref service_type_identifier) = self.service_type_identifier {
result.push_str(&format!("{{111:{service_type_identifier}}}"));
}
if let Some(ref payment_controls) = self.payment_controls_info {
let mut value = payment_controls.code_word.clone();
if let Some(ref additional) = payment_controls.additional_info {
value.push('/');
value.push_str(additional);
}
result.push_str(&format!("{{434:{value}}}"));
}
if let Some(ref payment_release) = self.payment_release_information {
let mut value = payment_release.code.clone();
if let Some(ref additional) = payment_release.additional_info {
value.push('/');
value.push_str(additional);
}
result.push_str(&format!("{{165:{value}}}"));
}
if let Some(ref sanctions) = self.sanctions_screening_info {
let mut value = sanctions.code_word.clone();
if let Some(ref additional) = sanctions.additional_info {
value.push('/');
value.push_str(additional);
}
result.push_str(&format!("{{433:{value}}}"));
}
write!(f, "{result}")
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct Trailer {
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_and_training: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub possible_duplicate_emission: Option<PossibleDuplicateEmission>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delayed_message: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_reference: Option<MessageReference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub possible_duplicate_message: Option<PossibleDuplicateMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_originated_message: Option<SystemOriginatedMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mac: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct PossibleDuplicateEmission {
#[serde(skip_serializing_if = "Option::is_none")]
pub time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_input_reference: Option<MessageInputReference>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct MessageReference {
pub date: String,
pub full_time: String,
pub message_input_reference: MessageInputReference,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct PossibleDuplicateMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_output_reference: Option<MessageOutputReference>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct MessageOutputReference {
pub date: String,
pub lt_identifier: String,
pub branch_code: String,
pub session_number: String,
pub sequence_number: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
pub struct SystemOriginatedMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_input_reference: Option<MessageInputReference>,
}
impl Trailer {
pub fn parse(block5: &str) -> Result<Self> {
let mut trailer = Trailer::default();
if block5.contains("{CHK:")
&& let Some(start) = block5.find("{CHK:")
&& let Some(end) = block5[start..].find('}')
{
trailer.checksum = Some(block5[start + 5..start + end].to_string());
}
if block5.contains("{TNG}") {
trailer.test_and_training = Some(true);
}
if block5.contains("{DLM}") {
trailer.delayed_message = Some(true);
}
if block5.contains("{MAC:")
&& let Some(start) = block5.find("{MAC:")
&& let Some(end) = block5[start..].find('}')
{
trailer.mac = Some(block5[start + 5..start + end].to_string());
}
Ok(trailer)
}
}
impl std::fmt::Display for Trailer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut result = String::new();
if let Some(ref checksum) = self.checksum {
result.push_str(&format!("{{CHK:{checksum}}}"));
}
if let Some(true) = self.test_and_training {
result.push_str("{TNG}");
}
if let Some(true) = self.delayed_message {
result.push_str("{DLM}");
}
if let Some(ref possible_duplicate_emission) = self.possible_duplicate_emission {
result.push_str(&format!(
"{{PDE:{}}}",
possible_duplicate_emission.time.as_deref().unwrap_or("")
));
}
if let Some(ref message_reference) = self.message_reference {
result.push_str(&format!("{{MRF:{}}}", message_reference.date));
}
if let Some(ref mac) = self.mac {
result.push_str(&format!("{{MAC:{mac}}}"));
}
write!(f, "{result}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_application_header_input_parsing() {
let block2 = "I103DEUTDEFFAXXXN";
let header = ApplicationHeader::parse(block2).unwrap();
match header {
ApplicationHeader::Input(input) => {
assert_eq!(input.message_type, "103");
assert_eq!(input.destination_address, "DEUTDEFFAXXX");
assert_eq!(input.receiver_bic, "DEUTDEFF");
assert_eq!(input.priority, "N");
assert_eq!(input.delivery_monitoring, None);
assert_eq!(input.obsolescence_period, None);
}
ApplicationHeader::Output(_) => panic!("Expected Input header, got Output"),
}
}
#[test]
fn test_application_header_input_parsing_with_monitoring() {
let block2 = "I103DEUTDEFFAXXXU3003";
let header = ApplicationHeader::parse(block2).unwrap();
match header {
ApplicationHeader::Input(input) => {
assert_eq!(input.message_type, "103");
assert_eq!(input.destination_address, "DEUTDEFFAXXX");
assert_eq!(input.receiver_bic, "DEUTDEFF");
assert_eq!(input.priority, "U");
assert_eq!(input.delivery_monitoring, Some("3".to_string()));
assert_eq!(input.obsolescence_period, Some("003".to_string()));
}
ApplicationHeader::Output(_) => panic!("Expected Input header, got Output"),
}
}
#[test]
fn test_application_header_output_parsing() {
let block2 = "O1031535051028DEUTDEFFAXXX08264556280510281535N";
let header = ApplicationHeader::parse(block2).unwrap();
match header {
ApplicationHeader::Output(output) => {
assert_eq!(output.message_type, "103");
assert_eq!(output.input_time, "1535");
assert_eq!(output.output_date, "051028");
assert_eq!(output.output_time, "1535");
assert_eq!(output.priority, Some("N".to_string()));
assert_eq!(output.mir.date, "051028");
assert_eq!(output.mir.lt_identifier, "DEUTDEFFAXXX");
assert_eq!(output.mir.branch_code, "XXX");
assert_eq!(output.mir.session_number, "0826");
assert_eq!(output.mir.sequence_number, "455628");
}
ApplicationHeader::Input(_) => panic!("Expected Output header, got Input"),
}
}
#[test]
fn test_application_header_output_parsing_different_message_type() {
let block2 = "O2021245051028CHASUS33AXXX08264556280510281245U";
let header = ApplicationHeader::parse(block2).unwrap();
match header {
ApplicationHeader::Output(output) => {
assert_eq!(output.message_type, "202");
assert_eq!(output.mir.lt_identifier, "CHASUS33AXXX");
assert_eq!(output.priority, Some("U".to_string()));
}
ApplicationHeader::Input(_) => panic!("Expected Output header, got Input"),
}
}
#[test]
fn test_application_header_invalid_direction() {
let block2 = "X103DEUTDEFFAXXXN";
let result = ApplicationHeader::parse(block2);
assert!(result.is_err());
if let Err(ParseError::InvalidBlockStructure { message, .. }) = result {
assert!(message.contains("Invalid direction indicator"));
} else {
panic!("Expected InvalidBlockStructure error");
}
}
#[test]
fn test_application_header_input_too_short() {
let block2 = "I103DEUTDEF"; let result = ApplicationHeader::parse(block2);
assert!(result.is_err());
}
#[test]
fn test_application_header_output_too_short() {
let block2 = "O103153505102"; let result = ApplicationHeader::parse(block2);
assert!(result.is_err());
if let Err(ParseError::InvalidBlockStructure { message, .. }) = result {
assert!(message.contains("Output Block 2 too short: expected at least 46 characters"));
} else {
panic!("Expected InvalidBlockStructure error");
}
}
#[test]
fn test_application_header_output_minimum_length_but_still_too_short() {
let block2 = "O10315350510280DE"; let result = ApplicationHeader::parse(block2);
assert!(result.is_err());
if let Err(ParseError::InvalidBlockStructure { message, .. }) = result {
assert!(message.contains("Output Block 2 too short: expected at least 46 characters"));
} else {
panic!("Expected InvalidBlockStructure error");
}
}
#[test]
fn test_basic_header_parsing() {
let block1 = "F01DEUTDEFFAXXX0000123456";
let header = BasicHeader::parse(block1).unwrap();
assert_eq!(header.application_id, "F");
assert_eq!(header.service_id, "01");
assert_eq!(header.logical_terminal, "DEUTDEFFAXXX");
assert_eq!(header.sender_bic, "DEUTDEFF");
assert_eq!(header.session_number, "0000");
assert_eq!(header.sequence_number, "123456");
}
#[test]
fn test_application_header_input_display() {
let header = ApplicationHeader::Input(InputApplicationHeader {
message_type: "103".to_string(),
destination_address: "DEUTDEFFAXXX".to_string(),
receiver_bic: "DEUTDEFF".to_string(),
priority: "U".to_string(),
delivery_monitoring: Some("3".to_string()),
obsolescence_period: Some("003".to_string()),
});
assert_eq!(header.to_string(), "I103DEUTDEFFAXXXU3003");
}
#[test]
fn test_application_header_output_display() {
let mir = MessageInputReference {
date: "051028".to_string(),
lt_identifier: "DEUTDEFFAXXX".to_string(),
branch_code: "XXX".to_string(),
session_number: "0826".to_string(),
sequence_number: "455628".to_string(),
};
let header = ApplicationHeader::Output(OutputApplicationHeader {
message_type: "103".to_string(),
input_time: "1535".to_string(),
mir,
output_date: "051028".to_string(),
output_time: "1535".to_string(),
priority: Some("N".to_string()),
});
assert_eq!(
header.to_string(),
"O1031535051028DEUTDEFFAXXX08264556280510281535N"
);
}
#[test]
fn test_application_header_helper_methods() {
let input_header = ApplicationHeader::Input(InputApplicationHeader {
message_type: "103".to_string(),
destination_address: "DEUTDEFFAXXX".to_string(),
receiver_bic: "DEUTDEFF".to_string(),
priority: "U".to_string(),
delivery_monitoring: None,
obsolescence_period: None,
});
assert_eq!(input_header.message_type(), "103");
assert_eq!(input_header.priority(), Some("U"));
let mir = MessageInputReference {
date: "051028".to_string(),
lt_identifier: "DEUTDEFFAXXX".to_string(),
branch_code: "XXX".to_string(),
session_number: "0826".to_string(),
sequence_number: "455628".to_string(),
};
let output_header = ApplicationHeader::Output(OutputApplicationHeader {
message_type: "202".to_string(),
input_time: "1535".to_string(),
mir,
output_date: "051028".to_string(),
output_time: "1535".to_string(),
priority: Some("N".to_string()),
});
assert_eq!(output_header.message_type(), "202");
assert_eq!(output_header.priority(), Some("N"));
}
}