hl7-net 0.1.0

Lightweight HL7 V2 parser/writer, ported from the Efferent HL7-V2 .NET library
Documentation
//! A lightweight HL7 v2 parser/writer.
//!
//! This is an idiomatic Rust port of the
//! [Efferent HL7-V2](https://github.com/Efferent-Health/HL7-V2) .NET library.
//! The element tree ([`SubComponent`], [`Component`], [`Field`], [`Segment`],
//! [`Message`]) is pure data; encoding/decoding is driven by an [`HL7Encoding`]
//! threaded through the parse/serialize/value methods.
//!
//! # Example
//!
//! ```
//! use hl7_net::Message;
//!
//! let text = "MSH|^~\\&|App|Fac|App2|Fac2|20200101000000||ADT^A01^ADT_A01|MSGID|P|2.5\r\
//!             PID|1||PATID1234^5^M11||EVERYMAN^ADAM^A^III||19610615|M\r";
//!
//! let mut message = Message::with_message(text);
//! assert!(message.parse(false).unwrap());
//!
//! assert_eq!(message.get_value("MSH.9.1").unwrap(), "ADT");
//! assert_eq!(message.get_value("PID.5.1").unwrap(), "EVERYMAN");
//! ```

#![warn(missing_docs)]

mod component;
mod encoding;
mod error;
mod field;
pub mod helper;
mod message;
mod segment;
mod sub_component;

pub use component::Component;
pub use encoding::HL7Encoding;
pub use error::Hl7Error;
pub use field::Field;
pub use message::Message;
pub use segment::Segment;
pub use sub_component::SubComponent;

/// Compiles and runs the code examples in `README.md` as doctests.
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;

#[cfg(test)]
mod tests {
    use super::*;

    const SAMPLE: &str = "MSH|^~\\&|SendingApp|SendingFac|ReceivingApp|ReceivingFac|20200101120000||ADT^A01^ADT_A01|MSGID1234|P|2.5\r\
PID|1||PATID1234^5^M11^ADT1^MR^GOOD HEALTH HOSPITAL~123456789^^^USSSA^SS||EVERYMAN^ADAM^A^III||19610615|M\r\
NK1|1|NUCLEAR^NELDA^W|SPO^SPOUSE\r";

    /// Parses [`SAMPLE`], asserting it round-trips, and returns the message.
    fn parsed() -> Message {
        let mut m = Message::with_message(SAMPLE);
        assert!(m.parse(false).unwrap(), "message should round-trip");
        m
    }

    /// MSH-derived metadata (version, structure, control ID, etc.) is populated.
    #[test]
    fn parses_and_extracts_metadata() {
        let m = parsed();
        assert_eq!(m.version, "2.5");
        assert_eq!(m.message_structure, "ADT_A01");
        assert_eq!(m.message_control_id, "MSGID1234");
        assert_eq!(m.processing_id, "P");
        assert_eq!(m.segment_count, 3);
    }

    /// Re-serializing a parsed message reproduces the original text exactly.
    #[test]
    fn round_trip_serialization_matches() {
        let m = parsed();
        assert_eq!(m.serialize().unwrap(), SAMPLE);
    }

    /// `get_value` resolves segment/field/component/subcomponent paths, with the
    /// bare-field form returning the first repetition.
    #[test]
    fn get_value_paths() {
        let m = parsed();
        assert_eq!(m.get_value("MSH.9").unwrap(), "ADT^A01^ADT_A01");
        assert_eq!(m.get_value("MSH.9.1").unwrap(), "ADT");
        assert_eq!(m.get_value("MSH.9.3").unwrap(), "ADT_A01");
        assert_eq!(m.get_value("PID.5.1").unwrap(), "EVERYMAN");
        assert_eq!(m.get_value("PID.5.2").unwrap(), "ADAM");
        // PID.3 has repetitions; the bare index returns the first repetition.
        assert_eq!(m.get_value("PID.3.4").unwrap(), "ADT1");
    }

    /// The componentized / repetition flags reflect the parsed structure.
    #[test]
    fn flags() {
        let m = parsed();
        assert!(m.is_componentized("PID.5").unwrap());
        assert!(m.has_repetitions("PID.3").unwrap());
        assert!(!m.is_componentized("PID.1").unwrap());
    }

    /// `set_value` mutates the element tree and the change survives serialization.
    #[test]
    fn set_value_updates_tree() {
        let mut m = parsed();
        assert!(m.set_value("PID.5.1", "SMITH").unwrap());
        assert_eq!(m.get_value("PID.5.1").unwrap(), "SMITH");
        // Re-serialize and confirm the new value is present.
        assert!(m.serialize().unwrap().contains("SMITH^ADAM"));
    }

    /// Encoding escapes the HL7 delimiters and decoding restores the original.
    #[test]
    fn encode_decode_round_trip() {
        let enc = HL7Encoding::default();
        let raw = "Smith & Sons | ^Special^";
        let encoded = enc.encode(raw);
        assert!(encoded.contains("\\T\\")); // & escaped
        assert!(encoded.contains("\\F\\")); // | escaped
        assert!(encoded.contains("\\S\\")); // ^ escaped
        assert_eq!(enc.decode(&encoded), raw);
    }

    /// A `""` value decodes to `None` and `None` encodes back to `""`.
    #[test]
    fn present_but_null() {
        let enc = HL7Encoding::default();
        let sub = SubComponent::new("\"\"");
        assert_eq!(sub.value(&enc), None);
        assert_eq!(enc.encode_opt(None), "\"\"");
    }

    /// A generated ACK swaps sender/receiver and carries an `AA` MSA segment.
    #[test]
    fn ack_generation() {
        let m = parsed();
        let ack = m.get_ack(false).expect("ack");
        assert_eq!(ack.message_structure, "ACK");
        // Sender/receiver are swapped.
        assert_eq!(ack.get_value("MSH.3").unwrap(), "ReceivingApp");
        assert_eq!(ack.get_value("MSH.5").unwrap(), "SendingApp");
        assert_eq!(ack.get_value("MSA.1").unwrap(), "AA");
        assert_eq!(ack.get_value("MSA.2").unwrap(), "MSGID1234");
    }

    /// MLLP framing wraps the message in the `<VT> … <FS><CR>` envelope.
    #[test]
    fn mllp_framing() {
        let m = parsed();
        let framed = m.get_mllp().unwrap();
        assert_eq!(framed[0], 0x0B);
        assert_eq!(framed[framed.len() - 2], 0x1C);
        assert_eq!(framed[framed.len() - 1], 0x0D);
    }
}