swift_mt_message/fields/common/
bic_field.rs1use serde::{Deserialize, Serialize};
2use std::str::FromStr;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9pub struct GenericBicField {
10 pub bic: BIC,
12 pub account: Option<String>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17pub struct BIC {
18 pub raw: String,
19
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub bank_code: Option<String>,
22
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub country_code: Option<String>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub location_code: Option<String>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub branch_code: Option<String>,
31}
32
33impl std::fmt::Display for BIC {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 write!(f, "{}", self.raw)
36 }
37}
38
39impl std::str::FromStr for BIC {
40 type Err = String;
41
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 if s.len() < 8 {
44 return Err("BIC must be at least 8 characters".to_string());
45 }
46
47 if s.len() > 11 {
48 let truncated = &s[..11];
50 return BIC::from_str(truncated);
51 }
52
53 Ok(BIC {
54 raw: s.to_string(),
55 bank_code: if s.len() > 4 {
56 Some(s[0..4].to_string())
57 } else {
58 None
59 },
60 country_code: if s.len() > 6 {
61 Some(s[4..6].to_string())
62 } else {
63 None
64 },
65 location_code: if s.len() > 8 {
66 Some(s[6..8].to_string())
67 } else {
68 None
69 },
70 branch_code: if s.len() == 11 {
71 Some(s[8..11].to_string())
72 } else {
73 None
74 },
75 })
76 }
77}
78
79impl crate::SwiftField for GenericBicField {
81 fn parse(value: &str) -> crate::Result<Self> {
82 let content = value.trim();
83
84 let content = Self::remove_field_tag_prefix(content);
86
87 if let Some(slash_pos) = content.find('/') {
89 let bic_str = &content[..slash_pos];
91 let account_str = &content[slash_pos + 1..];
92
93 let bic =
94 bic_str
95 .parse()
96 .map_err(|e: String| crate::ParseError::InvalidFieldFormat {
97 field_tag: "GENERICBICFIELD".to_string(),
98 message: format!("Failed to parse BIC: {e}"),
99 })?;
100
101 let account = if account_str.is_empty() {
102 None
103 } else {
104 Some(account_str.to_string())
105 };
106
107 Ok(GenericBicField { bic, account })
108 } else {
109 let bic =
111 content
112 .parse()
113 .map_err(|e: String| crate::ParseError::InvalidFieldFormat {
114 field_tag: "GENERICBICFIELD".to_string(),
115 message: format!("Failed to parse BIC: {e}"),
116 })?;
117
118 Ok(GenericBicField { bic, account: None })
119 }
120 }
121
122 fn to_swift_string(&self) -> String {
123 if let Some(ref account) = self.account {
124 format!("{}/{}", self.bic, account)
125 } else {
126 self.bic.to_string()
127 }
128 }
129
130 fn validate(&self) -> crate::ValidationResult {
131 let mut errors = Vec::new();
132 let mut warnings = Vec::new();
133
134 if self.bic.raw.len() != 8 && self.bic.raw.len() != 11 {
136 errors.push(crate::ValidationError::FormatValidation {
137 field_tag: "GENERICBICFIELD".to_string(),
138 message: format!("BIC must be 8 or 11 characters, got {}", self.bic.raw.len()),
139 });
140 }
141
142 if let Some(ref account) = self.account {
144 if account.len() > 35 {
145 errors.push(crate::ValidationError::LengthValidation {
146 field_tag: "GENERICBICFIELD".to_string(),
147 expected: "≤35".to_string(),
148 actual: account.len(),
149 });
150 }
151 if account.is_empty() {
152 warnings.push("Empty account number provided".to_string());
153 }
154 }
155
156 crate::ValidationResult {
157 is_valid: errors.is_empty(),
158 errors,
159 warnings,
160 }
161 }
162
163 fn format_spec() -> &'static str {
164 "4!a2!a2!c[3!c][/35x]"
165 }
166
167 fn sample() -> Self {
168 Self {
169 bic: BIC::from_str(&crate::sample::generate_valid_bic()).unwrap(),
170 account: Some(crate::sample::generate_account_number()),
171 }
172 }
173
174 fn sample_with_config(_config: &crate::sample::FieldConfig) -> Self {
175 Self::sample()
176 }
177}
178
179impl GenericBicField {
180 fn remove_field_tag_prefix(value: &str) -> &str {
183 use std::sync::OnceLock;
184 static FIELD_TAG_REGEX: OnceLock<regex::Regex> = OnceLock::new();
185
186 let regex = FIELD_TAG_REGEX.get_or_init(|| {
187 regex::Regex::new(r"^:?([0-9]{1,3}[A-Z]{0,2}):").unwrap()
190 });
191
192 if let Some(captures) = regex.find(value) {
193 &value[captures.end()..]
194 } else {
195 value
196 }
197 }
198}
199
200impl std::fmt::Display for GenericBicField {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 if let Some(ref account) = self.account {
203 write!(f, "{}/{}", self.bic, account)
204 } else {
205 write!(f, "{}", self.bic)
206 }
207 }
208}