use crate::ParseError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageId {
pub family: String,
pub msg_id: String,
pub variant: String,
pub version: String,
}
impl MessageId {
pub fn dotted(&self) -> String {
format!(
"{}.{}.{}.{}",
self.family, self.msg_id, self.variant, self.version
)
}
}
const NS_PREFIX: &str = "urn:iso:std:iso:20022:tech:xsd:";
pub fn parse_namespace(ns: &str) -> Result<MessageId, ParseError> {
let suffix = ns.strip_prefix(NS_PREFIX).ok_or_else(|| {
ParseError::InvalidEnvelope(format!(
"namespace does not start with \"{NS_PREFIX}\": {ns}"
))
})?;
let parts: Vec<&str> = suffix.splitn(4, '.').collect();
if parts.len() != 4 {
return Err(ParseError::InvalidEnvelope(format!(
"expected 4 dot-separated components in namespace suffix \"{suffix}\""
)));
}
Ok(MessageId {
family: parts[0].to_owned(),
msg_id: parts[1].to_owned(),
variant: parts[2].to_owned(),
version: parts[3].to_owned(),
})
}
pub fn detect_message_type(xml: &str) -> Result<MessageId, ParseError> {
let mut search = xml;
while let Some(pos) = search.find("xmlns") {
let after_xmlns = &search[pos + 5..];
let after_eq = if let Some(p) = after_xmlns.find('=') {
let between = &after_xmlns[..p];
if between.contains('>') || between.contains('<') {
search = &search[pos + 5..];
continue;
}
&after_xmlns[p + 1..]
} else {
search = &search[pos + 5..];
continue;
};
let after_eq = after_eq.trim_start();
let Some(quote_char @ ('"' | '\'')) = after_eq.chars().next() else {
search = &search[pos + 5..];
continue;
};
let after_open_quote = &after_eq[1..];
if let Some(close_pos) = after_open_quote.find(quote_char) {
let ns_value = &after_open_quote[..close_pos];
if ns_value.starts_with(NS_PREFIX) {
return parse_namespace(ns_value);
}
}
search = &search[pos + 5..];
}
Err(ParseError::InvalidEnvelope(
"no ISO 20022 namespace found in XML document".to_owned(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_namespace_pacs_008() {
let id = parse_namespace("urn:iso:std:iso:20022:tech:xsd:pacs.008.001.13").unwrap();
assert_eq!(id.family, "pacs");
assert_eq!(id.msg_id, "008");
assert_eq!(id.variant, "001");
assert_eq!(id.version, "13");
assert_eq!(id.dotted(), "pacs.008.001.13");
}
#[test]
fn parse_namespace_head_001() {
let id = parse_namespace("urn:iso:std:iso:20022:tech:xsd:head.001.001.04").unwrap();
assert_eq!(id.family, "head");
assert_eq!(id.msg_id, "001");
assert_eq!(id.variant, "001");
assert_eq!(id.version, "04");
}
#[test]
fn parse_namespace_invalid_prefix() {
let err = parse_namespace("urn:something:else:pacs.008.001.13").unwrap_err();
assert!(matches!(err, ParseError::InvalidEnvelope(_)));
}
#[test]
fn parse_namespace_wrong_component_count() {
let err = parse_namespace("urn:iso:std:iso:20022:tech:xsd:pacs.008.001").unwrap_err();
assert!(matches!(err, ParseError::InvalidEnvelope(_)));
}
#[test]
fn detect_message_type_document_root() {
let xml = r#"<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.13"><FIToFICstmrCdtTrf/></Document>"#;
let id = detect_message_type(xml).unwrap();
assert_eq!(id.dotted(), "pacs.008.001.13");
}
#[test]
fn detect_message_type_apphdr_root() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<AppHdr xmlns="urn:iso:std:iso:20022:tech:xsd:head.001.001.04">
<Fr/>
</AppHdr>"#;
let id = detect_message_type(xml).unwrap();
assert_eq!(id.dotted(), "head.001.001.04");
}
#[test]
fn detect_message_type_no_namespace_returns_error() {
let xml = r#"<Document><FIToFICstmrCdtTrf/></Document>"#;
assert!(detect_message_type(xml).is_err());
}
#[test]
fn detect_message_type_non_iso_namespace_returns_error() {
let xml = r#"<root xmlns="http://example.com/other"/>"#;
assert!(detect_message_type(xml).is_err());
}
#[test]
fn detect_message_type_pacs_002() {
let xml = r#"<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.002.001.14"/>"#;
let id = detect_message_type(xml).unwrap();
assert_eq!(id.family, "pacs");
assert_eq!(id.msg_id, "002");
assert_eq!(id.version, "14");
}
}