#![warn(missing_docs, clippy::pedantic)]
pub mod builder;
pub mod dictionary;
pub mod generic;
pub mod json;
pub mod message;
pub mod structure;
pub mod typed;
pub mod validate;
pub mod version;
pub use builder::Builder;
pub use dictionary::Dictionary;
pub use generic::Node;
pub use message::Message;
pub use typed::{FromHl7, FromHl7Text, FromHl7Value, Raw, ToHl7, ToHl7Text, ToHl7Value};
pub use validate::{Diagnostic, Severity};
pub use version::Version;
#[cfg(feature = "derive")]
pub use hl7_2_derive::{FromHl7, ToHl7};
pub use er7;
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub enum Error {
Empty,
MissingMsh,
BadMshHeader(String),
Path(String),
NoSuchSegment {
name: String,
occurrence: usize,
},
UnwritablePath(String),
MissingField {
path: String,
},
BadValue {
path: String,
expected: String,
found: String,
},
Dictionary(dictionary::Error),
Invalid(Vec<Diagnostic>),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Empty => write!(f, "input contains no HL7 segments"),
Error::MissingMsh => write!(f, "message does not start with an MSH segment"),
Error::BadMshHeader(detail) => write!(f, "malformed MSH header: {detail}"),
Error::Path(detail) => write!(f, "invalid HL7 path: {detail}"),
Error::NoSuchSegment { name, occurrence } => {
write!(
f,
"message has no {name} segment at occurrence {occurrence}"
)
}
Error::UnwritablePath(detail) => write!(f, "cannot write to this path: {detail}"),
Error::MissingField { path } => write!(f, "{path}: required value is missing"),
Error::BadValue {
path,
expected,
found,
} => write!(f, "{path}: expected {expected}, found {found:?}"),
Error::Dictionary(error) => write!(f, "invalid dictionary: {error}"),
Error::Invalid(diagnostics) => {
write!(f, "message failed validation:")?;
for diagnostic in diagnostics {
write!(f, "\n {diagnostic}")?;
}
Ok(())
}
}
}
}
impl std::error::Error for Error {}
impl From<er7::Error> for Error {
fn from(error: er7::Error) -> Error {
match error {
er7::Error::Empty => Error::Empty,
er7::Error::MissingHeader(_) => Error::MissingMsh,
er7::Error::BadHeader(detail) => Error::BadMshHeader(detail),
er7::Error::BadPath(detail) => Error::Path(detail),
}
}
}
impl From<dictionary::Error> for Error {
fn from(error: dictionary::Error) -> Error {
Error::Dictionary(error)
}
}
#[derive(Debug, Clone, Default)]
pub struct Options {
pub version: Option<Version>,
pub dictionary: Option<Arc<Dictionary>>,
pub strict: bool,
}
impl Options {
#[must_use]
pub fn new() -> Options {
Options::default()
}
#[must_use]
pub fn with_version(mut self, version: Version) -> Options {
self.version = Some(version);
self
}
#[must_use]
pub fn with_dictionary(mut self, dictionary: Arc<Dictionary>) -> Options {
self.dictionary = Some(dictionary);
self
}
#[must_use]
pub fn strict(mut self) -> Options {
self.strict = true;
self
}
}
pub fn parse(text: &str) -> Result<Message, Error> {
Message::parse(text, &Options::default())
}
pub fn parse_with_options(text: &str, options: &Options) -> Result<Message, Error> {
Message::parse(text, options)
}
pub fn split_messages(text: &str) -> Vec<String> {
er7::split_messages(&normalize(text))
.into_iter()
.map(str::to_string)
.collect()
}
pub(crate) fn normalize(text: &str) -> String {
text.trim_start_matches('\u{feff}')
.split(['\r', '\n'])
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<&str>>()
.join("\r")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_before_parsing() {
assert_eq!(normalize("MSH|A\r\n\r\n PID|1 \n"), "MSH|A\rPID|1");
assert_eq!(normalize("\u{feff}MSH|A"), "MSH|A");
assert!(parse(" MSH|^~\\&|APP||||1||ACK|1|P|2.5\r MSA|AA|1").is_ok());
}
#[test]
fn maps_er7_errors_onto_this_crates_type() {
assert!(matches!(parse(""), Err(Error::Empty)));
assert!(matches!(parse("PID|1"), Err(Error::MissingMsh)));
assert!(matches!(parse("MSH"), Err(Error::BadMshHeader(_))));
}
#[test]
fn strict_mode_turns_diagnostics_into_a_failure() {
let text = "MSH|^~\\&|A||||20240101||ACK^A01|1|P|2.5"; assert!(parse(text).is_ok(), "lenient by default");
let strict = Options::new().strict();
match parse_with_options(text, &strict) {
Err(Error::Invalid(diagnostics)) => {
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].kind, validate::Kind::SegmentMissing);
}
other => panic!("expected a validation failure, got {other:?}"),
}
let text = "MSH|^~\\&|A||||20240101||ZZZ^Z01|1|P|2.5";
assert!(parse_with_options(text, &strict).is_ok());
}
#[test]
fn forcing_a_version_overrides_the_header() {
let text = "MSH|^~\\&|A||||1||ACK^A01|1|P|2.5\rMSA|AA|1";
let options = Options::new().with_version(Version::V2_3);
let message = parse_with_options(text, &options).unwrap();
assert_eq!(message.version(), Version::V2_3);
assert_eq!(message.dictionary().segment_fields("ERR").unwrap().len(), 1);
}
#[test]
fn splits_batches_into_messages() {
let batch = "FHS|^~\\&|A\rBHS|^~\\&|A\r\
MSH|^~\\&|A||||1||ACK|1|P|2.5\rMSA|AA|1\r\
MSH|^~\\&|A||||2||ACK|2|P|2.5\rMSA|AA|2\r\
BTS|2\rFTS|1";
let messages = split_messages(batch);
assert_eq!(messages.len(), 2);
assert!(messages[1].contains("MSA|AA|2"));
assert!(messages.iter().all(|text| parse(text).is_ok()));
}
}