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.002" | "pacs.002.001.10" => "urn:iso:std:iso:20022:tech:xsd:pacs.002.001.10",
201 "pain.001" | "pain.001.001.09" => "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09",
202 "pain.008" | "pain.008.001.08" => "urn:iso:std:iso:20022:tech:xsd:pain.008.001.08",
203 "camt.052" | "camt.052.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.052.001.08",
204 "camt.053" | "camt.053.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.053.001.08",
205 "camt.054" | "camt.054.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.054.001.08",
206 "camt.056" | "camt.056.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.056.001.08",
207 "camt.057" | "camt.057.001.06" => "urn:iso:std:iso:20022:tech:xsd:camt.057.001.06",
208 "camt.060" | "camt.060.001.05" => "urn:iso:std:iso:20022:tech:xsd:camt.060.001.05",
209 "camt.027" | "camt.027.001.07" => "urn:iso:std:iso:20022:tech:xsd:camt.027.001.07",
210 "camt.029" | "camt.029.001.09" => "urn:iso:std:iso:20022:tech:xsd:camt.029.001.09",
211 _ => {
212 return format!("urn:iso:std:iso:20022:tech:xsd:{message_type}");
213 }
214 };
215 namespace.to_string()
216}
217
218pub fn create_pacs008_xml<D: Serialize>(
220 message: D,
221 from_bic: String,
222 to_bic: String,
223 business_msg_id: String,
224) -> Result<String, XmlError> {
225 use crate::header::bah_pacs_008_001_08::{
226 BranchAndFinancialInstitutionIdentification62, BusinessApplicationHeaderV02,
227 FinancialInstitutionIdentification182, Party44Choice1,
228 };
229
230 let header = BusinessApplicationHeaderV02 {
231 char_set: None,
232 fr: Party44Choice1 {
233 fi_id: Some(BranchAndFinancialInstitutionIdentification62 {
234 fin_instn_id: FinancialInstitutionIdentification182 {
235 bicfi: from_bic,
236 clr_sys_mmb_id: None,
237 lei: None,
238 },
239 }),
240 },
241 to: Party44Choice1 {
242 fi_id: Some(BranchAndFinancialInstitutionIdentification62 {
243 fin_instn_id: FinancialInstitutionIdentification182 {
244 bicfi: to_bic,
245 clr_sys_mmb_id: None,
246 lei: None,
247 },
248 }),
249 },
250 biz_msg_idr: business_msg_id,
251 msg_def_idr: "pacs.008.001.08".to_string(),
252 biz_svc: "swift.ug".to_string(),
253 mkt_prctc: None,
254 cre_dt: chrono::Utc::now()
255 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
256 .to_string(),
257 cpy_dplct: None,
258 pssbl_dplct: None,
259 prty: None,
260 rltd: None,
261 };
262
263 to_mx_xml(message, header, "pacs.008", None)
264}
265
266pub fn create_pain001_xml<D: Serialize>(
268 message: D,
269 from_bic: String,
270 to_bic: String,
271 business_msg_id: String,
272) -> Result<String, XmlError> {
273 use crate::header::bah_pain_001_001_09::{
274 BranchAndFinancialInstitutionIdentification64, BusinessApplicationHeaderV02,
275 FinancialInstitutionIdentification183, Party44Choice1,
276 };
277
278 let header = BusinessApplicationHeaderV02 {
279 char_set: None,
280 fr: Party44Choice1 {
281 fi_id: Some(BranchAndFinancialInstitutionIdentification64 {
282 fin_instn_id: FinancialInstitutionIdentification183 {
283 bicfi: from_bic,
284 clr_sys_mmb_id: None,
285 lei: None,
286 },
287 }),
288 },
289 to: Party44Choice1 {
290 fi_id: Some(BranchAndFinancialInstitutionIdentification64 {
291 fin_instn_id: FinancialInstitutionIdentification183 {
292 bicfi: to_bic,
293 clr_sys_mmb_id: None,
294 lei: None,
295 },
296 }),
297 },
298 biz_msg_idr: business_msg_id,
299 msg_def_idr: "pain.001.001.09".to_string(),
300 biz_svc: "swift.ug".to_string(),
301 mkt_prctc: None,
302 cre_dt: chrono::Utc::now()
303 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
304 .to_string(),
305 cpy_dplct: None,
306 pssbl_dplct: None,
307 prty: None,
308 rltd: None,
309 };
310
311 to_mx_xml(message, header, "pain.001", None)
312}
313
314pub fn create_camt053_xml<D: Serialize>(
316 message: D,
317 from_bic: String,
318 to_bic: String,
319 business_msg_id: String,
320) -> Result<String, XmlError> {
321 use crate::header::bah_camt_053_001_08::{
322 BranchAndFinancialInstitutionIdentification63, BusinessApplicationHeaderV02,
323 FinancialInstitutionIdentification182, Party44Choice1,
324 };
325
326 let header = BusinessApplicationHeaderV02 {
327 char_set: None,
328 fr: Party44Choice1 {
329 fi_id: Some(BranchAndFinancialInstitutionIdentification63 {
330 fin_instn_id: FinancialInstitutionIdentification182 {
331 bicfi: from_bic,
332 clr_sys_mmb_id: None,
333 lei: None,
334 },
335 }),
336 },
337 to: Party44Choice1 {
338 fi_id: Some(BranchAndFinancialInstitutionIdentification63 {
339 fin_instn_id: FinancialInstitutionIdentification182 {
340 bicfi: to_bic,
341 clr_sys_mmb_id: None,
342 lei: None,
343 },
344 }),
345 },
346 biz_msg_idr: business_msg_id,
347 msg_def_idr: "camt.053.001.08".to_string(),
348 biz_svc: "swift.ug".to_string(),
349 mkt_prctc: None,
350 cre_dt: chrono::Utc::now()
351 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
352 .to_string(),
353 cpy_dplct: None,
354 pssbl_dplct: None,
355 prty: None,
356 rltd: None,
357 };
358
359 to_mx_xml(message, header, "camt.053", None)
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_namespace_lookup() {
368 assert_eq!(
369 get_namespace_for_message_type("pacs.008"),
370 "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08"
371 );
372 assert_eq!(
373 get_namespace_for_message_type("pain.001"),
374 "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09"
375 );
376 assert_eq!(
377 get_namespace_for_message_type("camt.053"),
378 "urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"
379 );
380 }
381
382 #[test]
383 fn test_xml_config_default() {
384 let config = XmlConfig::default();
385 assert!(config.include_declaration);
386 assert!(config.pretty_print);
387 assert_eq!(config.indent, " ");
388 assert!(!config.include_schema_location);
389 }
390}