Skip to main content

hl7_net/
lib.rs

1//! A lightweight HL7 v2 parser/writer.
2//!
3//! This is an idiomatic Rust port of the
4//! [Efferent HL7-V2](https://github.com/Efferent-Health/HL7-V2) .NET library.
5//! The element tree ([`SubComponent`], [`Component`], [`Field`], [`Segment`],
6//! [`Message`]) is pure data; encoding/decoding is driven by an [`HL7Encoding`]
7//! threaded through the parse/serialize/value methods.
8//!
9//! # Example
10//!
11//! ```
12//! use hl7_net::Message;
13//!
14//! let text = "MSH|^~\\&|App|Fac|App2|Fac2|20200101000000||ADT^A01^ADT_A01|MSGID|P|2.5\r\
15//!             PID|1||PATID1234^5^M11||EVERYMAN^ADAM^A^III||19610615|M\r";
16//!
17//! let mut message = Message::with_message(text);
18//! assert!(message.parse(false).unwrap());
19//!
20//! assert_eq!(message.get_value("MSH.9.1").unwrap(), "ADT");
21//! assert_eq!(message.get_value("PID.5.1").unwrap(), "EVERYMAN");
22//! ```
23
24#![warn(missing_docs)]
25
26mod component;
27mod encoding;
28mod error;
29mod field;
30pub mod helper;
31mod message;
32mod segment;
33mod sub_component;
34
35pub use component::Component;
36pub use encoding::HL7Encoding;
37pub use error::Hl7Error;
38pub use field::Field;
39pub use message::Message;
40pub use segment::Segment;
41pub use sub_component::SubComponent;
42
43/// Compiles and runs the code examples in `README.md` as doctests.
44#[cfg(doctest)]
45#[doc = include_str!("../README.md")]
46struct ReadmeDoctests;
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    const SAMPLE: &str = "MSH|^~\\&|SendingApp|SendingFac|ReceivingApp|ReceivingFac|20200101120000||ADT^A01^ADT_A01|MSGID1234|P|2.5\r\
53PID|1||PATID1234^5^M11^ADT1^MR^GOOD HEALTH HOSPITAL~123456789^^^USSSA^SS||EVERYMAN^ADAM^A^III||19610615|M\r\
54NK1|1|NUCLEAR^NELDA^W|SPO^SPOUSE\r";
55
56    /// Parses [`SAMPLE`], asserting it round-trips, and returns the message.
57    fn parsed() -> Message {
58        let mut m = Message::with_message(SAMPLE);
59        assert!(m.parse(false).unwrap(), "message should round-trip");
60        m
61    }
62
63    /// MSH-derived metadata (version, structure, control ID, etc.) is populated.
64    #[test]
65    fn parses_and_extracts_metadata() {
66        let m = parsed();
67        assert_eq!(m.version, "2.5");
68        assert_eq!(m.message_structure, "ADT_A01");
69        assert_eq!(m.message_control_id, "MSGID1234");
70        assert_eq!(m.processing_id, "P");
71        assert_eq!(m.segment_count, 3);
72    }
73
74    /// Re-serializing a parsed message reproduces the original text exactly.
75    #[test]
76    fn round_trip_serialization_matches() {
77        let m = parsed();
78        assert_eq!(m.serialize().unwrap(), SAMPLE);
79    }
80
81    /// `get_value` resolves segment/field/component/subcomponent paths, with the
82    /// bare-field form returning the first repetition.
83    #[test]
84    fn get_value_paths() {
85        let m = parsed();
86        assert_eq!(m.get_value("MSH.9").unwrap(), "ADT^A01^ADT_A01");
87        assert_eq!(m.get_value("MSH.9.1").unwrap(), "ADT");
88        assert_eq!(m.get_value("MSH.9.3").unwrap(), "ADT_A01");
89        assert_eq!(m.get_value("PID.5.1").unwrap(), "EVERYMAN");
90        assert_eq!(m.get_value("PID.5.2").unwrap(), "ADAM");
91        // PID.3 has repetitions; the bare index returns the first repetition.
92        assert_eq!(m.get_value("PID.3.4").unwrap(), "ADT1");
93    }
94
95    /// The componentized / repetition flags reflect the parsed structure.
96    #[test]
97    fn flags() {
98        let m = parsed();
99        assert!(m.is_componentized("PID.5").unwrap());
100        assert!(m.has_repetitions("PID.3").unwrap());
101        assert!(!m.is_componentized("PID.1").unwrap());
102    }
103
104    /// `set_value` mutates the element tree and the change survives serialization.
105    #[test]
106    fn set_value_updates_tree() {
107        let mut m = parsed();
108        assert!(m.set_value("PID.5.1", "SMITH").unwrap());
109        assert_eq!(m.get_value("PID.5.1").unwrap(), "SMITH");
110        // Re-serialize and confirm the new value is present.
111        assert!(m.serialize().unwrap().contains("SMITH^ADAM"));
112    }
113
114    /// Encoding escapes the HL7 delimiters and decoding restores the original.
115    #[test]
116    fn encode_decode_round_trip() {
117        let enc = HL7Encoding::default();
118        let raw = "Smith & Sons | ^Special^";
119        let encoded = enc.encode(raw);
120        assert!(encoded.contains("\\T\\")); // & escaped
121        assert!(encoded.contains("\\F\\")); // | escaped
122        assert!(encoded.contains("\\S\\")); // ^ escaped
123        assert_eq!(enc.decode(&encoded), raw);
124    }
125
126    /// A `""` value decodes to `None` and `None` encodes back to `""`.
127    #[test]
128    fn present_but_null() {
129        let enc = HL7Encoding::default();
130        let sub = SubComponent::new("\"\"");
131        assert_eq!(sub.value(&enc), None);
132        assert_eq!(enc.encode_opt(None), "\"\"");
133    }
134
135    /// A generated ACK swaps sender/receiver and carries an `AA` MSA segment.
136    #[test]
137    fn ack_generation() {
138        let m = parsed();
139        let ack = m.get_ack(false).expect("ack");
140        assert_eq!(ack.message_structure, "ACK");
141        // Sender/receiver are swapped.
142        assert_eq!(ack.get_value("MSH.3").unwrap(), "ReceivingApp");
143        assert_eq!(ack.get_value("MSH.5").unwrap(), "SendingApp");
144        assert_eq!(ack.get_value("MSA.1").unwrap(), "AA");
145        assert_eq!(ack.get_value("MSA.2").unwrap(), "MSGID1234");
146    }
147
148    /// MLLP framing wraps the message in the `<VT> … <FS><CR>` envelope.
149    #[test]
150    fn mllp_framing() {
151        let m = parsed();
152        let framed = m.get_mllp().unwrap();
153        assert_eq!(framed[0], 0x0B);
154        assert_eq!(framed[framed.len() - 2], 0x1C);
155        assert_eq!(framed[framed.len() - 1], 0x0D);
156    }
157}