hl7-net 0.1.0

Lightweight HL7 V2 parser/writer, ported from the Efferent HL7-V2 .NET library
Documentation
use crate::error::Hl7Error;

/// Recognized segment delimiters, in priority order (longest first).
const SEGMENT_DELIMITERS: [&str; 4] = ["\r\n", "\n\r", "\r", "\n"];

/// HL7 encoding/decoding rules: the field, component, repetition, escape and
/// subcomponent delimiters plus the routines that escape and unescape values.
///
/// `escape_character` uses `'\0'` to mean "no escape character" (matching the
/// `(char)0` sentinel from the original .NET implementation).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HL7Encoding {
    /// Field delimiter (`|`, encoded as `\F\`).
    pub field_delimiter: char,
    /// Component delimiter (`^`, encoded as `\S\`).
    pub component_delimiter: char,
    /// Repetition delimiter (`~`, encoded as `\R\`).
    pub repeat_delimiter: char,
    /// Escape character (`\`, encoded as `\E\`). `'\0'` means "disabled".
    pub escape_character: char,
    /// Subcomponent delimiter (`&`, encoded as `\T\`).
    pub subcomponent_delimiter: char,
    /// Segment delimiter (defaults to `\r`).
    pub segment_delimiter: String,
    /// String representation of a "present but null" value (defaults to `""`).
    pub present_but_null: String,
}

impl Default for HL7Encoding {
    fn default() -> Self {
        Self {
            field_delimiter: '|',
            component_delimiter: '^',
            repeat_delimiter: '~',
            escape_character: '\\',
            subcomponent_delimiter: '&',
            segment_delimiter: "\r".to_string(),
            present_but_null: "\"\"".to_string(),
        }
    }
}

impl HL7Encoding {
    /// Creates an encoding with the default HL7 delimiters.
    pub fn new() -> Self {
        Self::default()
    }

    /// All delimiter characters concatenated as they appear in MSH-2
    /// (`^~\&` for the defaults). The escape character is omitted when disabled.
    pub fn all_delimiters(&self) -> String {
        let mut s = String::new();
        s.push(self.field_delimiter);
        s.push(self.component_delimiter);
        s.push(self.repeat_delimiter);
        if self.escape_character != '\0' {
            s.push(self.escape_character);
        }
        s.push(self.subcomponent_delimiter);
        s
    }

    /// Sets the delimiter characters from the MSH delimiter string in the order
    /// field, component, repetition, escape, subcomponent.
    ///
    /// When the 5th character equals the field delimiter, the escape character is
    /// treated as disabled and the 4th character becomes the subcomponent delimiter.
    pub fn evaluate_delimiters(&mut self, delimiters: &str) -> Result<(), Hl7Error> {
        let chars: Vec<char> = delimiters.chars().collect();

        if chars.len() < 5 {
            return Err(Hl7Error::with_code(
                "Not enough delimiter characters in MSH segment",
                Self::bad(),
            ));
        }

        self.field_delimiter = chars[0];
        self.component_delimiter = chars[1];
        self.repeat_delimiter = chars[2];

        if chars[4] == self.field_delimiter {
            self.escape_character = '\0';
            self.subcomponent_delimiter = chars[3];
        } else {
            self.escape_character = chars[3];
            self.subcomponent_delimiter = chars[4];
        }

        Ok(())
    }

    /// Detects and stores the segment delimiter used in `message`.
    pub fn evaluate_segment_delimiter(&mut self, message: &str) -> Result<(), Hl7Error> {
        for delim in SEGMENT_DELIMITERS {
            if message.contains(delim) {
                self.segment_delimiter = delim.to_string();
                return Ok(());
            }
        }

        Err(Hl7Error::with_code(
            "Segment delimiter not found in message",
            Self::bad(),
        ))
    }

