use crate::{Error as FrameError, encode};
use hl7_2 as v2;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AckCode {
Accept,
Error,
Reject,
CommitAccept,
CommitError,
CommitReject,
}
impl AckCode {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
AckCode::Accept => "AA",
AckCode::Error => "AE",
AckCode::Reject => "AR",
AckCode::CommitAccept => "CA",
AckCode::CommitError => "CE",
AckCode::CommitReject => "CR",
}
}
#[must_use]
pub fn is_accept(self) -> bool {
matches!(self, AckCode::Accept | AckCode::CommitAccept)
}
}
impl fmt::Display for AckCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Error {
NotText,
NotHl7(v2::Error),
Build(v2::Error),
Framing(FrameError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::NotText => write!(f, "the received payload is not valid UTF-8"),
Error::NotHl7(error) => {
write!(f, "the received payload is not an HL7 message: {error}")
}
Error::Build(error) => write!(f, "could not build the acknowledgement: {error}"),
Error::Framing(error) => write!(f, "{error}"),
}
}
}
impl std::error::Error for Error {}
impl From<FrameError> for Error {
fn from(error: FrameError) -> Error {
Error::Framing(error)
}
}
pub fn acknowledge(
payload: &[u8],
code: AckCode,
control_id: &str,
timestamp: &str,
) -> Result<Vec<u8>, Error> {
let message = parse(payload)?;
let ack = acknowledge_message(&message, code, control_id, timestamp).map_err(Error::Build)?;
Ok(encode(ack.to_er7().as_bytes()))
}
pub fn acknowledge_message(
message: &v2::Message,
code: AckCode,
control_id: &str,
timestamp: &str,
) -> Result<v2::Message, v2::Error> {
v2::builder::acknowledge(message, code.as_str(), control_id, timestamp).build()
}
#[cfg(feature = "clock")]
pub fn acknowledge_now(payload: &[u8], code: AckCode, control_id: &str) -> Result<Vec<u8>, Error> {
acknowledge(payload, code, control_id, &now())
}
#[cfg(feature = "clock")]
#[must_use]
pub fn now() -> String {
chrono::Local::now().format("%Y%m%d%H%M%S").to_string()
}
pub fn parse(payload: &[u8]) -> Result<v2::Message, Error> {
let text = std::str::from_utf8(payload).map_err(|_| Error::NotText)?;
v2::parse(text).map_err(Error::NotHl7)
}
#[cfg(test)]
mod tests {
use super::*;
const RECEIVED: &str =
"MSH|^~\\&|LAB|ACME|EHR|CLINIC|20260814080000||ORU^R01|99|P|2.5\rPID|1\rOBR|1";
#[test]
fn answers_a_message_with_its_own_control_id() {
let frame = acknowledge(
RECEIVED.as_bytes(),
AckCode::Accept,
"ACK00001",
"20260814080100",
)
.unwrap();
let text = String::from_utf8(crate::decode(&frame).unwrap().to_vec()).unwrap();
assert_eq!(
text,
"MSH|^~\\&|EHR|CLINIC|LAB|ACME|20260814080100||ACK^R01^ACK|ACK00001|P|2.5\r\
MSA|AA|99"
);
}
#[test]
fn sender_and_receiver_change_places() {
let frame = acknowledge(RECEIVED.as_bytes(), AckCode::Accept, "1", "20260814").unwrap();
let ack = parse(crate::decode(&frame).unwrap()).unwrap();
assert_eq!(ack.get("MSH-3.1").unwrap().as_deref(), Some("EHR"));
assert_eq!(ack.get("MSH-5.1").unwrap().as_deref(), Some("LAB"));
assert_eq!(ack.get("MSH-6.1").unwrap().as_deref(), Some("ACME"));
}
#[test]
fn every_code_reaches_msa_1() {
for (code, expected) in [
(AckCode::Accept, "AA"),
(AckCode::Error, "AE"),
(AckCode::Reject, "AR"),
(AckCode::CommitAccept, "CA"),
(AckCode::CommitError, "CE"),
(AckCode::CommitReject, "CR"),
] {
let frame = acknowledge(RECEIVED.as_bytes(), code, "1", "20260814").unwrap();
let ack = parse(crate::decode(&frame).unwrap()).unwrap();
assert_eq!(ack.get("MSA-1").unwrap().as_deref(), Some(expected));
assert_eq!(code.to_string(), expected);
}
assert!(AckCode::Accept.is_accept());
assert!(AckCode::CommitAccept.is_accept());
assert!(!AckCode::Error.is_accept());
}
#[test]
fn answers_in_the_release_the_sender_spoke() {
let received = "MSH|^~\\&|LAB|ACME|EHR|CLINIC|20260814||ORU^R01|99|P|2.3\rPID|1";
let frame = acknowledge(received.as_bytes(), AckCode::Accept, "1", "20260814").unwrap();
let ack = parse(crate::decode(&frame).unwrap()).unwrap();
assert_eq!(ack.version(), v2::Version::V2_3);
assert_eq!(ack.get("MSH-12").unwrap().as_deref(), Some("2.3"));
}
#[test]
fn a_payload_that_is_not_a_message_says_which_way_it_failed() {
assert_eq!(
acknowledge(&[0xff, 0xfe], AckCode::Accept, "1", "2"),
Err(Error::NotText)
);
match acknowledge(b"not hl7", AckCode::Accept, "1", "2") {
Err(Error::NotHl7(_)) => {}
other => panic!("expected a parse failure, got {other:?}"),
}
}
#[test]
fn a_receiver_can_say_what_was_wrong() {
let message = v2::parse(RECEIVED).unwrap();
let mut nack =
acknowledge_message(&message, AckCode::Error, "N1", "20260814080100").unwrap();
nack.set("MSA-3", "OBR-4 is required").unwrap();
assert!(nack.to_er7().contains("MSA|AE|99|OBR-4 is required"));
}
#[cfg(feature = "clock")]
#[test]
fn the_clock_feature_fills_in_the_timestamp() {
let stamp = now();
assert_eq!(stamp.len(), 14, "{stamp}");
assert!(stamp.bytes().all(|b| b.is_ascii_digit()), "{stamp}");
let frame = acknowledge_now(RECEIVED.as_bytes(), AckCode::Accept, "1").unwrap();
let ack = parse(crate::decode(&frame).unwrap()).unwrap();
assert_eq!(ack.get("MSH-7.1").unwrap().unwrap().len(), 14);
assert_eq!(ack.validate(), []);
}
}