1use crate::mx_envelope::MxEnvelope;
4use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
5use quick_xml::writer::Writer;
6use quick_xml::{de::from_str as xml_from_str, se::to_string as xml_to_string};
7use serde::{Deserialize, Serialize};
8use std::error::Error;
9use std::fmt;
10use std::io::Cursor;
11
12#[derive(Debug, Clone)]
14pub struct XmlConfig {
15 pub include_declaration: bool,
17 pub pretty_print: bool,
19 pub indent: String,
21 pub include_schema_location: bool,
23}
24
25impl Default for XmlConfig {
26 fn default() -> Self {
27 Self {
28 include_declaration: true,
29 pretty_print: true,
30 indent: " ".to_string(),
31 include_schema_location: false,
32 }
33 }
34}
35
36#[derive(Debug)]
38pub enum XmlError {
39 SerializationError(String),
40 DeserializationError(String),
41 ValidationError(String),
42}
43
44impl fmt::Display for XmlError {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 XmlError::SerializationError(msg) => write!(f, "XML Serialization Error: {msg}"),
48 XmlError::DeserializationError(msg) => write!(f, "XML Deserialization Error: {msg}"),
49 XmlError::ValidationError(msg) => write!(f, "XML Validation Error: {msg}"),
50 }
51 }
52}
53
54impl Error for XmlError {}
55
56pub fn to_mx_xml<H, D>(
58 message: D,
59 header: H,
60 message_type: &str,
61 config: Option<XmlConfig>,
62) -> Result<String, XmlError>
63where
64 H: Serialize,
65 D: Serialize,
66{
67 let config = config.unwrap_or_default();
68
69 let document_namespace = get_namespace_for_message_type(message_type);
71
72 let envelope = MxEnvelope::new(header, message, document_namespace);
74
75 if config.pretty_print {
77 format_mx_xml(&envelope, &config)
78 } else {
79 xml_to_string(&envelope).map_err(|e| XmlError::SerializationError(e.to_string()))
81 }
82}
83
84pub fn from_mx_xml<H, D>(xml: &str) -> Result<MxEnvelope<H, D>, XmlError>
86where
87 H: for<'de> Deserialize<'de>,
88 D: for<'de> Deserialize<'de>,
89{
90 xml_from_str(xml).map_err(|e| XmlError::DeserializationError(e.to_string()))
91}
92
93fn format_mx_xml<H, D>(envelope: &MxEnvelope<H, D>, config: &XmlConfig) -> Result<String, XmlError>
95where
96 H: Serialize,
97 D: Serialize,
98{
99 let mut writer = Writer::new_with_indent(Cursor::new(Vec::new()), b' ', config.indent.len());
100
101 if config.include_declaration {
103 writer
104 .write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
105 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
106 }
107
108 let envelope_elem = BytesStart::new("Envelope");
110 writer
111 .write_event(Event::Start(envelope_elem))
112 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
113
114 let mut app_hdr_elem = BytesStart::new("AppHdr");
116 app_hdr_elem.push_attribute(("xmlns", "urn:iso:std:iso:20022:tech:xsd:head.001.001.02"));
117
118 writer
119 .write_event(Event::Start(app_hdr_elem))
120 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
121
122 let app_hdr_xml = xml_to_string(&envelope.app_hdr)
124 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
125
126 let app_hdr_xml = app_hdr_xml
128 .trim_start_matches("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
129 .trim();
130
131 let app_hdr_inner = if app_hdr_xml.starts_with("<AppHdr>") {
133 app_hdr_xml
134 .trim_start_matches("<AppHdr>")
135 .trim_end_matches("</AppHdr>")
136 } else if app_hdr_xml.starts_with("<AppHdr") {
137 if let Some(pos) = app_hdr_xml.find('>') {
139 let content = &app_hdr_xml[pos + 1..];
140 content.trim_end_matches("</AppHdr>")
141 } else {
142 app_hdr_xml
143 }
144 } else {
145 app_hdr_xml
146 };
147
148 writer
149 .write_event(Event::Text(BytesText::from_escaped(app_hdr_inner)))
150 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
151
152 writer
154 .write_event(Event::End(BytesEnd::new("AppHdr")))
155 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
156
157 let mut doc_elem = BytesStart::new("Document");
159 if let Some(ref xmlns) = envelope.document.xmlns {
160 doc_elem.push_attribute(("xmlns", xmlns.as_str()));
161 }
162
163 writer
164 .write_event(Event::Start(doc_elem))
165 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
166
167 let message_xml = xml_to_string(&envelope.document.message)
169 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
170
171 let message_xml = message_xml
173 .trim_start_matches("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
174 .trim();
175
176 writer
177 .write_event(Event::Text(BytesText::from_escaped(message_xml)))
178 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
179
180 writer
182 .write_event(Event::End(BytesEnd::new("Document")))
183 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
184
185 writer
187 .write_event(Event::End(BytesEnd::new("Envelope")))
188 .map_err(|e| XmlError::SerializationError(e.to_string()))?;
189
190 let result = writer.into_inner().into_inner();
191 String::from_utf8(result).map_err(|e| XmlError::SerializationError(e.to_string()))
192}
193
194fn get_namespace_for_message_type(message_type: &str) -> String {
196 let namespace = match message_type {
197 "pacs.008" | "pacs.008.001.08" => "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08",
198 "pacs.009" | "pacs.009.001.08" => "urn:iso:std:iso:20022:tech:xsd:pacs.009.001.08",
199 "pacs.003" | "pacs.003.001.08" => "urn:iso:std:iso:20022:tech:xsd:pacs.003.001.08",
200 "pacs.004" | "pacs.004.001.09" => "urn:iso:std:iso:20022:tech:xsd:pacs.004.001.09",
201 "pacs.002" | "pacs.002.001.10" => "urn:iso:std:iso:20022:tech:xsd:pacs.002.001.10",
202 "pain.001" | "pain.001.001.09" => "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09",
203 "pain.002" | "pain.002.001.10" => "urn:iso:std:iso:20022:tech:xsd:pain.002.001.10",
204 "pain.008" | "pain.008.001.08" => "urn:iso:std:iso:20022:tech:xsd:pain.008.001.08",
205 "camt.025" | "camt.025.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.025.001.08",
206 "camt.052" | "camt.052.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.052.001.08",
207 "camt.053" | "camt.053.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.053.001.08",
208 "camt.054" | "camt.054.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.054.001.08",
209 "camt.056" | "camt.056.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.056.001.08",
210 "camt.057" | "camt.057.001.06" => "urn:iso:std:iso:20022:tech:xsd:camt.057.001.06",
211 "camt.060" | "camt.060.001.05" => "urn:iso:std:iso:20022:tech:xsd:camt.060.001.05",
212 "camt.027" | "camt.027.001.07" => "urn:iso:std:iso:20022:tech:xsd:camt.027.001.07",
213 "camt.029" | "camt.029.001.09" => "urn:iso:std:iso:20022:tech:xsd:camt.029.001.09",
214 "camt.107" | "camt.107.001.01" => "urn:iso:std:iso:20022:tech:xsd:camt.107.001.01",
215 "camt.108" | "camt.108.001.01" => "urn:iso:std:iso:20022:tech:xsd:camt.108.001.01",
216 "camt.109" | "camt.109.001.01" => "urn:iso:std:iso:20022:tech:xsd:camt.109.001.01",
217 _ => {
218 return format!("urn:iso:std:iso:20022:tech:xsd:{message_type}");
219 }
220 };
221 namespace.to_string()
222}
223
224pub fn create_pacs008_xml<D: Serialize>(
226 message: D,
227 from_bic: String,
228 to_bic: String,
229 business_msg_id: String,
230) -> Result<String, XmlError> {
231 use crate::header::bah_pacs_008_001_08::{
232 BranchAndFinancialInstitutionIdentification62, BusinessApplicationHeaderV02,
233 FinancialInstitutionIdentification182, Party44Choice1,
234 };
235
236 let header = BusinessApplicationHeaderV02 {
237 char_set: None,
238 fr: Party44Choice1 {
239 fi_id: Some(BranchAndFinancialInstitutionIdentification62 {
240 fin_instn_id: FinancialInstitutionIdentification182 {
241 bicfi: from_bic,
242 clr_sys_mmb_id: None,
243 lei: None,
244 },
245 }),
246 },
247 to: Party44Choice1 {
248 fi_id: Some(BranchAndFinancialInstitutionIdentification62 {
249 fin_instn_id: FinancialInstitutionIdentification182 {
250 bicfi: to_bic,
251 clr_sys_mmb_id: None,
252 lei: None,
253 },
254 }),
255 },
256 biz_msg_idr: business_msg_id,
257 msg_def_idr: "pacs.008.001.08".to_string(),
258 biz_svc: "swift.ug".to_string(),
259 mkt_prctc: None,
260 cre_dt: chrono::Utc::now()
261 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
262 .to_string(),
263 cpy_dplct: None,
264 pssbl_dplct: None,
265 prty: None,
266 rltd: None,
267 };
268
269 to_mx_xml(message, header, "pacs.008", None)
270}
271
272pub fn create_pain001_xml<D: Serialize>(
274 message: D,
275 from_bic: String,
276 to_bic: String,
277 business_msg_id: String,
278) -> Result<String, XmlError> {
279 use crate::header::bah_pain_001_001_09::{
280 BranchAndFinancialInstitutionIdentification64, BusinessApplicationHeaderV02,
281 FinancialInstitutionIdentification183, Party44Choice1,
282 };
283
284 let header = BusinessApplicationHeaderV02 {
285 char_set: None,
286 fr: Party44Choice1 {
287 fi_id: Some(BranchAndFinancialInstitutionIdentification64 {
288 fin_instn_id: FinancialInstitutionIdentification183 {
289 bicfi: from_bic,
290 clr_sys_mmb_id: None,
291 lei: None,
292 },
293 }),
294 },
295 to: Party44Choice1 {
296 fi_id: Some(BranchAndFinancialInstitutionIdentification64 {
297 fin_instn_id: FinancialInstitutionIdentification183 {
298 bicfi: to_bic,
299 clr_sys_mmb_id: None,
300 lei: None,
301 },
302 }),
303 },
304 biz_msg_idr: business_msg_id,
305 msg_def_idr: "pain.001.001.09".to_string(),
306 biz_svc: "swift.ug".to_string(),
307 mkt_prctc: None,
308 cre_dt: chrono::Utc::now()
309 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
310 .to_string(),
311 cpy_dplct: None,
312 pssbl_dplct: None,
313 prty: None,
314 rltd: None,
315 };
316
317 to_mx_xml(message, header, "pain.001", None)
318}
319
320pub fn create_camt053_xml<D: Serialize>(
322 message: D,
323 from_bic: String,
324 to_bic: String,
325 business_msg_id: String,
326) -> Result<String, XmlError> {
327 use crate::header::bah_camt_053_001_08::{
328 BranchAndFinancialInstitutionIdentification63, BusinessApplicationHeaderV02,
329 FinancialInstitutionIdentification182, Party44Choice1,
330 };
331
332 let header = BusinessApplicationHeaderV02 {
333 char_set: None,
334 fr: Party44Choice1 {
335 fi_id: Some(BranchAndFinancialInstitutionIdentification63 {
336 fin_instn_id: FinancialInstitutionIdentification182 {
337 bicfi: from_bic,
338 clr_sys_mmb_id: None,
339 lei: None,
340 },
341 }),
342 },
343 to: Party44Choice1 {
344 fi_id: Some(BranchAndFinancialInstitutionIdentification63 {
345 fin_instn_id: FinancialInstitutionIdentification182 {
346 bicfi: to_bic,
347 clr_sys_mmb_id: None,
348 lei: None,
349 },
350 }),
351 },
352 biz_msg_idr: business_msg_id,
353 msg_def_idr: "camt.053.001.08".to_string(),
354 biz_svc: "swift.ug".to_string(),
355 mkt_prctc: None,
356 cre_dt: chrono::Utc::now()
357 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
358 .to_string(),
359 cpy_dplct: None,
360 pssbl_dplct: None,
361 prty: None,
362 rltd: None,
363 };
364
365 to_mx_xml(message, header, "camt.053", None)
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_namespace_lookup() {
374 assert_eq!(
375 get_namespace_for_message_type("pacs.008"),
376 "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08"
377 );
378 assert_eq!(
379 get_namespace_for_message_type("pain.001"),
380 "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09"
381 );
382 assert_eq!(
383 get_namespace_for_message_type("camt.053"),
384 "urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"
385 );
386 }
387
388 #[test]
389 fn test_xml_config_default() {
390 let config = XmlConfig::default();
391 assert!(config.include_declaration);
392 assert!(config.pretty_print);
393 assert_eq!(config.indent, " ");
394 assert!(!config.include_schema_location);
395 }
396}