Skip to main content

hl7_2_from_json_into_er7/
lib.rs

1//! Convert HL7 v2.5 messages from the typed JSON representation the
2//! sibling `hl7-2-from-er7-into-json` crate produces back to ER7
3//! (pipe-delimited) encoding.
4//!
5//! This is the inverse of that crate. It names every JSON key after either
6//! an HL7 v2.5 data type or a bare position, but in both cases the number
7//! after a key's *last* dot is always the 1-based position at that level —
8//! field under a segment, component under a field, subcomponent under a
9//! component. Reconstruction leans on that one fact rather than an HL7
10//! v2.5 data-type dictionary, so this crate carries none; see
11//! `spec/index.md` for the exact rules and their limits.
12//!
13//! ```
14//! let json = r#"{
15//!   "ORM_O01": {
16//!     "MSH": { "MSH.1": "|", "MSH.2": "^~\\&", "MSH.9": {"MSG.1": "ORM", "MSG.2": "O01"} },
17//!     "ORM_O01.PATIENT": {
18//!       "PID": { "PID.5": { "XPN.1": {"FN.1": "TEST"}, "XPN.2": "FOUAZ" } }
19//!     }
20//!   }
21//! }"#;
22//! let er7 = hl7_2_from_json_into_er7::convert(json).unwrap();
23//! assert!(er7.starts_with(r"MSH|^~\&|"));
24//! assert!(er7.contains("PID|||||TEST^FOUAZ"));
25//! ```
26
27#![warn(missing_docs, clippy::pedantic)]
28
29pub mod json;
30pub mod reconstruct;
31
32/// The ER7 encoding layer this crate writes onto, re-exported so callers
33/// can name [`er7::Message`], [`er7::Separators`], and
34/// [`er7::RenderOptions`] without adding their own dependency.
35pub use er7;
36
37use std::fmt;
38
39/// Errors that can occur while turning JSON into an [`er7::Message`].
40///
41/// As with the forward crate, this is deliberately narrow: below the
42/// header, no shape of input is rejected — a key with an unparseable
43/// position, an unexpected scalar type, or a segment this crate has never
44/// heard of all reconstruct into *something* rather than failing (see
45/// `spec/index.md` §5). Only a document with no usable
46/// `{"...": {"MSH": ...}}` shape, or that is not well-formed JSON at all,
47/// produces an `Err`.
48#[derive(Debug)]
49pub enum Hl7Error {
50    /// The input is not well-formed JSON.
51    Json(json::JsonError),
52    /// The document isn't shaped like a converted message: not a
53    /// single-key object over an object of segments, or that object has no
54    /// segment entries at all.
55    Empty,
56    /// The first segment is not `MSH`, `FHS`, or `BHS`.
57    MissingMsh,
58    /// The header segment's `.1`/`.2` fields don't declare a usable
59    /// delimiter set.
60    BadMshHeader(String),
61}
62
63impl fmt::Display for Hl7Error {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            Hl7Error::Json(e) => write!(f, "malformed JSON: {e}"),
67            Hl7Error::Empty => write!(f, "document contains no HL7 segments"),
68            Hl7Error::MissingMsh => write!(f, "document does not start with an MSH segment"),
69            Hl7Error::BadMshHeader(detail) => write!(f, "malformed MSH header: {detail}"),
70        }
71    }
72}
73
74impl std::error::Error for Hl7Error {}
75
76impl From<json::JsonError> for Hl7Error {
77    fn from(error: json::JsonError) -> Hl7Error {
78        Hl7Error::Json(error)
79    }
80}
81
82/// Parse a converted-JSON document into an [`er7::Message`], reconstructing
83/// its full ER7 value tree — segments, fields, repetitions, components,
84/// and subcomponents.
85///
86/// Prefer this over [`convert`] when the caller wants to query or edit the
87/// message (via `er7`'s own API) rather than just its ER7 text.
88/// # Errors
89///
90/// [`Hl7Error`] when the text is not valid JSON, or when what it contains
91/// is not an HL7 message: no segments, or a first segment that is not MSH.
92pub fn parse(json_text: &str) -> Result<er7::Message, Hl7Error> {
93    let document = json::parse_document(json_text)?;
94    reconstruct::reconstruct(&document)
95}
96
97/// Convert one JSON document to ER7 text, with default rendering:
98/// carriage-return segment terminators, and no trailing terminator.
99/// # Errors
100///
101/// [`Hl7Error`] when the text is not valid JSON, or when what it contains
102/// is not an HL7 message: no segments, or a first segment that is not MSH.
103pub fn convert(json_text: &str) -> Result<String, Hl7Error> {
104    convert_with_options(json_text, er7::RenderOptions::default())
105}
106
107/// Convert one JSON document to ER7 text, choosing the segment terminator
108/// and whether the last segment gets one too — see [`er7::RenderOptions`].
109/// # Errors
110///
111/// [`Hl7Error`] when the text is not valid JSON, or when what it contains
112/// is not an HL7 message: no segments, or a first segment that is not MSH.
113pub fn convert_with_options(
114    json_text: &str,
115    options: er7::RenderOptions,
116) -> Result<String, Hl7Error> {
117    Ok(parse(json_text)?.to_er7_with(options))
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn maps_json_errors_onto_this_crates_type() {
126        assert!(matches!(convert("not json"), Err(Hl7Error::Json(_))));
127    }
128
129    #[test]
130    fn maps_missing_header_errors() {
131        assert!(matches!(
132            convert(r#"{"X": {"PID": {"PID.1": "1"}}}"#),
133            Err(Hl7Error::MissingMsh)
134        ));
135        assert!(matches!(convert(r#"{"X": {}}"#), Err(Hl7Error::Empty)));
136        assert!(matches!(convert("{}"), Err(Hl7Error::Empty)));
137    }
138}