    /// Escapes HL7 special characters in `val` according to the current delimiters.
    pub fn encode(&self, val: &str) -> String {
        if val.is_empty() {
            return String::new();
        }

        let chars: Vec<char> = val.chars().collect();
        let esc = self.escape_character;
        let mut sb = String::with_capacity(val.len());
        let mut i = 0;

        while i < chars.len() {
            let c = chars[i];
            let mut continue_encoding = true;

            if c == '<' {
                continue_encoding = false;

                if i + 2 < chars.len() && chars[i + 1] == 'B' && chars[i + 2] == '>' {
                    // <B> -> highlight on
                    sb.push(esc);
                    sb.push('H');
                    sb.push(esc);
                    i += 2;
                } else if i + 3 < chars.len()
                    && chars[i + 1] == '/'
                    && chars[i + 2] == 'B'
                    && chars[i + 3] == '>'
                {
                    // </B> -> highlight off
                    sb.push(esc);
                    sb.push('N');
                    sb.push(esc);
                    i += 3;
                } else if i + 3 < chars.len()
                    && chars[i + 1] == 'B'
                    && chars[i + 2] == 'R'
                    && chars[i + 3] == '>'
                {
                    // <BR> -> line break
                    sb.push(esc);
                    sb.push_str(".br");
                    sb.push(esc);
                    i += 3;
                } else {
                    continue_encoding = true;
                }
            }

            if continue_encoding {
                if c == self.component_delimiter {
                    sb.push(esc);
                    sb.push('S');
                    sb.push(esc);
                } else if c == esc {
                    sb.push(esc);
                    sb.push('E');
                    sb.push(esc);
                } else if c == self.field_delimiter {
                    sb.push(esc);
                    sb.push('F');
                    sb.push(esc);
                } else if c == self.repeat_delimiter {
                    sb.push(esc);
                    sb.push('R');
                    sb.push(esc);
                } else if c == self.subcomponent_delimiter {
                    sb.push(esc);
                    sb.push('T');
                    sb.push(esc);
                } else if c == '\n' || c == '\r' {
                    // Preserve other non-visible characters as hex escapes.
                    let mut v = format!("{:X}", c as u32);
                    if v.len() % 2 != 0 {
                        v.insert(0, '0');
                    }
                    sb.push(esc);
                    sb.push('X');
                    sb.push_str(&v);
                    sb.push(esc);
                } else {
                    sb.push(c);
                }
            }

            i += 1;
        }

        sb
    }

    /// Convenience for serialization: encodes `Some(value)` or returns the
    /// "present but null" representation for `None`.
    pub fn encode_opt(&self, val: Option<&str>) -> String {
        match val {
            Some(v) => self.encode(v),
            None => self.present_but_null.clone(),
        }
    }

    /// Decodes an escaped HL7 string back to its original characters.
    pub fn decode(&self, encoded: &str) -> String {
        if encoded.trim().is_empty() {
            return encoded.to_string();
        }

        if !encoded.contains(self.escape_character) {
            return encoded.to_string();
        }

        let chars: Vec<char> = encoded.chars().collect();
        let esc = self.escape_character;
        let mut result = String::with_capacity(encoded.len());
        let mut i = 0;

        while i < chars.len() {
            let c = chars[i];

            if c != esc {
                result.push(c);
                i += 1;
                continue;
            }

            // Skip the opening escape character.
            i += 1;

            // Find the closing escape character.
            let li = (i..chars.len()).find(|&k| chars[k] == esc);

            match li {
                None => {
                    // Unterminated escape sequence: keep it verbatim.
                    result.push(esc);
                    if i < chars.len() {
                        result.push(chars[i]);
                    }
                    i += 1;
                }
                Some(li) => {
                    let seq: String = chars[i..li].iter().collect();

                    if seq.is_empty() {
                        i = li + 1;
                        continue;
                    }

                    match seq.as_str() {
                        "H" => result.push_str("<B>"),
                        "N" => result.push_str("</B>"),
                        "F" => result.push(self.field_delimiter),
                        "S" => result.push(self.component_delimiter),
                        "T" => result.push(self.subcomponent_delimiter),
                        "R" => result.push(self.repeat_delimiter),
                        "E" => result.push(self.escape_character),
                        ".br" => result.push_str("<BR>"),
                        _ => {
                            if let Some(hex) = seq.strip_prefix('X') {
                                result.push_str(&Self::decode_hex_string(hex));
                            } else {
                                result.push_str(&seq);
                            }
                        }
                    }

                    i = li + 1;
                }
            }
        }

        result
    }

    /// Decodes a hexadecimal string (the payload of an `\X..\` escape) into a
    /// Unicode string.
    pub fn decode_hex_string(hex: &str) -> String {
        let n = hex.len();
        let mut bytes = Vec::with_capacity(n / 2);

        let mut i = 0;
        while i + 2 <= n {
            match u8::from_str_radix(&hex[i..i + 2], 16) {
                Ok(b) => bytes.push(b),
                Err(_) => bytes.push(0),
            }
            i += 2;
        }

        if bytes.len() == 1 {
            char::from_u32(bytes[0] as u32).map(String::from).unwrap_or_default()
        } else if bytes.len() == 2 && bytes[0] == 0 {
            char::from_u32(bytes[1] as u32).map(String::from).unwrap_or_default()
        } else {
            String::from_utf8_lossy(&bytes).into_owned()
        }
    }

    fn bad() -> &'static str {
        Hl7Error::BAD_MESSAGE
    }
}