Skip to main content

hl7_2_from_xml_into_er7/
lib.rs

1//! Convert HL7 v2.5 messages from the HL7 v2.xml XML representation
2//! (`urn:hl7-org:v2xml`) back to ER7 (pipe-delimited) encoding.
3//!
4//! This is the inverse of the sibling
5//! [`hl7-2-from-er7-into-xml`](https://github.com/hl7-rust/hl7-rust/tree/main/hl7-2-from-er7-into-xml)
6//! crate. That crate names every XML element after either an HL7 v2.5 data
7//! type or a bare position, but in both cases the number after an element
8//! name's *last* dot is always the 1-based position at that level — field
9//! under a segment, component under a field, subcomponent under a
10//! component. Reconstruction leans on that one fact rather than an HL7
11//! v2.5 data-type dictionary, so this crate carries none; see
12//! `spec/index.md` for the exact rules and their limits.
13//!
14//! ```
15//! let xml = r#"<ORM_O01 xmlns="urn:hl7-org:v2xml">
16//!   <MSH>
17//!     <MSH.1>|</MSH.1>
18//!     <MSH.2>^~\&amp;</MSH.2>
19//!     <MSH.9><MSG.1>ORM</MSG.1><MSG.2>O01</MSG.2></MSH.9>
20//!   </MSH>
21//!   <ORM_O01.PATIENT>
22//!     <PID><PID.5><XPN.1><FN.1>TEST</FN.1></XPN.1><XPN.2>FOUAZ</XPN.2></PID.5></PID>
23//!   </ORM_O01.PATIENT>
24//! </ORM_O01>"#;
25//! let er7 = hl7_2_from_xml_into_er7::convert(xml).unwrap();
26//! assert!(er7.starts_with(r"MSH|^~\&|"));
27//! assert!(er7.contains("PID|||||TEST^FOUAZ"));
28//! ```
29
30#![warn(missing_docs, clippy::pedantic)]
31// XML literals keep their `r#"..."#` delimiters even where no `"` currently
32// forces them: these are documents, and adding a quoted attribute to one
33// should not also mean changing its delimiter.
34#![allow(clippy::needless_raw_string_hashes)]
35
36pub mod reconstruct;
37/// The XML reader this crate is built on, re-exported so callers can name
38/// [`xml::Element`] without adding their own dependency.
39///
40/// Until 0.5.0 this was a module inside this crate, with a `Node` type of
41/// its own. It is now the standalone `hl7-2-xml-lite-helper` crate, which
42/// reads the same subset; the type is [`xml::Element`], its children are
43/// `children` rather than `kids`, and its text is a `String` that is empty
44/// rather than an `Option` that is `None`.
45pub use hl7_2_xml_lite_helper as xml;
46
47/// The ER7 encoding layer this crate writes onto, re-exported so callers
48/// can name [`er7::Message`], [`er7::Separators`], and
49/// [`er7::RenderOptions`] without adding their own dependency.
50pub use er7;
51
52use std::fmt;
53
54/// Errors that can occur while turning v2.xml into an [`er7::Message`].
55///
56/// As with the forward crate, this is deliberately narrow: below the
57/// header, no shape of input is rejected — an element with an unparseable
58/// position, an unexpected type, or a segment this crate has never heard
59/// of all reconstruct into *something* rather than failing (see
60/// `spec/index.md` §5). Only a document with no usable `MSH`/`FHS`/`BHS`
61/// header, or that is not well-formed XML at all, produces an `Err`.
62#[derive(Debug)]
63pub enum Hl7Error {
64    /// The input is not well-formed XML.
65    Xml(xml::Error),
66    /// The document has no segments at all (an empty or absent root
67    /// element).
68    Empty,
69    /// The first segment is not `MSH`, `FHS`, or `BHS`.
70    MissingMsh,
71    /// The header segment's `.1`/`.2` fields don't declare a usable
72    /// delimiter set.
73    BadMshHeader(String),
74}
75
76impl fmt::Display for Hl7Error {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Hl7Error::Xml(e) => write!(f, "malformed XML: {e}"),
80            Hl7Error::Empty => write!(f, "document contains no HL7 segments"),
81            Hl7Error::MissingMsh => write!(f, "document does not start with an MSH segment"),
82            Hl7Error::BadMshHeader(detail) => write!(f, "malformed MSH header: {detail}"),
83        }
84    }
85}
86
87impl std::error::Error for Hl7Error {}
88
89impl From<xml::Error> for Hl7Error {
90    fn from(error: xml::Error) -> Hl7Error {
91        Hl7Error::Xml(error)
92    }
93}
94
95/// Parse a v2.xml document into an [`er7::Message`], reconstructing its
96/// full ER7 value tree — segments, fields, repetitions, components, and
97/// subcomponents.
98///
99/// Prefer this over [`convert`] when the caller wants to query or edit the
100/// message (via `er7`'s own API) rather than just its ER7 text.
101/// # Errors
102///
103/// [`Hl7Error`] when the document cannot be read as XML, or when what it
104/// contains is not an HL7 message: no segments, or a first segment that is
105/// not MSH.
106pub fn parse(xml_text: &str) -> Result<er7::Message, Hl7Error> {
107    let root = xml::parse(xml_text)?;
108    reconstruct::reconstruct(&root)
109}
110
111/// Convert one v2.xml document to ER7 text, with default rendering:
112/// carriage-return segment terminators, and no trailing terminator.
113/// # Errors
114///
115/// [`Hl7Error`] when the document cannot be read as XML, or when what it
116/// contains is not an HL7 message: no segments, or a first segment that is
117/// not MSH.
118pub fn convert(xml_text: &str) -> Result<String, Hl7Error> {
119    convert_with_options(xml_text, er7::RenderOptions::default())
120}
121
122/// Convert one v2.xml document to ER7 text, choosing the segment
123/// terminator and whether the last segment gets one too — see
124/// [`er7::RenderOptions`].
125/// # Errors
126///
127/// [`Hl7Error`] when the document cannot be read as XML, or when what it
128/// contains is not an HL7 message: no segments, or a first segment that is
129/// not MSH.
130pub fn convert_with_options(
131    xml_text: &str,
132    options: er7::RenderOptions,
133) -> Result<String, Hl7Error> {
134    Ok(parse(xml_text)?.to_er7_with(options))
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn maps_xml_errors_onto_this_crates_type() {
143        assert!(matches!(convert("not xml"), Err(Hl7Error::Xml(_))));
144    }
145
146    #[test]
147    fn maps_missing_header_errors() {
148        assert!(matches!(
149            convert("<X><PID><PID.1>1</PID.1></PID></X>"),
150            Err(Hl7Error::MissingMsh)
151        ));
152        assert!(matches!(convert("<X></X>"), Err(Hl7Error::Empty)));
153    }
154}