use crate::xml;
pub const ACK_INTERACTION: &str = "MCCI_IN000002UV01";
const ACCEPTED: [&str; 2] = ["AA", "CA"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
Accepted,
Rejected(String),
}
impl Outcome {
#[must_use]
pub fn is_accepted(&self) -> bool {
matches!(self, Outcome::Accepted)
}
}
#[must_use]
pub fn success(control_id: &str) -> String {
format!(
concat!(
r#"<soapenv:Envelope xmlns:soapenv="{}">"#,
"<soapenv:Body>",
r#"<{} xmlns="urn:hl7-org:v3">"#,
"<acknowledgement>",
r#"<typeCode code="AA"/>"#,
"<targetMessage>",
r#"<id extension="{}"/>"#,
"</targetMessage>",
"</acknowledgement>",
"</{}>",
"</soapenv:Body>",
"</soapenv:Envelope>",
),
crate::fault::SOAP_NS,
ACK_INTERACTION,
xml::escape(control_id),
ACK_INTERACTION,
)
}
#[must_use]
pub fn evaluate(status: u16, body: &str) -> Outcome {
if !(200..300).contains(&status) {
return Outcome::Rejected(format!("HTTP {status}"));
}
let Ok(root) = xml::parse(body) else {
return Outcome::Accepted;
};
if let Some(fault) = root.find("Fault") {
let code = fault
.child("faultcode")
.map(|element| element.text.trim())
.unwrap_or_default();
let reason = fault
.child("faultstring")
.map(|element| element.text.trim())
.unwrap_or_default();
return Outcome::Rejected(match (code.is_empty(), reason.is_empty()) {
(true, true) => "SOAP fault".to_string(),
(true, false) => reason.to_string(),
(false, true) => code.to_string(),
(false, false) => format!("{code}: {reason}"),
});
}
if let Some(acknowledgement) = root.find("acknowledgement") {
let code = acknowledgement
.child("typeCode")
.and_then(|type_code| type_code.attribute("code"))
.unwrap_or_default();
if ACCEPTED.contains(&code) {
return Outcome::Accepted;
}
return Outcome::Rejected(format!("typeCode {code}"));
}
Outcome::Accepted
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_success_response_names_the_message_it_answers() {
let body = success("CTRL7");
let root = xml::parse(&body).unwrap();
assert_eq!(root.find("typeCode").unwrap().attribute("code"), Some("AA"));
assert_eq!(
root.find("targetMessage")
.unwrap()
.child("id")
.unwrap()
.attribute("extension"),
Some("CTRL7")
);
assert_eq!(evaluate(200, &body), Outcome::Accepted);
}
#[test]
fn a_control_id_cannot_break_out_of_the_response() {
let body = success("a<b>&c");
assert!(body.contains("a<b>&c"));
assert_eq!(
xml::parse(&body)
.unwrap()
.find("targetMessage")
.unwrap()
.child("id")
.unwrap()
.attribute("extension"),
Some("a<b>&c")
);
}
#[test]
fn a_failing_status_is_a_rejection_whatever_the_body_says() {
assert_eq!(
evaluate(500, &success("1")),
Outcome::Rejected("HTTP 500".into())
);
assert!(!evaluate(404, "").is_accepted());
}
#[test]
fn a_fault_under_http_200_is_still_a_rejection() {
let body = crate::Fault::validation("bad interaction").to_envelope();
assert_eq!(
evaluate(200, &body),
Outcome::Rejected("Client.Validation: bad interaction".into())
);
}
#[test]
fn an_hl7_acknowledgement_type_code_decides_when_one_is_returned() {
let with_code = |code: &str| {
format!(
"<Envelope><Body><MCCI_IN000002UV01><acknowledgement>\
<typeCode code=\"{code}\"/></acknowledgement></MCCI_IN000002UV01></Body></Envelope>"
)
};
assert!(evaluate(200, &with_code("AA")).is_accepted());
assert!(evaluate(200, &with_code("CA")).is_accepted());
assert!(!evaluate(200, &with_code("AE")).is_accepted());
assert!(!evaluate(200, &with_code("AR")).is_accepted());
assert_eq!(
evaluate(200, &with_code("AE")),
Outcome::Rejected("typeCode AE".into())
);
}
#[test]
fn a_minimal_endpoint_that_says_nothing_is_believed() {
assert!(evaluate(200, "").is_accepted());
assert!(evaluate(202, "OK").is_accepted());
assert!(evaluate(200, "<Envelope><Body/></Envelope>").is_accepted());
}
}