mx20022_parse/
envelope.rs1use crate::ParseError;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct MessageId {
22 pub family: String,
24 pub msg_id: String,
26 pub variant: String,
28 pub version: String,
30}
31
32impl MessageId {
33 pub fn dotted(&self) -> String {
35 format!(
36 "{}.{}.{}.{}",
37 self.family, self.msg_id, self.variant, self.version
38 )
39 }
40}
41
42const NS_PREFIX: &str = "urn:iso:std:iso:20022:tech:xsd:";
44
45pub fn parse_namespace(ns: &str) -> Result<MessageId, ParseError> {
54 let suffix = ns.strip_prefix(NS_PREFIX).ok_or_else(|| {
55 ParseError::InvalidEnvelope(format!(
56 "namespace does not start with \"{NS_PREFIX}\": {ns}"
57 ))
58 })?;
59
60 let parts: Vec<&str> = suffix.splitn(4, '.').collect();
62 if parts.len() != 4 {
63 return Err(ParseError::InvalidEnvelope(format!(
64 "expected 4 dot-separated components in namespace suffix \"{suffix}\""
65 )));
66 }
67
68 Ok(MessageId {
69 family: parts[0].to_owned(),
70 msg_id: parts[1].to_owned(),
71 variant: parts[2].to_owned(),
72 version: parts[3].to_owned(),
73 })
74}
75
76pub fn detect_message_type(xml: &str) -> Result<MessageId, ParseError> {
99 let mut search = xml;
101 while let Some(pos) = search.find("xmlns") {
102 let after_xmlns = &search[pos + 5..];
103
104 let after_eq = if let Some(p) = after_xmlns.find('=') {
106 let between = &after_xmlns[..p];
108 if between.contains('>') || between.contains('<') {
109 search = &search[pos + 5..];
110 continue;
111 }
112 &after_xmlns[p + 1..]
113 } else {
114 search = &search[pos + 5..];
115 continue;
116 };
117
118 let after_eq = after_eq.trim_start();
120 let Some(quote_char @ ('"' | '\'')) = after_eq.chars().next() else {
121 search = &search[pos + 5..];
122 continue;
123 };
124
125 let after_open_quote = &after_eq[1..];
126 if let Some(close_pos) = after_open_quote.find(quote_char) {
127 let ns_value = &after_open_quote[..close_pos];
128 if ns_value.starts_with(NS_PREFIX) {
129 return parse_namespace(ns_value);
130 }
131 }
132
133 search = &search[pos + 5..];
135 }
136
137 Err(ParseError::InvalidEnvelope(
138 "no ISO 20022 namespace found in XML document".to_owned(),
139 ))
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn parse_namespace_pacs_008() {
148 let id = parse_namespace("urn:iso:std:iso:20022:tech:xsd:pacs.008.001.13").unwrap();
149 assert_eq!(id.family, "pacs");
150 assert_eq!(id.msg_id, "008");
151 assert_eq!(id.variant, "001");
152 assert_eq!(id.version, "13");
153 assert_eq!(id.dotted(), "pacs.008.001.13");
154 }
155
156 #[test]
157 fn parse_namespace_head_001() {
158 let id = parse_namespace("urn:iso:std:iso:20022:tech:xsd:head.001.001.04").unwrap();
159 assert_eq!(id.family, "head");
160 assert_eq!(id.msg_id, "001");
161 assert_eq!(id.variant, "001");
162 assert_eq!(id.version, "04");
163 }
164
165 #[test]
166 fn parse_namespace_invalid_prefix() {
167 let err = parse_namespace("urn:something:else:pacs.008.001.13").unwrap_err();
168 assert!(matches!(err, ParseError::InvalidEnvelope(_)));
169 }
170
171 #[test]
172 fn parse_namespace_wrong_component_count() {
173 let err = parse_namespace("urn:iso:std:iso:20022:tech:xsd:pacs.008.001").unwrap_err();
174 assert!(matches!(err, ParseError::InvalidEnvelope(_)));
175 }
176
177 #[test]
178 fn detect_message_type_document_root() {
179 let xml = r#"<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.13"><FIToFICstmrCdtTrf/></Document>"#;
180 let id = detect_message_type(xml).unwrap();
181 assert_eq!(id.dotted(), "pacs.008.001.13");
182 }
183
184 #[test]
185 fn detect_message_type_apphdr_root() {
186 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
187<AppHdr xmlns="urn:iso:std:iso:20022:tech:xsd:head.001.001.04">
188 <Fr/>
189</AppHdr>"#;
190 let id = detect_message_type(xml).unwrap();
191 assert_eq!(id.dotted(), "head.001.001.04");
192 }
193
194 #[test]
195 fn detect_message_type_no_namespace_returns_error() {
196 let xml = r#"<Document><FIToFICstmrCdtTrf/></Document>"#;
197 assert!(detect_message_type(xml).is_err());
198 }
199
200 #[test]
201 fn detect_message_type_non_iso_namespace_returns_error() {
202 let xml = r#"<root xmlns="http://example.com/other"/>"#;
203 assert!(detect_message_type(xml).is_err());
204 }
205
206 #[test]
207 fn detect_message_type_pacs_002() {
208 let xml = r#"<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.002.001.14"/>"#;
209 let id = detect_message_type(xml).unwrap();
210 assert_eq!(id.family, "pacs");
211 assert_eq!(id.msg_id, "002");
212 assert_eq!(id.version, "14");
213 }
214}